From 652cccf66c5eb291f70fad3cae629afe3bb7da94 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 21 May 2026 13:35:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(mp):=20=E4=BA=94=E4=B8=93=E5=AE=B6=E7=BB=84?= =?UTF-8?q?=E5=85=A8=E9=9D=A2=E5=AE=A1=E8=AE=A1=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?=E2=80=94=20=E5=AE=89=E5=85=A8+=E5=8A=9F=E8=83=BD+UX+=E6=80=A7?= =?UTF-8?q?=E8=83=BD+=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安全修复: - 移除硬编码管理员凭据 admin/Admin@2026,改用环境变量注入 - 移除 forceSetAuth 全局 bridge 方法,减少攻击面 - sanitizeHtml 从黑名单正则升级为白名单方式 - secure-storage 实现 XOR+Base64 加密存储,不再明文 - 添加旧数据迁移逻辑 migrateLegacyStorage 功能修复: - 新增咨询创建页(consultation/create),修复"发起咨询"按钮导航失败 - 修复咨询详情页长轮询可能永远不启动(dataLoadedRef → useState) - 新增 createSession service API - 预约页面从主包移至分包,配置 commonChunks 优化主包体积 UX 修复: - 65 处硬编码字号 → var(--tk-font-*) token 替换 - AI 聊天页 13 处、咨询详情页 14 处、医生端核心页 38 处 - StatusTag 色值对齐设计系统色板 - Loading 文字从 --tk-font-h1(28px) 修正为 --tk-font-body-sm - EmptyState 文字从 --tk-font-num(30px)/--tk-font-h2(22px) 修正 - 医生端 5 处硬编码颜色 → SCSS 变量 --- apps/miniprogram/config/index.ts | 9 ++ apps/miniprogram/src/app.config.ts | 8 +- apps/miniprogram/src/app.tsx | 3 +- .../src/components/EmptyState/index.scss | 4 +- .../src/components/Loading/index.scss | 2 +- .../src/components/ui/StatusTag/index.tsx | 10 +- .../src/pages/consultation/create/index.scss | 68 ++++++++++ .../src/pages/consultation/create/index.tsx | 127 ++++++++++++++++++ apps/miniprogram/src/pages/login/index.tsx | 8 +- .../miniprogram/src/pages/messages/index.scss | 26 ++-- .../pages/pkg-consultation/detail/index.scss | 28 ++-- .../pages/pkg-consultation/detail/index.tsx | 6 +- .../pkg-doctor-core/action-inbox/index.scss | 24 ++-- .../pkg-doctor-core/consultation/index.scss | 10 +- .../pages/pkg-doctor-core/followup/index.scss | 12 +- .../src/pages/pkg-doctor-core/index.scss | 10 +- .../pages/pkg-doctor-core/patients/index.scss | 32 ++--- apps/miniprogram/src/services/consultation.ts | 8 ++ apps/miniprogram/src/utils/sanitize-html.ts | 51 ++++++- apps/miniprogram/src/utils/secure-storage.ts | 94 +++++++++++-- 20 files changed, 441 insertions(+), 99 deletions(-) create mode 100644 apps/miniprogram/src/pages/consultation/create/index.scss create mode 100644 apps/miniprogram/src/pages/consultation/create/index.tsx diff --git a/apps/miniprogram/config/index.ts b/apps/miniprogram/config/index.ts index 730c2b0..ee9ecde 100644 --- a/apps/miniprogram/config/index.ts +++ b/apps/miniprogram/config/index.ts @@ -20,6 +20,8 @@ export default defineConfig(async (merge) => { 'process.env.TARO_APP_WX_TEMPLATE_CRITICAL_ALERT': JSON.stringify(process.env.TARO_APP_WX_TEMPLATE_CRITICAL_ALERT || ''), 'process.env.TARO_APP_WX_TEMPLATE_HEALTH_ABNORMAL': JSON.stringify(process.env.TARO_APP_WX_TEMPLATE_HEALTH_ABNORMAL || ''), 'process.env.TARO_APP_DEFAULT_TENANT_ID': JSON.stringify(process.env.TARO_APP_DEFAULT_TENANT_ID || ''), + 'process.env.TARO_APP_DEV_USER': JSON.stringify(process.env.TARO_APP_DEV_USER || ''), + 'process.env.TARO_APP_DEV_PASS': JSON.stringify(process.env.TARO_APP_DEV_PASS || ''), }, copy: { patterns: [], options: {} }, framework: 'react', @@ -35,6 +37,13 @@ export default defineConfig(async (merge) => { exclude: [], include: [], }, + commonChunks: ['runtime', 'vendors', 'taro', 'common'], + addChunkPages(pages) { + pages.forEach((page) => { + if (page.name === 'app') return; + page.chunks?.unshift('common'); + }); + }, postcss: { pxtransform: { enable: true, config: {} }, cssModules: { diff --git a/apps/miniprogram/src/app.config.ts b/apps/miniprogram/src/app.config.ts index 9d1373a..792171a 100644 --- a/apps/miniprogram/src/app.config.ts +++ b/apps/miniprogram/src/app.config.ts @@ -5,11 +5,9 @@ export default defineAppConfig({ 'pages/health/index', 'pages/messages/index', 'pages/consultation/index', + 'pages/consultation/create/index', 'pages/mall/index', 'pages/profile/index', - 'pages/appointment/index', - 'pages/appointment/create/index', - 'pages/appointment/detail/index', 'pages/legal/user-agreement', 'pages/legal/privacy-policy', ], @@ -59,6 +57,10 @@ export default defineAppConfig({ root: 'pages/article', pages: ['index', 'detail/index'], }, + { + root: 'pages/appointment', + pages: ['index', 'create/index', 'detail/index'], + }, { root: 'pages/pkg-consultation', pages: ['detail/index'], diff --git a/apps/miniprogram/src/app.tsx b/apps/miniprogram/src/app.tsx index a38e3cc..3d601da 100644 --- a/apps/miniprogram/src/app.tsx +++ b/apps/miniprogram/src/app.tsx @@ -4,6 +4,7 @@ import ErrorBoundary from './components/ErrorBoundary'; import { flushEvents } from './services/analytics'; import { useAuthStore } from './stores/auth'; import { useUIStore } from './stores/ui'; +import { migrateLegacyStorage } from './utils/secure-storage'; import './app.scss'; function App({ children }: PropsWithChildren>) { @@ -12,6 +13,7 @@ function App({ children }: PropsWithChildren>) { // useDidShow 在首次 mount 时也会触发,不需要 useEffect 重复调用 useDidShow(() => { + migrateLegacyStorage(); restoreAuth(); restoreUI(); }); @@ -23,7 +25,6 @@ function App({ children }: PropsWithChildren>) { restoreAuth: () => { restoreAuth(); return useAuthStore.getState(); }, restoreUI, getAuthState: () => useAuthStore.getState(), - forceSetAuth: (state: Record) => useAuthStore.setState(state), }; return () => { delete (globalThis as any).__hms; }; }, []); diff --git a/apps/miniprogram/src/components/EmptyState/index.scss b/apps/miniprogram/src/components/EmptyState/index.scss index 166774a..f7df13c 100644 --- a/apps/miniprogram/src/components/EmptyState/index.scss +++ b/apps/miniprogram/src/components/EmptyState/index.scss @@ -27,13 +27,13 @@ } .empty-state-text { - font-size: var(--tk-font-num); + font-size: var(--tk-font-body-lg); color: $tx2; margin-bottom: var(--tk-gap-xs); } .empty-state-hint { - font-size: var(--tk-font-h2); + font-size: var(--tk-font-body-sm); color: var(--tk-text-secondary); margin-bottom: var(--tk-gap-xl); } diff --git a/apps/miniprogram/src/components/Loading/index.scss b/apps/miniprogram/src/components/Loading/index.scss index e8a3e11..a930605 100644 --- a/apps/miniprogram/src/components/Loading/index.scss +++ b/apps/miniprogram/src/components/Loading/index.scss @@ -25,6 +25,6 @@ } .loading-state-text { - font-size: var(--tk-font-h1); + font-size: var(--tk-font-body-sm); color: var(--tk-text-secondary); } diff --git a/apps/miniprogram/src/components/ui/StatusTag/index.tsx b/apps/miniprogram/src/components/ui/StatusTag/index.tsx index 5497953..709f2f2 100644 --- a/apps/miniprogram/src/components/ui/StatusTag/index.tsx +++ b/apps/miniprogram/src/components/ui/StatusTag/index.tsx @@ -35,11 +35,11 @@ const DEFAULT_COLOR_MAP: Record = { }; const COLOR_STYLES: Record = { - success: { bg: '#ECFDF5', color: '#5B7A5E' }, - warning: { bg: '#FFF7ED', color: '#C4873A' }, - error: { bg: '#FEF2F2', color: '#B54A4A' }, - info: { bg: '#EFF6FF', color: '#3B82F6' }, - default: { bg: '#F5F5F4', color: '#78716C' }, + success: { bg: '#E8F0E8', color: '#5B7A5E' }, + warning: { bg: '#FFF3E0', color: '#C4873A' }, + error: { bg: '#FDEAEA', color: '#B54A4A' }, + info: { bg: '#E3F0FA', color: '#4A7AB5' }, + default: { bg: '#F0EBE5', color: '#7A756E' }, }; const StatusTag: React.FC = ({ diff --git a/apps/miniprogram/src/pages/consultation/create/index.scss b/apps/miniprogram/src/pages/consultation/create/index.scss new file mode 100644 index 0000000..5a3f48a --- /dev/null +++ b/apps/miniprogram/src/pages/consultation/create/index.scss @@ -0,0 +1,68 @@ +@import '../../../styles/variables.scss'; + +.consult-create { + padding: var(--tk-gap-xl); + + &__section { + margin-bottom: var(--tk-gap-lg); + } + + &__label { + font-size: var(--tk-font-body-sm); + color: $tx2; + margin-bottom: var(--tk-gap-xs); + display: block; + } + + &__picker { + display: flex; + align-items: center; + justify-content: space-between; + background: $card; + border: 1px solid $bd; + border-radius: $r-sm; + padding: var(--tk-gap-md) var(--tk-gap-lg); + } + + &__picker-text { + font-size: var(--tk-font-body); + color: $tx; + } + + &__picker-arrow { + font-size: var(--tk-font-body-lg); + color: $tx3; + } + + &__hint { + margin: var(--tk-gap-xl) 0; + padding: var(--tk-gap-md); + background: $surface-alt; + border-radius: $r-sm; + } + + &__hint-text { + font-size: var(--tk-font-cap); + color: $tx2; + line-height: 1.6; + } + + &__submit { + background: $pri; + border-radius: $r; + padding: var(--tk-gap-lg); + text-align: center; + margin-top: var(--tk-gap-2xl); + box-shadow: $shadow-md; + + &--disabled { + opacity: 0.5; + } + } + + &__submit-text { + font-size: var(--tk-font-body-lg); + color: $white; + font-weight: 600; + } +} diff --git a/apps/miniprogram/src/pages/consultation/create/index.tsx b/apps/miniprogram/src/pages/consultation/create/index.tsx new file mode 100644 index 0000000..420889d --- /dev/null +++ b/apps/miniprogram/src/pages/consultation/create/index.tsx @@ -0,0 +1,127 @@ +import { useState } from 'react'; +import { View, Text, Input, Picker } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import { createSession } from '@/services/consultation'; +import { listDoctors } from '@/services/appointment'; +import { useAuthStore } from '@/stores/auth'; +import PageShell from '@/components/ui/PageShell'; +import { useElderClass } from '@/hooks/useElderClass'; +import './index.scss'; + +const CONSULTATION_TYPES = ['general', 'follow_up', 'urgent']; +const TYPE_LABELS: Record = { + general: '普通咨询', + follow_up: '随访咨询', + urgent: '紧急咨询', +}; + +interface DoctorOption { + id: string; + name: string; +} + +export default function ConsultationCreate() { + const currentPatient = useAuthStore((s) => s.currentPatient); + const [doctorList, setDoctorList] = useState([]); + const [selectedDoctorIdx, setSelectedDoctorIdx] = useState(-1); + const [typeIdx, setTypeIdx] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [doctorsLoaded, setDoctorsLoaded] = useState(false); + const modeClass = useElderClass(); + + const loadDoctors = async () => { + if (doctorsLoaded) return; + try { + const res = await listDoctors(); + const items = (res.data || []).map((d: any) => ({ id: d.id, name: d.name })); + setDoctorList(items); + setDoctorsLoaded(true); + } catch { + Taro.showToast({ title: '加载医生列表失败', icon: 'none' }); + } + }; + + const handleSubmit = async () => { + if (!currentPatient?.id) { + Taro.showToast({ title: '请先完善患者档案', icon: 'none' }); + return; + } + if (submitting) return; + setSubmitting(true); + try { + const session = await createSession({ + patient_id: currentPatient.id, + doctor_id: selectedDoctorIdx >= 0 ? doctorList[selectedDoctorIdx]?.id : undefined, + consultation_type: CONSULTATION_TYPES[typeIdx], + }); + Taro.showToast({ title: '创建成功', icon: 'success' }); + setTimeout(() => { + Taro.redirectTo({ url: `/pages/pkg-consultation/detail/index?id=${session.id}` }); + }, 500); + } catch { + Taro.showToast({ title: '创建失败,请重试', icon: 'none' }); + } finally { + setSubmitting(false); + } + }; + + const doctorNames = doctorList.map((d) => d.name); + const typeLabels = CONSULTATION_TYPES.map((t) => TYPE_LABELS[t] || t); + + return ( + + + + 咨询类型 + setTypeIdx(Number(e.detail.value))}> + + {typeLabels[typeIdx]} + + + + + + + 选择医生(可选) + 0 ? doctorNames : ['点击加载医生列表']} + value={selectedDoctorIdx >= 0 ? selectedDoctorIdx : 0} + onChange={(e) => { + if (!doctorsLoaded) { + loadDoctors(); + return; + } + setSelectedDoctorIdx(Number(e.detail.value)); + }} + onClick={() => { if (!doctorsLoaded) loadDoctors(); }} + > + + + {selectedDoctorIdx >= 0 && doctorsLoaded + ? doctorNames[selectedDoctorIdx] + : '不指定(系统分配)'} + + + + + + + + + 咨询创建后,医生将尽快回复您的消息。 + + + + + + {submitting ? '创建中...' : '发起咨询'} + + + + + ); +} diff --git a/apps/miniprogram/src/pages/login/index.tsx b/apps/miniprogram/src/pages/login/index.tsx index 4d09a2e..f88951d 100644 --- a/apps/miniprogram/src/pages/login/index.tsx +++ b/apps/miniprogram/src/pages/login/index.tsx @@ -90,8 +90,14 @@ export default function Login() { }; const handleDevQuickLogin = async () => { + const devUser = process.env.TARO_APP_DEV_USER || ''; + const devPass = process.env.TARO_APP_DEV_PASS || ''; + if (!devUser || !devPass) { + Taro.showToast({ title: '未配置开发账号', icon: 'none' }); + return; + } try { - const success = await credentialLogin('admin', 'Admin@2026'); + const success = await credentialLogin(devUser, devPass); if (success) { navigateAfterLogin(); } diff --git a/apps/miniprogram/src/pages/messages/index.scss b/apps/miniprogram/src/pages/messages/index.scss index 3f8c15e..c6a248e 100644 --- a/apps/miniprogram/src/pages/messages/index.scss +++ b/apps/miniprogram/src/pages/messages/index.scss @@ -27,7 +27,7 @@ .ai-chat-nav__title { font-family: Georgia, 'Times New Roman', serif; - font-size: 17px; + font-size: var(--tk-font-body); font-weight: 700; color: $tx; } @@ -47,7 +47,7 @@ } .ai-chat-nav__online-text { - font-size: 11px; + font-size: var(--tk-font-micro); color: $acc; } @@ -74,20 +74,20 @@ .ai-chat-welcome__avatar-char { color: $white; - font-size: 32px; + font-size: var(--tk-font-num-lg); font-weight: 600; font-family: Georgia, 'Times New Roman', serif; } .ai-chat-welcome__greeting { - font-size: 17px; + font-size: var(--tk-font-body); font-weight: 600; color: $tx; margin-top: 16px; } .ai-chat-welcome__desc { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx3; text-align: center; margin-top: 6px; @@ -103,7 +103,7 @@ } .ai-chat-welcome__hint { - font-size: 12px; + font-size: var(--tk-font-micro); color: $tx3; margin-bottom: 12px; } @@ -131,11 +131,11 @@ } .ai-chat-welcome__pill-icon { - font-size: 15px; + font-size: var(--tk-font-body-sm); } .ai-chat-welcome__pill-text { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx2; } @@ -177,7 +177,7 @@ .ai-msg__avatar-char { color: $white; - font-size: 15px; + font-size: var(--tk-font-body-sm); font-weight: 600; } @@ -203,7 +203,7 @@ .ai-msg__text { display: block; width: 100%; - font-size: 15px; + font-size: var(--tk-font-body-sm); color: $tx; line-height: 1.6; word-break: break-word; @@ -265,13 +265,13 @@ border: none; border-radius: 20px; padding: 0 14px; - font-size: 14px; + font-size: var(--tk-font-body-sm); color: $tx; } .ai-chat-bar__placeholder { color: $tx3; - font-size: 14px; + font-size: var(--tk-font-body-sm); } .ai-chat-bar__send { @@ -295,6 +295,6 @@ .ai-chat-bar__send-icon { color: $white; - font-size: 20px; + font-size: var(--tk-font-body-lg); font-weight: 700; } diff --git a/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss b/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss index 2d87696..bd9d6bd 100644 --- a/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss +++ b/apps/miniprogram/src/pages/pkg-consultation/detail/index.scss @@ -32,7 +32,7 @@ } .chat-nav__back-icon { - font-size: 24px; + font-size: var(--tk-font-h1); font-weight: 300; color: $tx; line-height: 1; @@ -46,7 +46,7 @@ .chat-nav__title { font-family: Georgia, 'Times New Roman', serif; - font-size: 17px; + font-size: var(--tk-font-body); font-weight: 700; color: $tx; } @@ -66,12 +66,12 @@ } .chat-nav__online-text { - font-size: 11px; + font-size: var(--tk-font-micro); color: $acc; } .chat-nav__offline-text { - font-size: 11px; + font-size: var(--tk-font-micro); color: $tx3; margin-top: 2px; } @@ -89,7 +89,7 @@ } .chat-nav__more-icon { - font-size: 18px; + font-size: var(--tk-font-body-lg); font-weight: 700; color: $tx3; letter-spacing: 1px; @@ -108,7 +108,7 @@ margin: 8px 0 12px; &__text { - font-size: 11px; + font-size: var(--tk-font-micro); color: $tx3; background: $surface-alt; padding: 3px 12px; @@ -145,7 +145,7 @@ .chat-msg__avatar-char { color: $white; - font-size: 15px; + font-size: var(--tk-font-body-sm); font-weight: 600; } @@ -167,7 +167,7 @@ } .chat-msg__text { - font-size: 15px; + font-size: var(--tk-font-body-sm); color: $tx; line-height: 1.6; word-wrap: break-word; @@ -186,7 +186,7 @@ padding: 80px 20px; &__text { - font-size: 14px; + font-size: var(--tk-font-body-sm); color: $tx3; } } @@ -221,7 +221,7 @@ } .chat-bar__add-icon { - font-size: 22px; + font-size: var(--tk-font-h2); color: $tx3; font-weight: 300; } @@ -233,13 +233,13 @@ border: none; border-radius: 20px; padding: 0 14px; - font-size: 14px; + font-size: var(--tk-font-body-sm); color: $tx; } .chat-bar__placeholder { color: $tx3; - font-size: 14px; + font-size: var(--tk-font-body-sm); } .chat-bar__send { @@ -263,11 +263,11 @@ .chat-bar__send-icon { color: $white; - font-size: 20px; + font-size: var(--tk-font-h2); font-weight: 700; } .chat-bar__closed-text { - font-size: 14px; + font-size: var(--tk-font-body-sm); color: $tx3; } diff --git a/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx b/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx index 97a298b..eb181c1 100644 --- a/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx +++ b/apps/miniprogram/src/pages/pkg-consultation/detail/index.tsx @@ -29,7 +29,7 @@ export default function ConsultationDetail() { const scrollViewRef = useRef(''); const messagesRef = useRef([]); const modeClass = useElderClass(); - const dataLoadedRef = useRef(false); + const [dataLoaded, setDataLoaded] = useState(false); useLongPolling({ pollFn: () => { @@ -48,7 +48,7 @@ export default function ConsultationDetail() { return next; }); }, - enabled: !!sessionId && dataLoadedRef.current && session?.status !== 'closed', + enabled: !!sessionId && dataLoaded && session?.status !== 'closed', }); useEffect(() => { @@ -70,7 +70,7 @@ export default function ConsultationDetail() { setMessages(msgs); messagesRef.current = msgs; scrollViewRef.current = `msg-${msgs.length}`; - dataLoadedRef.current = true; + setDataLoaded(true); } catch { Taro.showToast({ title: '加载失败', icon: 'none' }); } finally { diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss index 9b5d3d9..0a5bc7d 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/action-inbox/index.scss @@ -38,7 +38,7 @@ } &__patient { - font-size: 15px; + font-size: var(--tk-font-body-sm); font-weight: 600; color: $tx; } @@ -47,7 +47,7 @@ display: inline-block; padding: 1px 6px; border-radius: 4px; - font-size: 10px; + font-size: var(--tk-font-micro); font-weight: 600; color: $card; background: $dan; @@ -55,7 +55,7 @@ } &__desc { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx2; margin-top: 3px; overflow: hidden; @@ -65,7 +65,7 @@ } &__time { - font-size: 11px; + font-size: var(--tk-font-micro); color: $tx3; flex-shrink: 0; text-align: right; @@ -74,7 +74,7 @@ &__arrow { flex-shrink: 0; - font-size: 20px; + font-size: var(--tk-font-body-lg); color: $tx3; } } @@ -84,7 +84,7 @@ display: inline-block; padding: 2px 8px; border-radius: 6px; - font-size: 11px; + font-size: var(--tk-font-micro); font-weight: 600; line-height: 1.6; flex-shrink: 0; @@ -116,14 +116,14 @@ padding: 16px 20px; border-bottom: 1px solid $bd-l; - .dialog-title { font-size: 14px; font-weight: 600; color: $tx; } - .dialog-close { font-size: 13px; color: $tx3; } + .dialog-title { font-size: var(--tk-font-body-sm); font-weight: 600; color: $tx; } + .dialog-close { font-size: var(--tk-font-cap); color: $tx3; } } .dialog-body { padding: 16px 20px; } .dialog-patient { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx2; display: block; margin-bottom: 12px; @@ -150,8 +150,8 @@ } .thread-content { - .thread-label { font-size: 13px; color: $tx; display: block; } - .thread-time { font-size: 11px; color: $tx3; } + .thread-label { font-size: var(--tk-font-cap); color: $tx; display: block; } + .thread-time { font-size: var(--tk-font-micro); color: $tx3; } } .dialog-actions { @@ -165,7 +165,7 @@ text-align: center; padding: 12px; border-radius: $r-sm; - font-size: 13px; + font-size: var(--tk-font-cap); font-weight: 500; &.primary { background: var(--tk-pri); color: $card; } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss index 009aa3a..e7c976d 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/consultation/index.scss @@ -39,7 +39,7 @@ } &__name { - font-size: 15px; + font-size: var(--tk-font-body-sm); font-weight: 600; color: $tx; overflow: hidden; @@ -48,14 +48,14 @@ } &__time { - font-size: 12px; + font-size: var(--tk-font-micro); color: $tx3; flex-shrink: 0; margin-left: 8px; } &__msg { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx2; overflow: hidden; text-overflow: ellipsis; @@ -75,14 +75,14 @@ } &__badge-text { - font-size: 11px; + font-size: var(--tk-font-micro); color: $card; font-weight: 700; } &__arrow { flex-shrink: 0; - font-size: 20px; + font-size: var(--tk-font-body-lg); color: $tx3; } } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss index fad9ded..7a30982 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/followup/index.scss @@ -40,7 +40,7 @@ } &__patient { - font-size: 16px; + font-size: var(--tk-font-body); font-weight: 600; color: $tx; } @@ -49,19 +49,19 @@ display: inline-block; padding: 2px 8px; border-radius: 6px; - font-size: 11px; + font-size: var(--tk-font-micro); font-weight: 600; line-height: 1.6; } &__type { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx3; display: block; } &__date { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx2; font-weight: 500; flex-shrink: 0; @@ -78,12 +78,12 @@ } &__data-label { - font-size: 12px; + font-size: var(--tk-font-micro); color: $tx3; } &__data-value { - font-size: 14px; + font-size: var(--tk-font-body-sm); font-weight: 600; color: $tx; } diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/index.scss index e235cff..03e198f 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/index.scss @@ -22,7 +22,7 @@ &__title { font-family: Georgia, 'Times New Roman', serif; - font-size: 26px; + font-size: var(--tk-font-h1); font-weight: 700; color: $tx; display: block; @@ -30,14 +30,14 @@ } &__date { - font-size: 14px; + font-size: var(--tk-font-body-sm); color: $tx3; } // ── 小节标题(对齐原型:13px fontWeight600)── &__section-label { display: block; - font-size: 13px; + font-size: var(--tk-font-cap); font-weight: 600; color: $tx2; margin-bottom: 14px; @@ -60,7 +60,7 @@ &__stat-value { font-family: Georgia, 'Times New Roman', serif; - font-size: 28px; + font-size: var(--tk-font-num-lg); font-weight: 700; line-height: 1.1; @@ -71,7 +71,7 @@ } &__stat-label { - font-size: 12px; + font-size: var(--tk-font-micro); color: $tx3; margin-top: 4px; display: block; diff --git a/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss b/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss index d8841c9..8aa33e0 100644 --- a/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss +++ b/apps/miniprogram/src/pages/pkg-doctor-core/patients/index.scss @@ -15,7 +15,7 @@ margin-bottom: 12px; text { - font-size: 13px; + font-size: var(--tk-font-cap); color: $tx3; } } @@ -30,8 +30,8 @@ padding: 20px; text { - font-size: 13px; - color: #78716C; + font-size: var(--tk-font-cap); + color: $tx3; } } } @@ -48,20 +48,20 @@ border: 1px solid $bd; &__icon { - font-size: 14px; + font-size: var(--tk-font-body-sm); flex-shrink: 0; } &__input { flex: 1; - font-size: 14px; - color: #2D2A26; + font-size: var(--tk-font-body-sm); + color: $tx; height: 100%; } &__placeholder { - color: #78716C; - font-size: 14px; + color: $tx3; + font-size: var(--tk-font-body-sm); } } @@ -88,18 +88,18 @@ } &__name { - font-size: 15px; + font-size: var(--tk-font-body-sm); font-weight: 600; - color: #2D2A26; + color: $tx; } &__meta { - font-size: 12px; - color: #78716C; + font-size: var(--tk-font-micro); + color: $tx3; } &__diagnosis { - font-size: 13px; + font-size: var(--tk-font-cap); color: $doc-pri; margin-top: 4px; overflow: hidden; @@ -109,7 +109,7 @@ } &__last-visit { - font-size: 12px; + font-size: var(--tk-font-micro); color: $tx3; margin-top: 3px; display: block; @@ -117,7 +117,7 @@ &__arrow { flex-shrink: 0; - font-size: 20px; - color: #78716C; + font-size: var(--tk-font-body-lg); + color: $tx3; } } diff --git a/apps/miniprogram/src/services/consultation.ts b/apps/miniprogram/src/services/consultation.ts index 1c101ef..c21a3c3 100644 --- a/apps/miniprogram/src/services/consultation.ts +++ b/apps/miniprogram/src/services/consultation.ts @@ -74,3 +74,11 @@ export async function pollMessages(sessionId: string, afterId?: string) { const path = `/health/consultation-sessions/${sessionId}/messages/poll${query ? '?' + query : ''}`; return requestUnlimited('GET', path, undefined, 30000); } + +export async function createSession(params: { + patient_id: string; + doctor_id?: string; + consultation_type?: string; +}) { + return api.post('/health/consultation-sessions', params); +} diff --git a/apps/miniprogram/src/utils/sanitize-html.ts b/apps/miniprogram/src/utils/sanitize-html.ts index 10a10c7..6f31955 100644 --- a/apps/miniprogram/src/utils/sanitize-html.ts +++ b/apps/miniprogram/src/utils/sanitize-html.ts @@ -1,7 +1,52 @@ -const DANGEROUS_TAG_RE = /<(?:script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>|\/?(?:iframe|object|embed|form|input|textarea|style|link|meta)\b[^>]*)>/gi; -const DANGEROUS_ATTR_RE = /(?:\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)|(?:href|src)\s*=\s*(?:"(?:javascript|data):[^"]*"|'(?:javascript|data):[^']*'))/gi; +const ALLOWED_TAGS = new Set([ + 'p', 'br', 'hr', 'div', 'span', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'ul', 'ol', 'li', + 'table', 'thead', 'tbody', 'tr', 'th', 'td', + 'strong', 'em', 'b', 'i', 'u', 's', 'del', 'ins', + 'blockquote', 'pre', 'code', + 'a', 'img', + 'dl', 'dt', 'dd', + 'sup', 'sub', +]); + +const ALLOWED_ATTRS: Record> = { + '*': new Set(['class']), + a: new Set(['href', 'title']), + img: new Set(['src', 'alt', 'width', 'height']), + td: new Set(['colspan', 'rowspan']), + th: new Set(['colspan', 'rowspan']), +}; + +const URL_ATTRS = new Set(['href', 'src']); +const SAFE_URL_RE = /^(?:https?|mailto|tel):|^$/i; + +const TAG_RE = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/g; +const ATTR_RE = /([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g; export function sanitizeHtml(html: string): string { if (!html) return ''; - return html.replace(DANGEROUS_TAG_RE, '').replace(DANGEROUS_ATTR_RE, ''); + + return html.replace(TAG_RE, (fullMatch, tagName) => { + const tag = tagName.toLowerCase(); + + if (!ALLOWED_TAGS.has(tag)) return ''; + + const allowedForTag = ALLOWED_ATTRS[tag] || new Set(); + const allowedGlobal = ALLOWED_ATTRS['*']; + const combined = new Set([...allowedForTag, ...allowedGlobal]); + + const cleaned = fullMatch.replace(ATTR_RE, (_, attrName, dqVal, sqVal) => { + const attr = attrName.toLowerCase(); + const val = dqVal ?? sqVal ?? ''; + + if (!combined.has(attr)) return ''; + + if (URL_ATTRS.has(attr) && !SAFE_URL_RE.test(val)) return ''; + + return ` ${attr}="${val}"`; + }); + + return cleaned; + }); } diff --git a/apps/miniprogram/src/utils/secure-storage.ts b/apps/miniprogram/src/utils/secure-storage.ts index df86fa8..d4ddcda 100644 --- a/apps/miniprogram/src/utils/secure-storage.ts +++ b/apps/miniprogram/src/utils/secure-storage.ts @@ -1,22 +1,98 @@ import Taro from '@tarojs/taro'; -/** - * 持久化存储工具 — 小程序版本 - * - * 敏感数据依赖 HTTPS 传输 + 后端 AES-256-GCM 加密保护。 - * 导出函数名保留 secure* 前缀以保持调用点兼容,实际为明文存储。 - */ +const ENCRYPTION_KEY = process.env.TARO_APP_ENCRYPTION_KEY || 'hms-default-key'; + +function xorEncrypt(data: string, key: string): string { + let result = ''; + for (let i = 0; i < data.length; i++) { + result += String.fromCharCode(data.charCodeAt(i) ^ key.charCodeAt(i % key.length)); + } + return result; +} + +function toBase64(str: string): string { + try { + const buffer = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + buffer[i] = str.charCodeAt(i); + } + return Taro.arrayBufferToBase64(buffer.buffer); + } catch { + return ''; + } +} + +function fromBase64(b64: string): string { + try { + const buffer = Taro.base64ToArrayBuffer(b64); + const arr = new Uint8Array(buffer); + let result = ''; + for (let i = 0; i < arr.length; i++) { + result += String.fromCharCode(arr[i]); + } + return result; + } catch { + return ''; + } +} + +const STORAGE_PREFIX = '_es_'; export function secureSet(key: string, value: string): void { - Taro.setStorageSync(key, value); + if (!value) { + Taro.removeStorageSync(STORAGE_PREFIX + key); + return; + } + const encrypted = xorEncrypt(value, ENCRYPTION_KEY); + const encoded = toBase64(encrypted); + if (encoded) { + Taro.setStorageSync(STORAGE_PREFIX + key, encoded); + } else { + Taro.setStorageSync(STORAGE_PREFIX + key, value); + } } export function secureGet(key: string): string { - const raw = Taro.getStorageSync(key); + const prefixedKey = STORAGE_PREFIX + key; + const raw = Taro.getStorageSync(prefixedKey); if (!raw || typeof raw !== 'string') return ''; + + if (raw.startsWith('{') || raw.startsWith('eyJ')) { + try { + const decoded = fromBase64(raw); + if (decoded) { + return xorEncrypt(decoded, ENCRYPTION_KEY); + } + } catch { + // fallthrough + } + } return raw; } export function secureRemove(key: string): void { - Taro.removeStorageSync(key); + Taro.removeStorageSync(STORAGE_PREFIX + key); +} + +const MIGRATION_KEYS = [ + 'access_token', 'refresh_token', 'token_expires_at', + 'user_data', 'user_roles', 'tenant_id', 'wechat_openid', +]; + +export function migrateLegacyStorage(): void { + try { + for (const key of MIGRATION_KEYS) { + const prefixed = STORAGE_PREFIX + key; + const already = Taro.getStorageSync(prefixed); + if (already) continue; + + const legacy = Taro.getStorageSync(key); + if (!legacy || typeof legacy !== 'string') continue; + + secureSet(key, legacy); + Taro.removeStorageSync(key); + } + } catch { + // migration best-effort + } }