fix(mp): T40 UI 审计修复 — 28 项设计系统合规 + 安全加固 + 讨论记录
T40 UI 审计修复(60 页面全覆盖): - 新增 $acc-d/$wrn-d 渐变中间色变量,修复首页轮播渐变硬编码 - 替换 8 处裸 white 为 $white 设计变量(5 个 SCSS 文件) - 修复 7 处触摸目标 40/44px → 48px(健康/消息/咨询/预约/首页) - 3 页面新增 Loading 状态(体征录入/个人中心/就诊人添加) - statusTag 移除硬编码布局值,改用 SCSS mixin 控制 - 医生端 14 页面架构 Hook 层补充(useThrottledDidShow 替换 useEffect) - 移除 action-inbox 未使用 import 安全 P0 修复: - JWT 中间件加固:token 类型校验 + 过期预检 + 类型别名简化 - 速率限制增强:滑动窗口 + 暴力破解防护 - analytics handler 错误处理完善 文档: - T40 审计报告(24 PASS / 36 PASS_WITH_ISSUES / 0 NEEDS_WORK) - 5 份 DevTools/性能审计讨论记录 - wiki 症状导航 + 小程序章节更新
This commit is contained in:
@@ -40,8 +40,8 @@
|
||||
|
||||
.greeting-bell {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $r-pill;
|
||||
background: $pri-l;
|
||||
@include flex-center;
|
||||
@@ -340,28 +340,13 @@
|
||||
background: linear-gradient(135deg, $pri-d 0%, $pri 60%, $pri-l 100%);
|
||||
}
|
||||
&--2 {
|
||||
background: linear-gradient(135deg, $acc 0%, #3D5A40 60%, $acc-l 100%);
|
||||
background: linear-gradient(135deg, $acc 0%, $acc-d 60%, $acc-l 100%);
|
||||
}
|
||||
&--3 {
|
||||
background: linear-gradient(135deg, #8B6F4E 0%, $wrn 60%, $wrn-l 100%);
|
||||
background: linear-gradient(135deg, $wrn-d 0%, $wrn 60%, $wrn-l 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.guest-slide-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.guest-slide:nth-child(2) .guest-slide-bg {
|
||||
background: linear-gradient(135deg, $acc 0%, #3D5A40 60%, $acc-l 100%);
|
||||
}
|
||||
|
||||
.guest-slide:nth-child(3) .guest-slide-bg {
|
||||
background: linear-gradient(135deg, #8B6F4E 0%, $wrn 60%, $wrn-l 100%);
|
||||
}
|
||||
|
||||
.guest-slide-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { View, Text, Swiper, SwiperItem, Image } from '@tarojs/components';
|
||||
import { useState } from 'react';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useState, useMemo } from 'react';
|
||||
import Taro, { usePullDownRefresh, useDidShow, useDidHide } from '@tarojs/taro';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useUIStore } from '../../stores/ui';
|
||||
import { navigateToLogin } from '../../utils/navigate';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
import ProgressRing from '../../components/ProgressRing';
|
||||
import Loading from '../../components/Loading';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { trackPageView } from '@/services/analytics';
|
||||
import * as appointmentApi from '@/services/appointment';
|
||||
import * as followupApi from '@/services/followup';
|
||||
@@ -30,8 +31,6 @@ interface PublicBanner {
|
||||
image_url?: string;
|
||||
link_type?: string;
|
||||
link_target?: string;
|
||||
/** 下载后的本地临时路径 */
|
||||
local_path?: string;
|
||||
}
|
||||
|
||||
// ─── 访客首页 ───
|
||||
@@ -45,10 +44,14 @@ const FALLBACK_SLIDES = [
|
||||
function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
const [banners, setBanners] = useState<PublicBanner[]>([]);
|
||||
const [articles, setArticles] = useState<Article[]>([]);
|
||||
const [swiperAutoplay, setSwiperAutoplay] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
useDidShow(() => { setSwiperAutoplay(true); });
|
||||
useDidHide(() => { setSwiperAutoplay(false); });
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
loadPublicData();
|
||||
});
|
||||
}, 10_000);
|
||||
|
||||
const loadPublicData = async () => {
|
||||
let tenantId = Taro.getStorageSync('tenant_id');
|
||||
@@ -70,20 +73,12 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
|
||||
if (bannerData.status === 'fulfilled' && bannerData.value?.length > 0) {
|
||||
const apiBase = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1';
|
||||
const withLocal = await Promise.all(
|
||||
bannerData.value.map(async (b) => {
|
||||
if (!b.image_url) return b;
|
||||
try {
|
||||
const fullUrl = b.image_url.startsWith('http') ? b.image_url : `${apiBase}${b.image_url}`;
|
||||
const res = await Taro.downloadFile({ url: fullUrl });
|
||||
if (res.tempFilePath) {
|
||||
return { ...b, local_path: res.tempFilePath };
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return b;
|
||||
})
|
||||
);
|
||||
setBanners(withLocal);
|
||||
const resolved = bannerData.value.map((b) => {
|
||||
if (!b.image_url) return b;
|
||||
const fullUrl = b.image_url.startsWith('http') ? b.image_url : `${apiBase}${b.image_url}`;
|
||||
return { ...b, image_url: fullUrl };
|
||||
});
|
||||
setBanners(resolved);
|
||||
} else {
|
||||
setBanners(FALLBACK_SLIDES);
|
||||
}
|
||||
@@ -107,7 +102,7 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
indicatorDots
|
||||
indicatorColor='rgba(255,255,255,0.4)'
|
||||
indicatorActiveColor='#FFFFFF'
|
||||
autoplay
|
||||
autoplay={swiperAutoplay}
|
||||
circular
|
||||
interval={4000}
|
||||
duration={500}
|
||||
@@ -115,8 +110,8 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
{slides.map((slide, idx) => (
|
||||
<SwiperItem key={slide.id || idx}>
|
||||
<View className='guest-slide'>
|
||||
{(slide.local_path || slide.image_url) ? (
|
||||
<Image className='guest-slide-image' src={slide.local_path || slide.image_url} mode='aspectFill' />
|
||||
{(slide.image_url) ? (
|
||||
<Image className='guest-slide-image' src={slide.image_url} mode='aspectFill' lazyLoad />
|
||||
) : (
|
||||
<View className={`guest-slide-bg guest-slide-bg--${(idx % 3) + 1}`} />
|
||||
)}
|
||||
@@ -187,18 +182,21 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
// ─── 登录后首页 ───
|
||||
|
||||
function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
const { user, currentPatient } = useAuthStore();
|
||||
const { todaySummary, loading, refreshToday } = useHealthStore();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const todaySummary = useHealthStore((s) => s.todaySummary);
|
||||
const loading = useHealthStore((s) => s.loading);
|
||||
const refreshToday = useHealthStore((s) => s.refreshToday);
|
||||
const [reminders, setReminders] = useState<ReminderItem[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [remindersLoading, setRemindersLoading] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
const { trigger: triggerHomeRefresh } = useThrottledDidShow(() => {
|
||||
refreshToday();
|
||||
loadReminders();
|
||||
loadUnread();
|
||||
trackPageView('home');
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
Promise.all([refreshToday(true), loadReminders(), loadUnread()]).finally(() => {
|
||||
@@ -272,19 +270,19 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
const completedCount = indicators.filter(Boolean).length;
|
||||
const progressPercent = Math.round((completedCount / 4) * 100);
|
||||
|
||||
const indicatorCapsules = [
|
||||
const indicatorCapsules = useMemo(() => [
|
||||
{ label: '血压', done: !!summary.blood_pressure },
|
||||
{ label: '心率', done: !!summary.heart_rate },
|
||||
{ label: '血糖', done: !!summary.blood_sugar },
|
||||
{ label: '体重', done: !!summary.weight },
|
||||
];
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
|
||||
const healthItems = [
|
||||
const healthItems = useMemo(() => [
|
||||
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'systolic_bp_morning' },
|
||||
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' },
|
||||
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar' },
|
||||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
|
||||
];
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
|
||||
const getStatusTag = (status?: string) => {
|
||||
if (status === 'high' || status === 'low') return { label: status === 'high' ? '偏高' : '偏低', cls: 'tag-warn' };
|
||||
|
||||
Reference in New Issue
Block a user