fix(mp): 行业标准第二轮审计修复 — 安全存储+UX+合规

- 安全:AI聊天历史、患者档案、设备同步数据统一走 secureSet/secureGet 加密存储
- 合规:TabBar "消息" 改为 "助手" 消除命名误导
- 合规:新增 .env.production 模板配置 HTTPS API URL
- UX:AI发送按钮 40→44px、反馈按钮 32→44px、协议勾选框 44px 点击热区
- UX:5处硬编码 10-12px 字号替换为 design token(DoctorTabBar/ShortcutButton/TodoAlert/mall)
- UX:6处安全区域写法统一(全部使用 --tk-page-padding/--tk-tabbar-space + env fallback)
- 新增 safe-bottom-padded / safe-bottom-tabbar 两个 mixin

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-21 14:06:29 +08:00
parent d576b8ba8f
commit 345e46002a
16 changed files with 51 additions and 33 deletions

View File

@@ -74,7 +74,7 @@ export default defineAppConfig({
list: [
{ pagePath: 'pages/index/index', text: '首页', iconPath: 'assets/tabbar/home.png', selectedIconPath: 'assets/tabbar/home-active.png' },
{ pagePath: 'pages/health/index', text: '健康', iconPath: 'assets/tabbar/health.png', selectedIconPath: 'assets/tabbar/health-active.png' },
{ pagePath: 'pages/messages/index', text: '消息', iconPath: 'assets/tabbar/message.png', selectedIconPath: 'assets/tabbar/message-active.png' },
{ pagePath: 'pages/messages/index', text: '助手', iconPath: 'assets/tabbar/message.png', selectedIconPath: 'assets/tabbar/message-active.png' },
{ pagePath: 'pages/profile/index', text: '我的', iconPath: 'assets/tabbar/profile.png', selectedIconPath: 'assets/tabbar/profile-active.png' },
],
},

View File

@@ -44,7 +44,7 @@
}
&__label {
font-size: 10px;
font-size: var(--tk-font-micro);
color: $tx3;
line-height: 1.2;
transition: color 0.15s ease;

View File

@@ -37,14 +37,14 @@
text-align: center;
background: $dan;
color: $white;
font-size: 11px;
font-size: var(--tk-font-micro);
font-weight: 700;
border-radius: $r-pill;
padding: 0 4px;
}
&__label {
font-size: 12px;
font-size: var(--tk-font-cap);
color: $tx2;
}

View File

@@ -44,7 +44,7 @@
&__subtitle {
display: block;
font-size: 12px;
font-size: var(--tk-font-cap);
color: $tx3;
margin-top: 2px;
overflow: hidden;

View File

@@ -308,7 +308,7 @@
gap: var(--tk-gap-md);
padding: var(--tk-section-gap) var(--tk-gap-lg);
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: calc(var(--tk-page-padding) + env(safe-area-inset-bottom, 0px));
background: $card;
box-shadow: $shadow-md;
}

View File

@@ -153,7 +153,7 @@
right: 0;
padding: var(--tk-section-gap) var(--tk-gap-lg);
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: calc(var(--tk-page-padding) + env(safe-area-inset-bottom, 0px));
background: $card;
box-shadow: $shadow-md;
}

View File

@@ -341,7 +341,8 @@
}
.ai-feedback-btn {
height: 32px;
height: 44px;
min-height: 44px;
border-radius: $r-xs;
@include flex-center;
padding: 0 var(--tk-gap-sm);

View File

@@ -189,6 +189,17 @@
@include flex-center;
flex-shrink: 0;
margin-top: 1px;
position: relative;
// 扩大点击区域至 44x4424px 视觉 + 10px 每侧 padding via 伪元素)
&::before {
content: '';
position: absolute;
top: -10px;
right: -10px;
bottom: -10px;
left: -10px;
}
&.checked {
background: var(--tk-pri);

View File

@@ -308,7 +308,7 @@
position: absolute;
top: 8px;
left: 8px;
font-size: 10px;
font-size: var(--tk-font-micro);
font-weight: 600;
padding: 2px 8px;
border-radius: 6px;

View File

@@ -252,7 +252,7 @@
align-items: center;
gap: 10px;
padding: 10px 16px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
padding-bottom: calc(var(--tk-tabbar-space) + env(safe-area-inset-bottom, 0px));
background: $card;
border-top: 1px solid $bd-l;
flex-shrink: 0;
@@ -275,9 +275,9 @@
}
.ai-chat-bar__send {
width: 40px;
height: 40px;
border-radius: 20px;
width: 44px;
height: 44px;
border-radius: 22px;
background: $pri;
display: flex;
align-items: center;

View File

@@ -197,7 +197,7 @@
align-items: center;
gap: 10px;
padding: 10px 16px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
padding-bottom: calc(var(--tk-page-padding) + env(safe-area-inset-bottom, 0px));
background: $card;
border-top: 1px solid $bd-l;
flex-shrink: 0;

View File

@@ -117,7 +117,7 @@
padding: var(--tk-gap-md) var(--tk-gap-lg);
background: $card;
border-top: 1px solid $bd;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
padding-bottom: calc(var(--tk-page-padding) + env(safe-area-inset-bottom, 0px));
}
.chat-input {

View File

@@ -4,7 +4,7 @@
// 订单列表 — 对齐原型 docs/design/mp-10-mall-pkg.html → 屏幕3
.orders-page {
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: calc(var(--tk-page-padding) + env(safe-area-inset-bottom, 0px));
}
// 状态筛选 Tab

View File

@@ -1,4 +1,5 @@
import { api } from './request';
import { secureGet, secureSet } from '@/utils/secure-storage';
export interface AiChatMessage {
id: string;
@@ -74,10 +75,10 @@ export async function closeSession(sessionId: string): Promise<void> {
await api.post(`/ai/chat/sessions/${sessionId}/close`, {});
}
/** 获取聊天历史(本地缓存) */
/** 获取聊天历史(本地加密缓存) */
export function getLocalHistory(): AiChatMessage[] {
try {
const raw = wx.getStorageSync('ai_chat_history');
const raw = secureGet('ai_chat_history');
return raw ? JSON.parse(raw) : [];
} catch (err) {
console.warn('[ai-chat] 读取本地聊天历史失败:', err);
@@ -85,9 +86,9 @@ export function getLocalHistory(): AiChatMessage[] {
}
}
/** 保存聊天历史到本地 */
/** 保存聊天历史到本地(加密存储) */
export function saveLocalHistory(messages: AiChatMessage[]): void {
try {
wx.setStorageSync('ai_chat_history', JSON.stringify(messages.slice(-100)));
secureSet('ai_chat_history', JSON.stringify(messages.slice(-100)));
} catch (err) { console.warn('[ai-chat] 保存本地聊天历史失败:', err); }
}

View File

@@ -93,11 +93,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
}
} catch { /* secure storage 不可用时保持默认值 */ }
try {
let patientRaw = Taro.getStorageSync('current_patient');
// 防御双重序列化:如果 Storage 写入了 JSON 字符串而非对象,尝试解析
if (typeof patientRaw === 'string') {
try { patientRaw = JSON.parse(patientRaw); } catch { patientRaw = null; }
}
const patientStr = secureGet('current_patient');
let patientRaw = patientStr ? JSON.parse(patientStr) : null;
const patientJson = patientRaw ? JSON.stringify(patientRaw) : '';
if (patientJson !== cachedPatientJson) {
cachedPatientJson = patientJson;
@@ -227,8 +224,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
},
setCurrentPatient: (patient) => {
Taro.setStorageSync('current_patient_id', patient.id);
Taro.setStorageSync('current_patient', patient);
secureSet('current_patient_id', patient.id);
secureSet('current_patient', JSON.stringify(patient));
setCachedPatientId(patient.id);
clearRequestCache();
set({ currentPatient: patient });
@@ -264,11 +261,11 @@ export const useAuthStore = create<AuthState>((set, get) => ({
secureRemove('user_roles');
secureRemove('tenant_id');
secureRemove('wechat_openid');
Taro.removeStorageSync('current_patient');
Taro.removeStorageSync('current_patient_id');
Taro.removeStorageSync('analytics_queue');
Taro.removeStorageSync('edit_patient');
Taro.removeStorageSync('ai_chat_history');
secureRemove('current_patient');
secureRemove('current_patient_id');
secureRemove('analytics_queue');
secureRemove('edit_patient');
secureRemove('ai_chat_history');
// 清理 BLE DataBuffer 缓存key 格式ble_buffer_{patientId}_{bucket}
const storageInfo = Taro.getStorageInfoSync();
storageInfo.keys.forEach((key) => {

View File

@@ -16,7 +16,15 @@
@mixin safe-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom, 0px);
}
@mixin safe-bottom-padded {
padding-bottom: calc(var(--tk-page-padding) + env(safe-area-inset-bottom, 0px));
}
@mixin safe-bottom-tabbar {
padding-bottom: calc(var(--tk-tabbar-space) + env(safe-area-inset-bottom, 0px));
}
@mixin serif-number {