fix(mp): 五专家组全面审计修复 — 安全+功能+UX+性能+代码质量

安全修复:
- 移除硬编码管理员凭据 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 变量
This commit is contained in:
iven
2026-05-21 13:35:46 +08:00
parent e769a5785a
commit 652cccf66c
20 changed files with 441 additions and 99 deletions

View File

@@ -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;
}
}

View File

@@ -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<string, string> = {
general: '普通咨询',
follow_up: '随访咨询',
urgent: '紧急咨询',
};
interface DoctorOption {
id: string;
name: string;
}
export default function ConsultationCreate() {
const currentPatient = useAuthStore((s) => s.currentPatient);
const [doctorList, setDoctorList] = useState<DoctorOption[]>([]);
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 (
<PageShell title='发起咨询' scroll={false}>
<View className={`consult-create ${modeClass}`}>
<View className='consult-create__section'>
<Text className='consult-create__label'></Text>
<Picker mode='selector' range={typeLabels} value={typeIdx} onChange={(e) => setTypeIdx(Number(e.detail.value))}>
<View className='consult-create__picker'>
<Text className='consult-create__picker-text'>{typeLabels[typeIdx]}</Text>
<Text className='consult-create__picker-arrow'></Text>
</View>
</Picker>
</View>
<View className='consult-create__section'>
<Text className='consult-create__label'></Text>
<Picker
mode='selector'
range={doctorNames.length > 0 ? doctorNames : ['点击加载医生列表']}
value={selectedDoctorIdx >= 0 ? selectedDoctorIdx : 0}
onChange={(e) => {
if (!doctorsLoaded) {
loadDoctors();
return;
}
setSelectedDoctorIdx(Number(e.detail.value));
}}
onClick={() => { if (!doctorsLoaded) loadDoctors(); }}
>
<View className='consult-create__picker'>
<Text className='consult-create__picker-text'>
{selectedDoctorIdx >= 0 && doctorsLoaded
? doctorNames[selectedDoctorIdx]
: '不指定(系统分配)'}
</Text>
<Text className='consult-create__picker-arrow'></Text>
</View>
</Picker>
</View>
<View className='consult-create__hint'>
<Text className='consult-create__hint-text'>
</Text>
</View>
<View
className={`consult-create__submit ${submitting ? 'consult-create__submit--disabled' : ''}`}
onClick={handleSubmit}
>
<Text className='consult-create__submit-text'>
{submitting ? '创建中...' : '发起咨询'}
</Text>
</View>
</View>
</PageShell>
);
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -29,7 +29,7 @@ export default function ConsultationDetail() {
const scrollViewRef = useRef('');
const messagesRef = useRef<ConsultationMessage[]>([]);
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 {

View File

@@ -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; }

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}
}