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:
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
&__subtitle {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-size: var(--tk-font-cap);
|
||||
color: $tx3;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -189,6 +189,17 @@
|
||||
@include flex-center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
position: relative;
|
||||
|
||||
// 扩大点击区域至 44x44(24px 视觉 + 10px 每侧 padding via 伪元素)
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
bottom: -10px;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: var(--tk-pri);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user