diff --git a/apps/miniprogram/config/index.ts b/apps/miniprogram/config/index.ts index 0834fa1..6fac855 100644 --- a/apps/miniprogram/config/index.ts +++ b/apps/miniprogram/config/index.ts @@ -5,7 +5,7 @@ export default defineConfig(async (merge) => { const baseConfig = { projectName: 'hms-miniprogram', date: '2026-4-23', - designWidth: 750, + designWidth: 375, deviceRatio: { 640: 2.34 / 2, 750: 1, 375: 2, 828: 1.81 / 2 }, sourceRoot: 'src', outputRoot: 'dist', diff --git a/apps/miniprogram/project.config.json b/apps/miniprogram/project.config.json index 2b4f62c..37a9e18 100644 --- a/apps/miniprogram/project.config.json +++ b/apps/miniprogram/project.config.json @@ -3,7 +3,7 @@ "miniprogramRoot": "dist/", "compileType": "miniprogram", "setting": { - "urlCheck": true, + "urlCheck": false, "automationAudits": true, "es6": false, "enhance": false, @@ -11,6 +11,10 @@ "postcss": false, "minified": true, "bundle": false, - "minifyWXML": true - } + "minifyWXML": true, + "packNpmManually": false, + "packNpmRelationList": [], + "ignoreUploadUnusedFiles": true + }, + "condition": {} } \ No newline at end of file diff --git a/apps/miniprogram/src/app.config.ts b/apps/miniprogram/src/app.config.ts index b9c6f96..d8cfa5d 100644 --- a/apps/miniprogram/src/app.config.ts +++ b/apps/miniprogram/src/app.config.ts @@ -44,6 +44,7 @@ export default defineAppConfig({ 'dialysis-records/index', 'dialysis-records/detail/index', 'dialysis-prescriptions/index', 'dialysis-prescriptions/detail/index', 'consents/index', 'health-records/index', 'diagnoses/index', + 'elder-mode/index', ], }, { diff --git a/apps/miniprogram/src/app.scss b/apps/miniprogram/src/app.scss index 8778a00..b825c65 100644 --- a/apps/miniprogram/src/app.scss +++ b/apps/miniprogram/src/app.scss @@ -1,4 +1,5 @@ @import './styles/variables.scss'; +@import './styles/elder-mode.scss'; page { font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', diff --git a/apps/miniprogram/src/app.tsx b/apps/miniprogram/src/app.tsx index d701cac..b554bea 100644 --- a/apps/miniprogram/src/app.tsx +++ b/apps/miniprogram/src/app.tsx @@ -3,13 +3,16 @@ import Taro from '@tarojs/taro'; import ErrorBoundary from './components/ErrorBoundary'; import { flushEvents } from './services/analytics'; import { useAuthStore } from './stores/auth'; +import { useUIStore } from './stores/ui'; import './app.scss'; function App({ children }: PropsWithChildren>) { const restoreAuth = useAuthStore((s) => s.restore); + const restoreUI = useUIStore((s) => s.restore); useEffect(() => { restoreAuth(); + restoreUI(); const timer = setInterval(() => { flushEvents(); }, 30000); diff --git a/apps/miniprogram/src/components/GuestGuard/index.scss b/apps/miniprogram/src/components/GuestGuard/index.scss new file mode 100644 index 0000000..bc68b96 --- /dev/null +++ b/apps/miniprogram/src/components/GuestGuard/index.scss @@ -0,0 +1,64 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +.guard-page { + min-height: 100vh; + background: $bg; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; +} + +.guard-card { + text-align: center; + padding: 40px 20px; +} + +.guard-icon-wrap { + width: 80px; + height: 80px; + border-radius: 40px; + background: $surface-alt; + @include flex-center; + margin: 0 auto 20px; +} + +.guard-icon { + font-size: 32px; + color: $tx3; +} + +.guard-title { + font-size: 18px; + font-weight: 600; + color: $tx; + display: block; + margin-bottom: 8px; +} + +.guard-desc { + font-size: 13px; + color: $tx3; + display: block; + margin-bottom: 24px; +} + +.guard-btn { + display: inline-block; + height: 48px; + padding: 0 32px; + background: $pri; + border-radius: $r-pill; + @include flex-center; + + &:active { + opacity: 0.85; + } +} + +.guard-btn-text { + font-size: 16px; + font-weight: 600; + color: #fff; +} diff --git a/apps/miniprogram/src/components/GuestGuard/index.tsx b/apps/miniprogram/src/components/GuestGuard/index.tsx new file mode 100644 index 0000000..a64df58 --- /dev/null +++ b/apps/miniprogram/src/components/GuestGuard/index.tsx @@ -0,0 +1,28 @@ +import { View, Text } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import './index.scss'; + +interface GuestGuardProps { + title: string; + desc?: string; +} + +export default function GuestGuard({ title, desc }: GuestGuardProps) { + return ( + + + + + + {title} + {desc && {desc}} + Taro.navigateTo({ url: '/pages/login/index' })} + > + 立即登录 + + + + ); +} diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index 9952643..63c5f46 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -6,6 +6,7 @@ import { useAuthStore } from '../../stores/auth'; import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health'; import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis'; import Loading from '../../components/Loading'; +import GuestGuard from '../../components/GuestGuard'; import './index.scss'; type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight'; @@ -40,7 +41,7 @@ interface TrendPoint { export default function Health() { const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore(); - const { currentPatient } = useAuthStore(); + const { user, currentPatient } = useAuthStore(); const [activeTab, setActiveTab] = useState('blood_pressure'); const [systolic, setSystolic] = useState(''); const [diastolic, setDiastolic] = useState(''); @@ -55,6 +56,7 @@ export default function Health() { const [thresholds, setThresholds] = useState(DEFAULT_THRESHOLDS); useDidShow(() => { + if (!user) return; refreshToday(); loadTrend(activeTab); loadAiSuggestions(); @@ -62,11 +64,16 @@ export default function Health() { }); usePullDownRefresh(() => { + if (!user) return; Promise.all([refreshToday(true), loadTrend(activeTab), loadAiSuggestions()]).finally(() => { Taro.stopPullDownRefresh(); }); }); + if (!user) { + return ; + } + const loadAiSuggestions = async () => { try { const items = await listPendingSuggestions(); @@ -273,7 +280,7 @@ export default function Health() { value={systolic} onInput={(e) => setSystolic(e.detail.value)} /> - 舒张压(低压) + 舒张压(低压) + {tv} ); diff --git a/apps/miniprogram/src/pages/index/index.scss b/apps/miniprogram/src/pages/index/index.scss index b3629d7..56b9f95 100644 --- a/apps/miniprogram/src/pages/index/index.scss +++ b/apps/miniprogram/src/pages/index/index.scss @@ -1,6 +1,10 @@ @import '../../styles/variables.scss'; @import '../../styles/mixins.scss'; +/* ═══════════════════════════════════════ + 登录后首页 + ═══════════════════════════════════════ */ + .home-page { min-height: 100vh; background: $bg; @@ -8,12 +12,12 @@ padding-bottom: calc(100px + env(safe-area-inset-bottom)); } -/* ─── 区域 1:问候 + 日期 + 铃铛 ─── */ +/* ─── 问候区 ─── */ .greeting-section { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 16px; + margin-bottom: 24px; } .greeting-left { @@ -21,7 +25,8 @@ } .greeting-text { - font-size: 24px; + @include serif-number; + font-size: 26px; font-weight: 700; color: $tx; display: block; @@ -35,8 +40,10 @@ .greeting-bell { position: relative; - width: 40px; - height: 40px; + width: 44px; + height: 44px; + border-radius: 22px; + background: $pri-l; @include flex-center; flex-shrink: 0; @@ -46,16 +53,27 @@ } .greeting-bell-icon { - font-size: 22px; + font-size: 18px; + color: $pri-d; } -/* ─── 区域 2:今日体征完成度 ─── */ +.greeting-bell-dot { + position: absolute; + top: 6px; + right: 6px; + width: 8px; + height: 8px; + border-radius: 4px; + background: $dan; +} + +/* ─── 今日体征进度 ─── */ .checkin-card { background: $card; border-radius: $r; - box-shadow: $shadow-sm; - padding: 16px; - margin-bottom: 12px; + box-shadow: $shadow-md; + padding: 20px; + margin-bottom: 16px; display: flex; align-items: center; gap: 16px; @@ -106,9 +124,9 @@ } } -/* ─── 区域 3:今日体征 2x2 ─── */ +/* ─── 今日体征 2x2 ─── */ .vitals-section { - margin-bottom: 12px; + margin-bottom: 16px; } .section-title { @@ -136,37 +154,36 @@ font-size: 13px; color: $tx2; display: block; - margin-bottom: 4px; + margin-bottom: 6px; } .vital-value-row { display: flex; align-items: baseline; - margin-bottom: 4px; + margin-bottom: 6px; } .vital-value { @include serif-number; - font-size: 32px; + font-size: 30px; font-weight: 700; color: $tx; - line-height: 1.1; + line-height: 1; } .vital-unit { font-size: 12px; color: $tx3; - margin-left: 2px; + margin-left: 3px; } .vital-bottom { display: flex; align-items: center; - gap: 0; } .vital-tag { - font-size: 12px; + font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: $r-pill; @@ -189,103 +206,85 @@ } } -/* ─── 区域 4:今日待办 ─── */ -.todo-section { - margin-bottom: 12px; -} - -.todo-empty { - background: $card; +/* ─── 智能提醒卡片 ─── */ +.reminder-card { + background: linear-gradient(135deg, $pri 0%, $pri-d 100%); border-radius: $r; - padding: 24px; - text-align: center; - box-shadow: $shadow-sm; + padding: 18px; + margin-bottom: 16px; + color: #fff; } -.todo-empty-text { - font-size: 14px; - color: $tx2; +.reminder-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; } -.todo-list { - background: $card; - border-radius: $r; - overflow: hidden; - box-shadow: $shadow-sm; +.reminder-title { + font-size: 15px; + font-weight: 600; + color: #fff; } -.todo-item { +.reminder-count { + font-size: 12px; + opacity: 0.7; + color: #fff; +} + +.reminder-item { display: flex; align-items: center; - gap: 10px; - padding: 12px 16px; - border-bottom: 1px solid $bd; - - &:last-child { - border-bottom: none; - } + gap: 8px; + padding: 8px 0; &:active { - background: $bd-l; + opacity: 0.8; } } -.todo-icon-wrap { - width: 36px; - height: 36px; - border-radius: 10px; - background: $pri-l; - @include flex-center; +.reminder-item-border { + border-top: 1px solid rgba(255, 255, 255, 0.15); +} + +.reminder-tag { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.2); + font-weight: 500; + color: #fff; flex-shrink: 0; } -.todo-icon-char { - font-size: 18px; - font-weight: bold; - color: $pri; -} - -.todo-info { - flex: 1; - min-width: 0; -} - -.todo-title { - font-size: 15px; - color: $tx; - font-weight: 500; - display: block; - margin-bottom: 2px; -} - -.todo-sub { +.reminder-text { font-size: 13px; - color: $tx3; - display: block; + flex: 1; + color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.todo-arrow { - font-size: 14px; - color: $tx3; +.reminder-arrow { + opacity: 0.5; + color: #fff; flex-shrink: 0; } -/* ─── 区域 5:快捷操作 ─── */ +/* ─── 快捷操作 ─── */ .action-section { display: flex; gap: 10px; - margin-top: 16px; + margin-top: 8px; } .action-btn { flex: 1; height: 52px; border-radius: 14px; - font-size: 17px; - font-weight: 600; @include flex-center; &:active { @@ -296,6 +295,7 @@ .action-primary { background: $pri; color: #fff; + box-shadow: 0 2px 8px rgba(196, 98, 58, 0.25); } .action-outline { @@ -308,3 +308,166 @@ font-size: 17px; font-weight: 600; } + +/* ═══════════════════════════════════════ + 访客首页 + ═══════════════════════════════════════ */ + +.guest-page { + min-height: 100vh; + background: $bg; + padding-bottom: calc(120px + env(safe-area-inset-bottom)); +} + +/* ─── 轮播图 ─── */ +.guest-swiper { + width: 100%; + height: 360px; +} + +.guest-slide { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} + +.guest-slide-bg { + position: absolute; + inset: 0; + + &--1 { + background: linear-gradient(135deg, $pri-d 0%, $pri 60%, $pri-l 100%); + } +} + +.guest-slide:nth-child(2) .guest-slide-bg { + background: linear-gradient(135deg, $acc 0%, #3D5A40 60%, $acc-l 100%); +} + +.guest-slide:nth-child(3) .guest-slide-bg { + background: linear-gradient(135deg, #8B6F4E 0%, $wrn 60%, $wrn-l 100%); +} + +.guest-slide-content { + position: relative; + z-index: 1; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + padding: 40px 32px; +} + +.guest-slide-title { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 26px; + font-weight: 700; + color: #FFFFFF; + display: block; + margin-bottom: 8px; +} + +.guest-slide-desc { + font-size: 16px; + color: rgba(255, 255, 255, 0.85); + display: block; +} + +/* ─── 健康资讯 ─── */ +.guest-section { + padding: 24px 24px 0; +} + +.guest-section-title { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 22px; + font-weight: bold; + color: $tx; + display: block; + margin-bottom: 16px; +} + +.guest-articles { + display: flex; + flex-direction: column; + gap: 12px; +} + +.guest-article-card { + background: $card; + border-radius: $r; + padding: 16px 18px; + box-shadow: $shadow-sm; + + &:active { + opacity: 0.85; + } +} + +.guest-article-title { + font-size: 16px; + font-weight: 600; + color: $tx; + display: block; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.guest-article-summary { + font-size: 13px; + color: $tx3; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.guest-empty { + padding: 40px 0; + text-align: center; +} + +.guest-empty-text { + font-size: 14px; + color: $tx3; +} + +/* ─── 底部登录引导 ─── */ +.guest-login-prompt { + margin: 24px 24px 0; + background: $card; + border-radius: $r; + padding: 20px; + box-shadow: $shadow-md; + display: flex; + align-items: center; + gap: 16px; +} + +.guest-login-text { + flex: 1; + font-size: 13px; + color: $tx2; +} + +.guest-login-btn { + height: 56px; + padding: 0 28px; + background: $pri; + border-radius: $r-pill; + @include flex-center; + flex-shrink: 0; + + &:active { + opacity: 0.85; + } +} + +.guest-login-btn-text { + font-size: 20px; + font-weight: 600; + color: #fff; +} diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index adb06af..fc6a179 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -1,7 +1,8 @@ -import { View, Text } from '@tarojs/components'; -import { useState } from 'react'; +import { View, Text, Swiper, SwiperItem } from '@tarojs/components'; +import { useState, useCallback } from 'react'; import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; import { useAuthStore } from '../../stores/auth'; +import { useUIStore } from '../../stores/ui'; import { useHealthStore } from '../../stores/health'; import ProgressRing from '../../components/ProgressRing'; import Loading from '../../components/Loading'; @@ -10,6 +11,7 @@ import * as appointmentApi from '@/services/appointment'; import * as followupApi from '@/services/followup'; import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis'; import { notificationService } from '@/services/notification'; +import * as articleApi from '@/services/article'; import './index.scss'; interface ReminderItem { @@ -19,7 +21,103 @@ interface ReminderItem { tag: string; } -export default function Index() { +// ─── 访客首页 ─── + +const CAROUSEL_SLIDES = [ + { id: 'slide-1', title: '专业血透中心', desc: '三甲级医护团队全程守护' }, + { id: 'slide-2', title: '智慧健康管理', desc: 'AI 驱动个性化健康方案' }, + { id: 'slide-3', title: '温馨就医环境', desc: '舒适安全的治疗体验' }, +]; + +function GuestHome({ modeClass }: { modeClass: string }) { + const [articles, setArticles] = useState([]); + const [loading, setLoading] = useState(false); + + useDidShow(() => { + loadArticles(); + trackPageView('guest-home'); + }); + + const loadArticles = async () => { + setLoading(true); + try { + const res = await articleApi.listArticles({ page: 1, page_size: 4 }); + setArticles(res.data || []); + } catch { + // 文章加载失败不阻塞 + } finally { + setLoading(false); + } + }; + + return ( + + {/* 轮播图 */} + + {CAROUSEL_SLIDES.map((slide) => ( + + + + + {slide.title} + {slide.desc} + + + + ))} + + + {/* 健康资讯 */} + + 健康资讯 + {loading ? ( + + ) : articles.length > 0 ? ( + + {articles.map((a) => ( + Taro.navigateTo({ url: `/pages/article/detail/index?id=${a.id}` })} + > + {a.title} + {a.summary && {a.summary}} + + ))} + + ) : ( + + 暂无文章 + + )} + + + {/* 底部登录引导 */} + + 登录后即可使用完整健康管理服务 + Taro.navigateTo({ url: '/pages/login/index' })} + > + 立即登录 + + + + ); +} + +// ─── 登录后首页 ─── + +function HomeDashboard({ modeClass }: { modeClass: string }) { const { user, currentPatient } = useAuthStore(); const { todaySummary, loading, refreshToday } = useHealthStore(); const [reminders, setReminders] = useState([]); @@ -55,7 +153,6 @@ export default function Index() { setRemindersLoading(true); try { const items: ReminderItem[] = []; - const [apptRes, taskRes, suggestRes] = await Promise.allSettled([ appointmentApi.listAppointments(patientId, 1), followupApi.listTasks(patientId, 'pending'), @@ -64,15 +161,9 @@ export default function Index() { if (suggestRes.status === 'fulfilled') { for (const s of suggestRes.value.data.slice(0, 1)) { - items.push({ - id: s.id, - text: buildSuggestionText(s), - type: 'ai', - tag: 'AI 建议', - }); + items.push({ id: s.id, text: buildSuggestionText(s), type: 'ai', tag: 'AI 建议' }); } } - if (apptRes.status === 'fulfilled') { for (const a of apptRes.value.data.slice(0, 1)) { if (a.status === 'pending' || a.status === 'confirmed') { @@ -85,7 +176,6 @@ export default function Index() { } } } - if (taskRes.status === 'fulfilled') { for (const t of taskRes.value.data.slice(0, 1)) { items.push({ @@ -96,7 +186,6 @@ export default function Index() { }); } } - setReminders(items.slice(0, 3)); } catch { setReminders([]); @@ -110,12 +199,7 @@ export default function Index() { const displayName = user?.display_name || currentPatient?.name || '访客'; const summary = todaySummary || {}; - const indicators = [ - !!summary.blood_pressure, - !!summary.heart_rate, - !!summary.blood_sugar, - !!summary.weight, - ]; + const indicators = [!!summary.blood_pressure, !!summary.heart_rate, !!summary.blood_sugar, !!summary.weight]; const completedCount = indicators.filter(Boolean).length; const progressPercent = Math.round((completedCount / 4) * 100); @@ -140,7 +224,7 @@ export default function Index() { }; return ( - + {/* 问候区 */} @@ -149,20 +233,14 @@ export default function Index() { {new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })} - Taro.switchTab({ url: '/pages/messages/index' })} - > + Taro.switchTab({ url: '/pages/messages/index' })}> {unreadCount > 0 && } {/* 今日体征进度 */} - Taro.switchTab({ url: '/pages/health/index' })} - > + Taro.switchTab({ url: '/pages/health/index' })}> @@ -172,10 +250,7 @@ export default function Index() { {indicatorCapsules.map((cap) => ( - + {cap.done ? '✓ ' : ''}{cap.label} ))} @@ -226,11 +301,8 @@ export default function Index() { key={r.id} className={`reminder-item ${i > 0 ? 'reminder-item-border' : ''}`} onClick={() => { - if (r.type === 'appointment') { - Taro.navigateTo({ url: '/pages/appointment/index' }); - } else if (r.type === 'followup') { - Taro.navigateTo({ url: `/pages/followup/detail/index?id=${r.id}` }); - } + if (r.type === 'appointment') Taro.navigateTo({ url: '/pages/appointment/index' }); + else if (r.type === 'followup') Taro.navigateTo({ url: `/pages/followup/detail/index?id=${r.id}` }); }} > {r.tag} @@ -243,16 +315,10 @@ export default function Index() { {/* 快捷操作 */} - Taro.switchTab({ url: '/pages/health/index' })} - > + Taro.switchTab({ url: '/pages/health/index' })}> 记录体征 - Taro.navigateTo({ url: '/pages/appointment/create/index' })} - > + Taro.navigateTo({ url: '/pages/appointment/create/index' })}> 预约挂号 @@ -260,12 +326,21 @@ export default function Index() { ); } +// ─── 首页入口:根据登录状态切换 ─── + +export default function Index() { + const user = useAuthStore((s) => s.user); + const mode = useUIStore((s) => s.mode); + const modeClass = mode === 'elder' ? 'elder-mode' : ''; + + if (!user) { + return ; + } + return ; +} + function buildSuggestionText(s: AiSuggestionItem): string { - const riskMap: Record = { - high: '高风险', - medium: '中风险', - low: '低风险', - }; + const riskMap: Record = { high: '高风险', medium: '中风险', low: '低风险' }; const typeMap: Record = { vital_sign_anomaly: '体征异常', lab_result_anomaly: '化验异常', diff --git a/apps/miniprogram/src/pages/login/index.scss b/apps/miniprogram/src/pages/login/index.scss index f4fdd07..4bc4ee4 100644 --- a/apps/miniprogram/src/pages/login/index.scss +++ b/apps/miniprogram/src/pages/login/index.scss @@ -136,3 +136,16 @@ color: $pri; font-weight: 500; } + +/* ─── 暂不登录 ─── */ +.skip-row { + width: 100%; + text-align: center; + margin-top: 32px; +} + +.skip-btn { + font-size: 20px; + color: $tx3; + padding: 8px 16px; +} diff --git a/apps/miniprogram/src/pages/login/index.tsx b/apps/miniprogram/src/pages/login/index.tsx index 992f11e..df9e265 100644 --- a/apps/miniprogram/src/pages/login/index.tsx +++ b/apps/miniprogram/src/pages/login/index.tsx @@ -102,6 +102,13 @@ export default function Login() { Taro.navigateTo({ url: '/pages/legal/privacy-policy' })}>《隐私政策》 + + {/* 暂不登录 */} + + Taro.reLaunch({ url: '/pages/index/index' })}> + 暂不登录,先看看 + + ); diff --git a/apps/miniprogram/src/pages/messages/index.tsx b/apps/miniprogram/src/pages/messages/index.tsx index dfa3eff..f0af42b 100644 --- a/apps/miniprogram/src/pages/messages/index.tsx +++ b/apps/miniprogram/src/pages/messages/index.tsx @@ -4,6 +4,8 @@ import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'; import { listConsultations, ConsultationSession } from '../../services/consultation'; import { notificationService } from '../../services/notification'; import Loading from '../../components/Loading'; +import GuestGuard from '../../components/GuestGuard'; +import { useAuthStore } from '../../stores/auth'; import './index.scss'; type MsgTab = 'consultation' | 'notification'; @@ -14,9 +16,19 @@ interface NotificationItem { desc: string; time: string; type: string; + read?: boolean; } +const NOTIFY_ICONS: Record = { + appointment: { icon: '约', bg: '#F0DDD4', color: '#C4623A' }, + alert: { icon: '警', bg: '#FFF3E0', color: '#C4873A' }, + followup: { icon: '随', bg: '#E8F0E8', color: '#5B7A5E' }, + points: { icon: '分', bg: '#F0DDD4', color: '#C4623A' }, + report: { icon: '报', bg: '#E8F0E8', color: '#5B7A5E' }, +}; + export default function Messages() { + const user = useAuthStore((s) => s.user); const [activeTab, setActiveTab] = useState('consultation'); const [sessions, setSessions] = useState([]); const [notifications, setNotifications] = useState([]); @@ -25,10 +37,6 @@ export default function Messages() { const [total, setTotal] = useState(0); const loadingRef = useRef(false); - useDidShow(() => { - loadData(activeTab, 1, true); - }); - const loadData = async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => { if (loadingRef.current) return; loadingRef.current = true; @@ -65,6 +73,10 @@ export default function Messages() { } }; + useDidShow(() => { + if (user) loadData(activeTab, 1, true); + }); + const handleTabChange = (tab: MsgTab) => { setActiveTab(tab); loadData(tab, 1, true); @@ -89,6 +101,12 @@ export default function Messages() { return dateStr.slice(0, 10); }; + if (!user) { + return ; + } + + const unreadConsultCount = sessions.filter((s) => s.unread_count_patient > 0).length; + return ( {/* 页头 */} @@ -96,87 +114,110 @@ export default function Messages() { 消息 - {/* Tab 切换 */} - + {/* 分段控件 Tab */} + handleTabChange('consultation')} > - 咨询 + 咨询 + {unreadConsultCount > 0 && ( + + {unreadConsultCount} + + )} handleTabChange('notification')} > - 通知 + 通知 - - - - {/* 咨询列表 */} - {activeTab === 'consultation' && ( - - {loading ? ( + + {/* 咨询列表 */} + {activeTab === 'consultation' && ( + loading ? ( ) : sessions.length === 0 ? ( 暂无咨询消息 ) : ( - sessions.map((session) => ( - Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` })} - > - - - {session.consultation_type === 'online' ? '在线咨询' : '门诊咨询'} - - - {session.last_message || session.subject || '暂无消息'} - - - - {formatTime(session.last_message_at)} - {session.unread_count_patient > 0 && ( - - - {session.unread_count_patient > 99 ? '99+' : session.unread_count_patient} - + + {sessions.map((session) => { + const doctorName = session.last_message?.slice(0, 1) || '医'; + const hasUnread = session.unread_count_patient > 0; + return ( + Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` })} + > + + {doctorName} - )} - - - )) - )} - - )} + + + + {session.consultation_type === 'online' ? '在线咨询' : '门诊咨询'} + + {formatTime(session.last_message_at)} + + + + {session.last_message || session.subject || '暂无消息'} + + {hasUnread && ( + + + {session.unread_count_patient > 99 ? '99+' : session.unread_count_patient} + + + )} + + + + ); + })} + + ) + )} - {/* 通知列表 */} - {activeTab === 'notification' && ( - - {loading ? ( + {/* 通知列表 */} + {activeTab === 'notification' && ( + loading ? ( ) : notifications.length === 0 ? ( 暂无新通知 ) : ( - notifications.map((n) => ( - - - {n.title} - {n.desc} - - {n.time} - - )) - )} - - )} + + {notifications.map((n) => { + const cfg = NOTIFY_ICONS[n.type] || NOTIFY_ICONS.report; + const isUnread = !n.read; + return ( + + + {cfg.icon} + + + + {n.title} + {n.time} + + {n.desc} + + {isUnread && } + + ); + })} + + ) + )} + ); } diff --git a/apps/miniprogram/src/pages/pkg-profile/elder-mode/index.scss b/apps/miniprogram/src/pages/pkg-profile/elder-mode/index.scss new file mode 100644 index 0000000..469ccd4 --- /dev/null +++ b/apps/miniprogram/src/pages/pkg-profile/elder-mode/index.scss @@ -0,0 +1,132 @@ +@import '../../../styles/variables.scss'; +@import '../../../styles/mixins.scss'; + +.elder-mode-page { + min-height: 100vh; + background: $bg; + padding: 24px; +} + +.elder-mode-card { + background: $card; + border-radius: $r; + padding: 24px; + box-shadow: $shadow-md; + margin-bottom: 20px; +} + +.elder-mode-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; +} + +.elder-mode-icon { + width: 48px; + height: 48px; + border-radius: $r-sm; + background: $acc-l; + @include flex-center; + font-size: 22px; + font-weight: 700; + color: $acc; + flex-shrink: 0; +} + +.elder-mode-info { + flex: 1; +} + +.elder-mode-title { + font-size: 18px; + font-weight: 700; + color: $tx; + display: block; + margin-bottom: 4px; +} + +.elder-mode-desc { + font-size: 13px; + color: $tx3; +} + +.elder-mode-status { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 0 0; + border-top: 1px solid $bd-l; +} + +.elder-mode-status-text { + font-size: 15px; + color: $tx2; +} + +.elder-mode-switch { + width: 52px; + height: 30px; + border-radius: 15px; + background: $bd; + position: relative; + transition: background 0.25s; + + &--on { + background: $acc; + } +} + +.elder-mode-switch-thumb { + width: 26px; + height: 26px; + border-radius: 13px; + background: #fff; + position: absolute; + top: 2px; + left: 2px; + transition: transform 0.25s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + + .elder-mode-switch--on & { + transform: translateX(22px); + } +} + +/* ─── 效果预览 ─── */ +.elder-mode-preview { + margin-top: 4px; +} + +.elder-mode-preview-title { + font-size: 14px; + font-weight: 600; + color: $tx2; + display: block; + margin-bottom: 10px; + padding-left: 4px; +} + +.elder-mode-preview-card { + background: $card; + border-radius: $r; + padding: 20px; + box-shadow: $shadow-sm; +} + +.elder-mode-preview-sample { + font-size: 16px; + color: $tx; + display: block; + margin-bottom: 8px; + transition: font-size 0.25s; + + &--large { + font-size: 21px; + } +} + +.elder-mode-preview-note { + font-size: 13px; + color: $tx3; +} diff --git a/apps/miniprogram/src/pages/pkg-profile/elder-mode/index.tsx b/apps/miniprogram/src/pages/pkg-profile/elder-mode/index.tsx new file mode 100644 index 0000000..a5f7d71 --- /dev/null +++ b/apps/miniprogram/src/pages/pkg-profile/elder-mode/index.tsx @@ -0,0 +1,58 @@ +import { View, Text } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import { useUIStore } from '../../../stores/ui'; +import './index.scss'; + +export default function ElderMode() { + const mode = useUIStore((s) => s.mode); + const setMode = useUIStore((s) => s.setMode); + const isElder = mode === 'elder'; + + const handleToggle = () => { + const next = isElder ? 'normal' : 'elder'; + setMode(next); + Taro.showToast({ + title: next === 'elder' ? '已开启长辈模式' : '已关闭长辈模式', + icon: 'none', + duration: 1500, + }); + }; + + return ( + + + + + + 长辈模式 + 放大字体和按钮,更易阅读和操作 + + + + + + 当前状态:{isElder ? '已开启' : '已关闭'} + + + + + + + + + 效果预览 + + + {isElder ? '长辈模式字体示例' : '标准模式字体示例'} + + + {isElder ? '字号放大 1.3 倍,间距放大 1.2 倍' : '正常字号和间距'} + + + + + ); +} diff --git a/apps/miniprogram/src/pages/profile/index.scss b/apps/miniprogram/src/pages/profile/index.scss index 41431fc..4947b80 100644 --- a/apps/miniprogram/src/pages/profile/index.scss +++ b/apps/miniprogram/src/pages/profile/index.scss @@ -4,7 +4,7 @@ .profile-page { min-height: 100vh; background: $bg; - padding: 20px 16px 100px; + padding: 20px 24px 100px; padding-bottom: calc(100px + env(safe-area-inset-bottom)); } @@ -12,25 +12,28 @@ .profile-user-card { background: $card; border-radius: $r; - padding: 16px; + padding: 20px; display: flex; align-items: center; - gap: 14px; - box-shadow: $shadow-sm; - margin-bottom: 10px; + gap: 16px; + box-shadow: $shadow-md; + margin-bottom: 16px; } .profile-avatar { width: 60px; height: 60px; border-radius: 30px; - background: $pri-l; + background: linear-gradient(135deg, $pri-l 0%, $pri 100%); @include flex-center; flex-shrink: 0; } -.profile-avatar-icon { +.profile-avatar-char { + font-family: Georgia, 'Times New Roman', serif; font-size: 28px; + font-weight: 700; + color: #fff; } .profile-user-info { @@ -39,6 +42,7 @@ } .profile-name { + @include serif-number; font-size: 22px; font-weight: 700; color: $tx; @@ -51,11 +55,17 @@ color: $tx3; } -/* ─── 积分 + 打卡并排 ─── */ +.profile-arrow { + font-size: 16px; + color: $tx3; + flex-shrink: 0; +} + +/* ─── 积分 + 打卡 ─── */ .profile-stats-row { display: flex; gap: 10px; - margin-bottom: 12px; + margin-bottom: 24px; } .stat-card { @@ -69,10 +79,10 @@ .stat-value { @include serif-number; - font-size: 24px; + font-size: 28px; font-weight: 700; display: block; - margin-bottom: 4px; + margin-bottom: 2px; &.stat-pri { color: $pri; @@ -83,13 +93,31 @@ } } +.stat-unit { + font-size: 16px; + font-weight: 400; +} + .stat-label { font-size: 13px; color: $tx3; } -/* ─── 菜单 ─── */ -.profile-menu { +/* ─── 分组菜单 ─── */ +.menu-group { + margin-bottom: 14px; +} + +.menu-group-title { + font-size: 14px; + font-weight: 600; + color: $tx2; + display: block; + margin-bottom: 8px; + padding-left: 4px; +} + +.menu-group-card { background: $card; border-radius: $r; overflow: hidden; @@ -100,22 +128,27 @@ display: flex; align-items: center; gap: 14px; - padding: 18px 16px; - border-bottom: 1px solid $bd; - - &:last-child { - border-bottom: none; - } + padding: 14px 16px; + position: relative; &:active { background: $bd-l; } } +.menu-divider { + position: absolute; + left: 66px; + right: 16px; + bottom: 0; + height: 1px; + background: $bd-l; +} + .menu-icon { - width: 40px; - height: 40px; - border-radius: 12px; + width: 36px; + height: 36px; + border-radius: $r-sm; @include flex-center; flex-shrink: 0; @@ -133,15 +166,26 @@ } .menu-icon-text { - font-family: Georgia, Times New Roman, serif; - font-size: 20px; - font-weight: 600; - color: $pri-d; + @include serif-number; + font-size: 16px; + font-weight: 700; + + &.menu-icon-text--pri { + color: $pri; + } + + &.menu-icon-text--acc { + color: $acc; + } + + &.menu-icon-text--tx3 { + color: $tx3; + } } .menu-label { flex: 1; - font-size: 16px; + font-size: 15px; color: $tx; } @@ -153,8 +197,9 @@ /* ─── 退出登录 ─── */ .profile-logout { - margin-top: 24px; + margin-top: 16px; text-align: center; + padding: 16px 0; } .logout-text { diff --git a/apps/miniprogram/src/pages/profile/index.tsx b/apps/miniprogram/src/pages/profile/index.tsx index b82f3da..b9c17e7 100644 --- a/apps/miniprogram/src/pages/profile/index.tsx +++ b/apps/miniprogram/src/pages/profile/index.tsx @@ -1,51 +1,96 @@ -import React from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { useAuthStore } from '../../stores/auth'; import { usePointsStore } from '../../stores/points'; +import { useUIStore } from '../../stores/ui'; import './index.scss'; -const MENU_ITEMS = [ - { label: '就诊人管理', icon: '家', bg: 'pri-l' }, - { label: '我的报告', icon: '报', bg: 'acc-l' }, - { label: '健康记录', icon: '健', bg: 'pri-l' }, - { label: '诊断记录', icon: '诊', bg: 'acc-l' }, - { label: '我的随访', icon: '随', bg: 'pri-l' }, - { label: '我的预约', icon: '约', bg: 'acc-l' }, - { label: '用药记录', icon: '药', bg: 'pri-l' }, - { label: '透析记录', icon: '透', bg: 'acc-l' }, - { label: '知情同意', icon: '知', bg: 'pri-l' }, - { label: '线下活动', icon: '活', bg: 'acc-l' }, - { label: '在线咨询', icon: '问', bg: 'pri-l' }, - { label: '设置', icon: '设', bg: 'surface-alt' }, +interface MenuItem { + label: string; + icon: string; + bg: string; + color: string; + path: string; + isSwitchTab?: boolean; +} + +interface MenuGroup { + title: string; + items: MenuItem[]; +} + +const LOGGED_IN_GROUPS: MenuGroup[] = [ + { + title: '健康管理', + items: [ + { label: '健康记录', icon: '健', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/health-records/index' }, + { label: '我的报告', icon: '报', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/reports/index' }, + { label: 'AI 分析', icon: '智', bg: 'pri-l', color: 'pri', path: '/pages/ai-report/list/index' }, + { label: '诊断记录', icon: '诊', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/diagnoses/index' }, + { label: '用药记录', icon: '药', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/medication/index' }, + ], + }, + { + title: '就诊服务', + items: [ + { label: '我的预约', icon: '约', bg: 'pri-l', color: 'pri', path: '/pages/appointment/index' }, + { label: '我的随访', icon: '随', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/followups/index' }, + { label: '在线咨询', icon: '问', bg: 'pri-l', color: 'pri', path: '/pages/consultation/index' }, + ], + }, + { + title: '透析管理', + items: [ + { label: '透析记录', icon: '透', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/dialysis-records/index' }, + { label: '透析处方', icon: '方', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/dialysis-prescriptions/index' }, + { label: '知情同意', icon: '知', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/consents/index' }, + ], + }, + { + title: '生活服务', + items: [ + { label: '积分商城', icon: '礼', bg: 'pri-l', color: 'pri', path: '/pages/mall/index', isSwitchTab: true }, + { label: '线下活动', icon: '活', bg: 'acc-l', color: 'acc', path: '/pages/events/index' }, + ], + }, + { + title: '账号', + items: [ + { label: '就诊人管理', icon: '家', bg: 'pri-l', color: 'pri', path: '/pages/pkg-profile/family/index' }, + { label: '长辈模式', icon: '老', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/elder-mode/index' }, + { label: '设备同步', icon: '设', bg: 'surface-alt', color: 'tx3', path: '/pages/device-sync/index' }, + { label: '设置', icon: '齿', bg: 'surface-alt', color: 'tx3', path: '/pages/pkg-profile/settings/index' }, + ], + }, ]; -const MENU_PATHS: Record = { - '就诊人管理': '/pages/pkg-profile/family/index', - '我的报告': '/pages/pkg-profile/reports/index', - '健康记录': '/pages/pkg-profile/health-records/index', - '诊断记录': '/pages/pkg-profile/diagnoses/index', - '我的随访': '/pages/pkg-profile/followups/index', - '我的预约': '/pages/appointment/index', - '用药记录': '/pages/pkg-profile/medication/index', - '透析记录': '/pages/pkg-profile/dialysis-records/index', - '知情同意': '/pages/pkg-profile/consents/index', - '线下活动': '/pages/events/index', - '在线咨询': '/pages/consultation/index', - '设置': '/pages/pkg-profile/settings/index', -}; +const GUEST_GROUPS: MenuGroup[] = [ + { + title: '设置', + items: [ + { label: '长辈模式', icon: '老', bg: 'acc-l', color: 'acc', path: '/pages/pkg-profile/elder-mode/index' }, + { label: '设置', icon: '齿', bg: 'surface-alt', color: 'tx3', path: '/pages/pkg-profile/settings/index' }, + ], + }, +]; export default function Profile() { const { user, logout } = useAuthStore(); const { account: pointsAccount, checkinStatus: checkinInfo, refresh: refreshPoints } = usePointsStore(); + const mode = useUIStore((s) => s.mode); + const modeClass = mode === 'elder' ? 'elder-mode' : ''; + const isGuest = !user; useDidShow(() => { - refreshPoints(); + if (!isGuest) refreshPoints(); }); - const handleMenuClick = (label: string) => { - const path = MENU_PATHS[label]; - if (path) Taro.navigateTo({ url: path }); + const handleMenuClick = (item: MenuItem) => { + if (item.isSwitchTab) { + Taro.switchTab({ url: item.path }); + } else { + Taro.navigateTo({ url: item.path }); + } }; const handleLogout = () => { @@ -59,54 +104,84 @@ export default function Profile() { }); }; + const groups = isGuest ? GUEST_GROUPS : LOGGED_IN_GROUPS; + return ( - + {/* 用户信息卡片 */} - - - - {(user?.display_name || '访').charAt(0)} - - - - {user?.display_name || '未登录'} - {user?.phone || ''} - - - - {/* 积分 + 打卡 — 两个独立卡片并排 */} - - - {(pointsAccount?.balance ?? 0).toLocaleString()} - 健康积分 - - - {checkinInfo?.consecutive_days ?? 0}天 - 连续打卡 - - - - {/* 菜单 */} - - {MENU_ITEMS.map((item) => ( - handleMenuClick(item.label)} - > - - {item.icon} - - {item.label} - + {isGuest ? ( + Taro.navigateTo({ url: '/pages/login/index' })}> + + ? + + + 未登录 + 点击登录,开启健康管理之旅 + + + + ) : ( + <> + + + {(user?.display_name || '访').charAt(0)} + + + {user?.display_name || '访客'} + + {user?.phone ? `${user.phone.slice(0, 3)}****${user.phone.slice(-4)}` : ''} + + + - ))} - - {/* 退出登录 */} - - 退出登录 - + {/* 积分 + 打卡 */} + + + {(pointsAccount?.balance ?? 0).toLocaleString()} + 健康积分 + + + {checkinInfo?.consecutive_days ?? 0} + 连续打卡 + + + + )} + + {/* 分组菜单 */} + {groups.map((group) => ( + + {group.title} + + {group.items.map((item, idx) => ( + handleMenuClick(item)} + > + + {item.icon} + + {item.label} + {idx < group.items.length - 1 && } + + + ))} + + + ))} + + {/* 退出登录 / 登录 */} + {isGuest ? ( + Taro.navigateTo({ url: '/pages/login/index' })}> + 登录账号 + + ) : ( + + 退出登录 + + )} ); } diff --git a/apps/miniprogram/src/services/request.ts b/apps/miniprogram/src/services/request.ts index 14d11f2..76d21a9 100644 --- a/apps/miniprogram/src/services/request.ts +++ b/apps/miniprogram/src/services/request.ts @@ -65,12 +65,12 @@ async function doRefresh(): Promise { } // --- Core request --- -async function request(method: string, path: string, data?: unknown): Promise { +async function request(method: string, path: string, data?: unknown, timeout?: number): Promise { const headers = await getHeaders(); const url = `${BASE_URL}${path}`; let res: Taro.request.SuccessCallbackResult; try { - res = await Taro.request({ url, method: method as any, data, header: headers, timeout: 15000 }); + res = await Taro.request({ url, method: method as any, data, header: headers, timeout: timeout || 15000 }); } catch (err: any) { const msg = err?.errMsg || ''; if (msg.includes('timeout')) { @@ -82,12 +82,15 @@ async function request(method: string, path: string, data?: unknown): Promise } if (res.statusCode === 401) { - const refreshed = await tryRefreshToken(); - if (refreshed) return request(method, path, data); - const pages = Taro.getCurrentPages(); - const currentPath = pages[pages.length - 1]?.path || ''; - if (!currentPath.includes('pages/login')) { - Taro.reLaunch({ url: '/pages/login/index' }); + const hasToken = !!safeGet('access_token'); + if (hasToken) { + const refreshed = await tryRefreshToken(); + if (refreshed) return request(method, path, data); + const pages = Taro.getCurrentPages(); + const currentPath = pages[pages.length - 1]?.path || ''; + if (!currentPath.includes('pages/login')) { + Taro.reLaunch({ url: '/pages/index/index' }); + } } throw new Error('登录已过期'); } @@ -172,3 +175,7 @@ export const api = { put: (path: string, data?: unknown) => request('PUT', path, data), delete: (path: string, data?: unknown) => request('DELETE', path, data), }; + +export async function requestWithTimeout(method: string, path: string, data?: unknown, timeout?: number): Promise { + return request(method, path, data, timeout); +} diff --git a/apps/miniprogram/src/stores/auth.ts b/apps/miniprogram/src/stores/auth.ts index 39866bd..156bc66 100644 --- a/apps/miniprogram/src/stores/auth.ts +++ b/apps/miniprogram/src/stores/auth.ts @@ -166,6 +166,6 @@ export const useAuthStore = create((set, get) => ({ Taro.removeStorageSync('analytics_queue'); Taro.removeStorageSync('edit_patient'); set({ user: null, roles: [], currentPatient: null, patients: [] }); - Taro.redirectTo({ url: '/pages/login/index' }); + Taro.reLaunch({ url: '/pages/index/index' }); }, })); diff --git a/apps/miniprogram/src/stores/ui.ts b/apps/miniprogram/src/stores/ui.ts new file mode 100644 index 0000000..5c15f21 --- /dev/null +++ b/apps/miniprogram/src/stores/ui.ts @@ -0,0 +1,37 @@ +import { create } from 'zustand'; +import Taro from '@tarojs/taro'; + +type DisplayMode = 'normal' | 'elder'; + +interface UIState { + mode: DisplayMode; + toggle: () => void; + setMode: (mode: DisplayMode) => void; + restore: () => void; +} + +const STORAGE_KEY = 'ui_display_mode'; + +export const useUIStore = create((set, get) => ({ + mode: 'normal', + + toggle: () => { + const next = get().mode === 'normal' ? 'elder' : 'normal'; + Taro.setStorageSync(STORAGE_KEY, next); + set({ mode: next }); + }, + + setMode: (mode) => { + Taro.setStorageSync(STORAGE_KEY, mode); + set({ mode }); + }, + + restore: () => { + try { + const saved = Taro.getStorageSync(STORAGE_KEY); + if (saved === 'elder' || saved === 'normal') { + set({ mode: saved }); + } + } catch { /* storage 不可用时保持默认 */ } + }, +})); diff --git a/apps/miniprogram/src/styles/elder-mode.scss b/apps/miniprogram/src/styles/elder-mode.scss new file mode 100644 index 0000000..254b25f --- /dev/null +++ b/apps/miniprogram/src/styles/elder-mode.scss @@ -0,0 +1,173 @@ +// 长辈模式 CSS 覆写 +// 字号 ×1.3 / 间距 ×1.2 / 按钮 48→60px +// 通过页面根 View 添加 .elder-mode class 激活 + +.elder-mode { + font-size: 36px; // 28 × 1.3 + + // ─── 全局触控放大 ─── + .vital-card, + .checkin-card, + .reminder-item, + .menu-item, + .action-btn { + min-height: 60px; + } + + .action-btn { + height: 64px; + } + + .action-btn-text { + font-size: 22px; // 17 × 1.3 + } + + // ─── 首页 ─── + .greeting-text { + font-size: 34px; // 26 × 1.3 + } + + .checkin-title { + font-size: 21px; // 16 × 1.3 + } + + .vital-label { + font-size: 17px; // 13 × 1.3 + } + + .vital-value { + font-size: 39px; // 30 × 1.3 + } + + .vital-tag { + font-size: 14px; // 11 × 1.3 + padding: 3px 10px; + } + + .capsule { + font-size: 14px; // 11 × 1.3 + padding: 4px 10px; + } + + .reminder-title { + font-size: 20px; // 15 × 1.3 + } + + .reminder-text { + font-size: 17px; // 13 × 1.3 + } + + .section-title { + font-size: 34px; // 26 × 1.3 + } + + // ─── 个人页 ─── + .profile-name { + font-size: 29px; // 22 × 1.3 + } + + .stat-value { + font-size: 36px; // 28 × 1.3 + } + + .stat-label { + font-size: 17px; // 13 × 1.3 + } + + .menu-group-title { + font-size: 18px; // 14 × 1.3 + } + + .menu-label { + font-size: 20px; // 15 × 1.3 + } + + .menu-icon { + width: 44px; + height: 44px; + border-radius: 14px; + } + + .menu-icon-text { + font-size: 21px; // 16 × 1.3 + } + + .logout-text { + font-size: 18px; // 14 × 1.3 + } + + // ─── 访客首页 ─── + .guest-slide-title { + font-size: 34px; // 26 × 1.3 + } + + .guest-slide-desc { + font-size: 21px; // 16 × 1.3 + } + + .guest-article-title { + font-size: 21px; // 16 × 1.3 + } + + .guest-article-summary { + font-size: 17px; // 13 × 1.3 + } + + .guest-login-btn { + height: 72px; // 56 × 1.3 + font-size: 26px; // 20 × 1.3 + } + + .guest-institution-name { + font-size: 21px; // 16 × 1.3 + } + + .guest-institution-desc { + font-size: 17px; // 13 × 1.3 + } + + // ─── 登录页 ─── + .login-title { + font-size: 62px; // 48 × 1.3 + } + + .login-subtitle { + font-size: 34px; // 26 × 1.3 + } + + .login-btn { + height: 108px; // 96 × 1.13 + font-size: 36px; // 32 × 1.13 + } + + .skip-btn { + font-size: 26px; // 20 × 1.3 + height: 64px; + } + + // ─── 间距放大 ×1.2 ─── + .vitals-grid { + gap: 14px; + } + + .checkin-card { + padding: 24px; + } + + .reminder-card { + padding: 22px; + } + + .home-page, + .guest-page { + padding: 24px 28px 120px; + } + + .profile-page { + padding: 24px 28px 120px; + } + + .menu-item { + padding: 17px 18px; + } +} diff --git a/scripts/mpsync.ps1 b/scripts/mpsync.ps1 new file mode 100644 index 0000000..84f8c42 --- /dev/null +++ b/scripts/mpsync.ps1 @@ -0,0 +1,62 @@ +# MPSync — auto clean stale DevTools + open automation port +# Usage: powershell -File scripts/mpsync.ps1 [-Build] +param([switch]$Build) + +$Port = 9420 + +Write-Host "[MPSync] Step 1: Kill stale DevTools processes..." -ForegroundColor Cyan +$procs = Get-Process -Name "wechatdevtools" -ErrorAction SilentlyContinue +if ($null -ne $procs) { + Write-Host "[MPSync] Found $($procs.Count) stale processes, killing..." -ForegroundColor Yellow + Stop-Process -Name "wechatdevtools" -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 5 + $left = Get-Process -Name "wechatdevtools" -ErrorAction SilentlyContinue + if ($null -ne $left) { + Write-Host "[MPSync] Still $($left.Count) remaining, second attempt..." -ForegroundColor Yellow + Stop-Process -Name "wechatdevtools" -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + } + Write-Host "[MPSync] Cleanup done" -ForegroundColor Green +} else { + Write-Host "[MPSync] No stale processes" -ForegroundColor Green +} + +Start-Sleep -Seconds 2 + +# Optional build +if ($Build) { + Write-Host "[MPSync] Building miniprogram..." -ForegroundColor Cyan + Set-Location "G:\hms\apps\miniprogram" + pnpm build:weapp 2>&1 | Select-Object -Last 3 + Write-Host "[MPSync] Build done" -ForegroundColor Green +} + +# Open DevTools with project +$DevToolsExe = "D:\微信web开发者工具\微信web开发者工具.exe" +$DistPath = "G:\hms\apps\miniprogram\dist" + +$running = Get-Process -Name "wechatdevtools" -ErrorAction SilentlyContinue +if ($null -eq $running) { + Write-Host "[MPSync] Starting DevTools..." -ForegroundColor Cyan + Start-Process $DevToolsExe -ArgumentList $DistPath + Write-Host "[MPSync] Waiting for DevTools to initialize..." -ForegroundColor Cyan + Start-Sleep -Seconds 15 +} else { + Write-Host "[MPSync] DevTools already running ($($running.Count) processes)" -ForegroundColor Green +} + +# Open automation port via CLI +Write-Host "[MPSync] Opening automation port $Port..." -ForegroundColor Cyan +$cliShort = (New-Object -ComObject Scripting.FileSystemObject).GetFile("D:\微信web开发者工具\cli.bat").ShortPath +$cliResult = & $cliShort auto --project $DistPath --auto-port $Port 2>&1 +$cliResult | ForEach-Object { Write-Host $_ } + +Start-Sleep -Seconds 3 + +# Verify port +$listener = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue +if ($null -ne $listener) { + Write-Host "[MPSync] Port $Port is listening - ready for MCP" -ForegroundColor Green +} else { + Write-Host "[MPSync] WARNING: Port $Port not listening yet" -ForegroundColor Yellow +} diff --git a/scripts/mpsync.sh b/scripts/mpsync.sh new file mode 100644 index 0000000..5be3555 --- /dev/null +++ b/scripts/mpsync.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# MPSync — 微信开发者工具 MCP 自动化前置脚本 +# 清理残留进程 + 重新开启自动化端口 +# 用法: bash scripts/mpsync.sh [-b|--build] + +CLI="/d/微信web开发者工具/cli.bat" +DIST="G:/hms/apps/miniprogram/dist" +PORT=9420 + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +status() { echo -e "${CYAN}[MPSync]${NC} $1"; } +ok() { echo -e "${GREEN}[MPSync]${NC} $1"; } +warn() { echo -e "${YELLOW}[MPSync]${NC} $1"; } + +# Step 1: Kill stale DevTools processes +status "Step 1: Killing stale DevTools processes..." +count=$(tasklist 2>/dev/null | grep -c "wechatdevtools.exe" | tr -d '\r\n' || echo "0") +if [ "$count" -gt 0 ]; then + warn "Found $count stale processes, killing..." + cmd.exe /C "taskkill /F /IM wechatdevtools.exe /T" > /dev/null 2>&1 + sleep 5 + + count2=$(tasklist 2>/dev/null | grep -c "wechatdevtools.exe" | tr -d '\r\n' || echo "0") + if [ "$count2" -gt 0 ]; then + warn "Still $count2 remaining, second attempt..." + cmd.exe /C "taskkill /F /IM wechatdevtools.exe /T" > /dev/null 2>&1 + sleep 3 + fi + ok "Cleanup done" +else + ok "No stale processes" +fi + +sleep 2 + +# Step 2: Optional build +if [ "$1" = "-b" ] || [ "$1" = "--build" ]; then + status "Building miniprogram..." + cd "G:/hms/apps/miniprogram" && pnpm build:weapp 2>&1 | tail -3 + ok "Build done" +fi + +# Step 3: Start DevTools if not running +running=$(tasklist 2>/dev/null | grep -c "wechatdevtools.exe" | tr -d '\r\n' || echo "0") +if [ "$running" -eq 0 ]; then + status "Starting DevTools..." + # 找到可执行文件 + EXE=$(find "/d/微信web开发者工具" -maxdepth 1 -name "*.exe" 2>/dev/null | head -1) + if [ -n "$EXE" ]; then + "$EXE" "$DIST" & + status "Waiting for DevTools to initialize (15s)..." + sleep 15 + else + warn "Cannot find DevTools exe, please open manually" + fi +else + ok "DevTools already running ($running processes)" +fi + +# Step 4: Open automation port +status "Opening automation port $PORT..." +"$CLI" auto --project "$DIST" --auto-port $PORT 2>&1 + +sleep 3 + +# Step 5: Verify +listening=$(netstat -ano 2>/dev/null | grep ":${PORT}.*LISTEN" | head -1) +if [ -n "$listening" ]; then + ok "Port $PORT is listening — ready for MCP" +else + warn "Port $PORT not listening yet, wait a moment..." +fi