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: [
|
list: [
|
||||||
{ pagePath: 'pages/index/index', text: '首页', iconPath: 'assets/tabbar/home.png', selectedIconPath: 'assets/tabbar/home-active.png' },
|
{ 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/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' },
|
{ pagePath: 'pages/profile/index', text: '我的', iconPath: 'assets/tabbar/profile.png', selectedIconPath: 'assets/tabbar/profile-active.png' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
font-size: 10px;
|
font-size: var(--tk-font-micro);
|
||||||
color: $tx3;
|
color: $tx3;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease;
|
||||||
|
|||||||
@@ -37,14 +37,14 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background: $dan;
|
background: $dan;
|
||||||
color: $white;
|
color: $white;
|
||||||
font-size: 11px;
|
font-size: var(--tk-font-micro);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
border-radius: $r-pill;
|
border-radius: $r-pill;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
font-size: 12px;
|
font-size: var(--tk-font-cap);
|
||||||
color: $tx2;
|
color: $tx2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
&__subtitle {
|
&__subtitle {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 12px;
|
font-size: var(--tk-font-cap);
|
||||||
color: $tx3;
|
color: $tx3;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -308,7 +308,7 @@
|
|||||||
gap: var(--tk-gap-md);
|
gap: var(--tk-gap-md);
|
||||||
padding: var(--tk-section-gap) var(--tk-gap-lg);
|
padding: var(--tk-section-gap) var(--tk-gap-lg);
|
||||||
padding-bottom: constant(safe-area-inset-bottom);
|
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;
|
background: $card;
|
||||||
box-shadow: $shadow-md;
|
box-shadow: $shadow-md;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
padding: var(--tk-section-gap) var(--tk-gap-lg);
|
padding: var(--tk-section-gap) var(--tk-gap-lg);
|
||||||
padding-bottom: constant(safe-area-inset-bottom);
|
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;
|
background: $card;
|
||||||
box-shadow: $shadow-md;
|
box-shadow: $shadow-md;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,7 +341,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-feedback-btn {
|
.ai-feedback-btn {
|
||||||
height: 32px;
|
height: 44px;
|
||||||
|
min-height: 44px;
|
||||||
border-radius: $r-xs;
|
border-radius: $r-xs;
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
padding: 0 var(--tk-gap-sm);
|
padding: 0 var(--tk-gap-sm);
|
||||||
|
|||||||
@@ -189,6 +189,17 @@
|
|||||||
@include flex-center;
|
@include flex-center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// 扩大点击区域至 44x44(24px 视觉 + 10px 每侧 padding via 伪元素)
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px;
|
||||||
|
bottom: -10px;
|
||||||
|
left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
background: var(--tk-pri);
|
background: var(--tk-pri);
|
||||||
|
|||||||
@@ -308,7 +308,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
font-size: 10px;
|
font-size: var(--tk-font-micro);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -252,7 +252,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px 16px;
|
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;
|
background: $card;
|
||||||
border-top: 1px solid $bd-l;
|
border-top: 1px solid $bd-l;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -275,9 +275,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-chat-bar__send {
|
.ai-chat-bar__send {
|
||||||
width: 40px;
|
width: 44px;
|
||||||
height: 40px;
|
height: 44px;
|
||||||
border-radius: 20px;
|
border-radius: 22px;
|
||||||
background: $pri;
|
background: $pri;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -197,7 +197,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px 16px;
|
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;
|
background: $card;
|
||||||
border-top: 1px solid $bd-l;
|
border-top: 1px solid $bd-l;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
padding: var(--tk-gap-md) var(--tk-gap-lg);
|
padding: var(--tk-gap-md) var(--tk-gap-lg);
|
||||||
background: $card;
|
background: $card;
|
||||||
border-top: 1px solid $bd;
|
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 {
|
.chat-input {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// 订单列表 — 对齐原型 docs/design/mp-10-mall-pkg.html → 屏幕3
|
// 订单列表 — 对齐原型 docs/design/mp-10-mall-pkg.html → 屏幕3
|
||||||
|
|
||||||
.orders-page {
|
.orders-page {
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: calc(var(--tk-page-padding) + env(safe-area-inset-bottom, 0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态筛选 Tab
|
// 状态筛选 Tab
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { api } from './request';
|
import { api } from './request';
|
||||||
|
import { secureGet, secureSet } from '@/utils/secure-storage';
|
||||||
|
|
||||||
export interface AiChatMessage {
|
export interface AiChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -74,10 +75,10 @@ export async function closeSession(sessionId: string): Promise<void> {
|
|||||||
await api.post(`/ai/chat/sessions/${sessionId}/close`, {});
|
await api.post(`/ai/chat/sessions/${sessionId}/close`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取聊天历史(本地缓存) */
|
/** 获取聊天历史(本地加密缓存) */
|
||||||
export function getLocalHistory(): AiChatMessage[] {
|
export function getLocalHistory(): AiChatMessage[] {
|
||||||
try {
|
try {
|
||||||
const raw = wx.getStorageSync('ai_chat_history');
|
const raw = secureGet('ai_chat_history');
|
||||||
return raw ? JSON.parse(raw) : [];
|
return raw ? JSON.parse(raw) : [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[ai-chat] 读取本地聊天历史失败:', err);
|
console.warn('[ai-chat] 读取本地聊天历史失败:', err);
|
||||||
@@ -85,9 +86,9 @@ export function getLocalHistory(): AiChatMessage[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存聊天历史到本地 */
|
/** 保存聊天历史到本地(加密存储) */
|
||||||
export function saveLocalHistory(messages: AiChatMessage[]): void {
|
export function saveLocalHistory(messages: AiChatMessage[]): void {
|
||||||
try {
|
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); }
|
} catch (err) { console.warn('[ai-chat] 保存本地聊天历史失败:', err); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,11 +93,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
} catch { /* secure storage 不可用时保持默认值 */ }
|
} catch { /* secure storage 不可用时保持默认值 */ }
|
||||||
try {
|
try {
|
||||||
let patientRaw = Taro.getStorageSync('current_patient');
|
const patientStr = secureGet('current_patient');
|
||||||
// 防御双重序列化:如果 Storage 写入了 JSON 字符串而非对象,尝试解析
|
let patientRaw = patientStr ? JSON.parse(patientStr) : null;
|
||||||
if (typeof patientRaw === 'string') {
|
|
||||||
try { patientRaw = JSON.parse(patientRaw); } catch { patientRaw = null; }
|
|
||||||
}
|
|
||||||
const patientJson = patientRaw ? JSON.stringify(patientRaw) : '';
|
const patientJson = patientRaw ? JSON.stringify(patientRaw) : '';
|
||||||
if (patientJson !== cachedPatientJson) {
|
if (patientJson !== cachedPatientJson) {
|
||||||
cachedPatientJson = patientJson;
|
cachedPatientJson = patientJson;
|
||||||
@@ -227,8 +224,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setCurrentPatient: (patient) => {
|
setCurrentPatient: (patient) => {
|
||||||
Taro.setStorageSync('current_patient_id', patient.id);
|
secureSet('current_patient_id', patient.id);
|
||||||
Taro.setStorageSync('current_patient', patient);
|
secureSet('current_patient', JSON.stringify(patient));
|
||||||
setCachedPatientId(patient.id);
|
setCachedPatientId(patient.id);
|
||||||
clearRequestCache();
|
clearRequestCache();
|
||||||
set({ currentPatient: patient });
|
set({ currentPatient: patient });
|
||||||
@@ -264,11 +261,11 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
secureRemove('user_roles');
|
secureRemove('user_roles');
|
||||||
secureRemove('tenant_id');
|
secureRemove('tenant_id');
|
||||||
secureRemove('wechat_openid');
|
secureRemove('wechat_openid');
|
||||||
Taro.removeStorageSync('current_patient');
|
secureRemove('current_patient');
|
||||||
Taro.removeStorageSync('current_patient_id');
|
secureRemove('current_patient_id');
|
||||||
Taro.removeStorageSync('analytics_queue');
|
secureRemove('analytics_queue');
|
||||||
Taro.removeStorageSync('edit_patient');
|
secureRemove('edit_patient');
|
||||||
Taro.removeStorageSync('ai_chat_history');
|
secureRemove('ai_chat_history');
|
||||||
// 清理 BLE DataBuffer 缓存(key 格式:ble_buffer_{patientId}_{bucket})
|
// 清理 BLE DataBuffer 缓存(key 格式:ble_buffer_{patientId}_{bucket})
|
||||||
const storageInfo = Taro.getStorageInfoSync();
|
const storageInfo = Taro.getStorageInfoSync();
|
||||||
storageInfo.keys.forEach((key) => {
|
storageInfo.keys.forEach((key) => {
|
||||||
|
|||||||
@@ -16,7 +16,15 @@
|
|||||||
|
|
||||||
@mixin safe-bottom {
|
@mixin safe-bottom {
|
||||||
padding-bottom: constant(safe-area-inset-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 {
|
@mixin serif-number {
|
||||||
|
|||||||
Reference in New Issue
Block a user