Initial commit: HealthManager full-stack health management platform
Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR Frontend patient: React 19 + TypeScript + Vite (mobile H5) Frontend doctor: React 19 + TypeScript + Vite (desktop web)
This commit is contained in:
591
frontend-patient/技术报告.md
Normal file
591
frontend-patient/技术报告.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# 健康管家(健康管家)— 技术审核报告
|
||||
|
||||
> **项目名称**:health-manager-demo
|
||||
> **中文名称**:健康管家 · 心脏健康管理
|
||||
> **定位**:PCI 术后患者心脏健康管理移动端 Web 应用
|
||||
> **审核日期**:2026-05-19
|
||||
> **审核范围**:全部源代码(87 个文件)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [项目概述](#1-项目概述)
|
||||
2. [技术栈](#2-技术栈)
|
||||
3. [目录结构与文件说明](#3-目录结构与文件说明)
|
||||
4. [架构分析](#4-架构分析)
|
||||
5. [Bug 清单](#5-bug-清单)
|
||||
6. [代码质量问题](#6-代码质量问题)
|
||||
7. [安全性问题](#7-安全性问题)
|
||||
8. [性能问题](#8-性能问题)
|
||||
9. [无障碍(Accessibility)问题](#9-无障碍accessibility问题)
|
||||
10. [改进建议](#10-改进建议)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 这是什么项目?
|
||||
|
||||
这是一个**心脏健康管理 App 的前端原型**,目标用户是做过 **PCI(冠状动脉支架植入)手术**的患者。它用网页技术(React)模拟了一个手机 App,包含以下功能模块:
|
||||
|
||||
| 模块 | 功能 |
|
||||
|------|------|
|
||||
| 登录/注册 | 手机号 + 短信验证码登录 |
|
||||
| 首页看板 | 健康概览(血压、心率)、快捷入口、健康小贴士 |
|
||||
| 健康数据 | 手动录入血压/心率/血糖/血氧/体重/步数,查看趋势图,健康日历 |
|
||||
| 设备绑定 | 模拟蓝牙设备绑定(血压计、手表、体脂秤等) |
|
||||
| 用药管理 | 药品列表、服药记录、用药依从性统计 |
|
||||
| 在线问诊 | 医生列表、科室筛选、图文聊天 |
|
||||
| 报告解读 | 上传/查看医疗报告、AI 解读 |
|
||||
| 复查管理 | 术后复查预约记录 |
|
||||
| 运动饮食 | 运动/饮食推荐、运动/饮食打卡 |
|
||||
| 通知中心 | 系统通知列表 |
|
||||
| 个人中心 | 个人资料、设置、隐私政策、关于 |
|
||||
|
||||
### 1.2 重要前提
|
||||
|
||||
**这是一个纯前端 Demo,没有后端服务。** 所有数据(用户、健康记录、医生列表、药品等)都是写在前端代码里的假数据(Mock Data)。点击交互看似"正常",但数据不会真正保存到任何服务器,刷新页面后所有修改都会丢失。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
| 类别 | 技术 | 版本 | 用途 |
|
||||
|------|------|------|------|
|
||||
| 语言 | TypeScript | ~6.0.2 | 类型安全 |
|
||||
| 框架 | React | ^19.2.6 | UI 构建 |
|
||||
| 构建工具 | Vite | ^8.0.12 | 开发/打包 |
|
||||
| 路由 | react-router-dom | ^7.15.1 | 页面跳转 |
|
||||
| 状态管理 | zustand | ^5.0.13 | 全局状态(登录状态、通知) |
|
||||
| 图表 | echarts + echarts-for-react | ^6.0.0 | 折线图、柱状图、饼图 |
|
||||
| 动画 | framer-motion | ^12.39.0 | 页面切换动画 |
|
||||
| 日期 | dayjs | ^1.11.20 | 日期格式化 |
|
||||
| 样式 | CSS Modules + CSS 变量 | Vite 内置 | 组件级样式隔离 |
|
||||
| 代码检查 | ESLint | ^10.3.0 | 代码规范 |
|
||||
|
||||
### 2.1 技术要点说明
|
||||
|
||||
- **React 19**:最新版 React,支持 Suspense、Server Components 等新特性(本项目未使用)
|
||||
- **zustand**:比 Redux 更轻量的状态管理库,本项目用它管理登录状态和通知,登录状态会持久化到 localStorage
|
||||
- **echarts**:Apache 出品的图表库,本项目用它画趋势图、饼图、柱状图
|
||||
- **CSS Modules**:每个组件有自己的 `.module.css` 文件,样式不会互相污染
|
||||
- **framer-motion**:给页面切换加过渡动画
|
||||
- **dayjs**:比 moment.js 轻量 97% 的日期处理库
|
||||
|
||||
---
|
||||
|
||||
## 3. 目录结构与文件说明
|
||||
|
||||
```
|
||||
haruite-medical-demo/
|
||||
│
|
||||
├── index.html # 应用入口 HTML(设置 viewport、标题、PWA meta)
|
||||
├── package.json # 项目配置(名称、依赖、npm 脚本)
|
||||
├── vite.config.ts # Vite 构建配置(路径别名、端口、插件)
|
||||
├── tsconfig.json # TypeScript 根配置(引用子配置)
|
||||
├── tsconfig.app.json # 应用代码 TS 配置(JSX、路径别名)
|
||||
├── tsconfig.node.json # Node 端 TS 配置(vite.config.ts 用)
|
||||
├── eslint.config.js # ESLint 代码检查规则(扁平配置)
|
||||
├── README.md # 项目说明
|
||||
│
|
||||
├── public/
|
||||
│ ├── favicon.svg # 浏览器标签页图标
|
||||
│ └── icons.svg # 社交媒体图标(Bluesky、Discord、GitHub、X)
|
||||
│
|
||||
└── src/
|
||||
├── main.tsx # 应用启动入口,渲染 <App /> 到 <div id="root">
|
||||
├── App.tsx # 根组件,初始化路由
|
||||
│
|
||||
├── assets/styles/ # 全局样式
|
||||
│ ├── variables.css # CSS 变量(颜色、间距、圆角、阴影、字体、层级)
|
||||
│ ├── reset.css # CSS 重置(统一各浏览器默认样式)
|
||||
│ └── global.css # 全局样式(页面布局、滚动容器、过渡动画)
|
||||
│
|
||||
├── types/ # TypeScript 类型定义
|
||||
│ ├── index.ts # 统一导出所有类型
|
||||
│ ├── user.ts # User、LoginRequest、RegisterRequest
|
||||
│ ├── health.ts # HealthRecord、HealthStats、MeasurementType
|
||||
│ ├── device.ts # Device、DeviceType、DeviceStatus
|
||||
│ ├── medication.ts # Medication、MedicationRecord、MedicationAdherence
|
||||
│ ├── consultation.ts # Doctor、Consultation、ConsultationMessage
|
||||
│ ├── report.ts # Report、ReportFinding、ReportResult
|
||||
│ ├── followup.ts # FollowUp、FollowUpStatus
|
||||
│ ├── exercise-diet.ts # FoodItem、ExerciseRecord、DietRecord
|
||||
│ ├── notification.ts # Notification、NotificationGroup
|
||||
│ └── calendar.ts # CalendarMarker、CalendarDay
|
||||
│
|
||||
├── utils/ # 工具函数
|
||||
│ ├── constants.ts # 常量(测量类型、导航项、药品列表、健康提示)
|
||||
│ ├── format.ts # 格式化(日期、数字、血压风险等级)
|
||||
│ ├── storage.ts # localStorage 封装(带错误处理)
|
||||
│ └── validator.ts # 表单验证(手机号、验证码、必填、数字范围)
|
||||
│
|
||||
├── hooks/ # 自定义 React Hooks
|
||||
│ ├── useAuth.ts # 登录状态 Hook(useAuthStore 的封装)
|
||||
│ └── useCountdown.ts # 倒计时 Hook(短信验证码重发倒计时)
|
||||
│
|
||||
├── stores/ # 全局状态管理(zustand)
|
||||
│ ├── auth.store.ts # 登录状态(用户信息、token、登录/注册/登出)
|
||||
│ └── notification.store.ts # 通知状态(通知列表、未读数)
|
||||
│
|
||||
├── services/ # API 服务层
|
||||
│ ├── api.ts # API 请求封装(目前直接返回 Mock 数据)
|
||||
│ ├── auth.service.ts # 登录/注册/短信/个人资料
|
||||
│ ├── health.service.ts # 健康记录 CRUD + 统计分析
|
||||
│ ├── device.service.ts # 设备扫描/绑定/解绑
|
||||
│ ├── medication.service.ts # 药品 CRUD + 用药记录 + 依从性
|
||||
│ ├── consultation.service.ts # 问诊(医生列表、咨询、消息)
|
||||
│ ├── report.service.ts # 报告上传/查看/解读
|
||||
│ ├── followup.service.ts # 复查预约 CRUD
|
||||
│ ├── exercise-diet.service.ts # 运动/饮食记录 + 推荐
|
||||
│ └── notification.service.ts # 通知列表/已读/未读数
|
||||
│
|
||||
├── mock/ # Mock 假数据
|
||||
│ ├── index.ts # Mock 基础设施(延迟模拟、响应格式、ID 生成)
|
||||
│ ├── users.ts # 假用户数据(1 个用户)
|
||||
│ ├── health-records.ts # 假健康记录(91 天 × 6 种测量类型)
|
||||
│ ├── devices.ts # 假设备数据(4 已绑定 + 2 可扫描)
|
||||
│ ├── medications.ts # 假药品 + 30 天服药记录
|
||||
│ ├── consultations.ts # 假医生(8 人)+ 问诊记录
|
||||
│ ├── reports.ts # 假医疗报告(5 份)
|
||||
│ ├── followups.ts # 假复查记录(3 条)
|
||||
│ ├── exercise-diet.ts # 假运动/饮食记录(14 天)
|
||||
│ └── notifications.ts # 假通知(8 条)
|
||||
│
|
||||
├── router/ # 路由配置
|
||||
│ ├── index.tsx # 全部路由定义
|
||||
│ └── AuthGuard.tsx # 登录守卫(未登录自动跳转登录页)
|
||||
│
|
||||
├── components/ # 可复用组件
|
||||
│ ├── common/ # 通用 UI 组件
|
||||
│ │ ├── Badge.tsx/css # 角标/红点
|
||||
│ │ ├── Button.tsx/css # 按钮(主要/次要/线框/文字 + 加载态)
|
||||
│ │ ├── Card.tsx/css # 卡片容器
|
||||
│ │ ├── Empty.tsx/css # 空状态占位
|
||||
│ │ ├── Input.tsx/css # 输入框(带标签和错误提示)
|
||||
│ │ └── Toast.tsx/css # 轻提示(全局 toast 通知)
|
||||
│ ├── layout/ # 布局组件
|
||||
│ │ ├── AppLayout.tsx/css # 主布局(顶部 + 内容区 + 底部 TabBar)
|
||||
│ │ ├── StackLayout.tsx # 堆栈页面布局(无底栏,仅 <Outlet />)
|
||||
│ │ ├── PageHeader.tsx/css # 页面顶栏(返回按钮 + 标题 + 右侧操作)
|
||||
│ │ └── TabBar.tsx/css # 底部导航栏(首页/健康/服务/我的)
|
||||
│ └── charts/ # 图表组件
|
||||
│ ├── BarChart.tsx # 柱状图(echarts)
|
||||
│ ├── LineChart.tsx # 折线图(支持双线 + 警戒线)
|
||||
│ └── PieChart.tsx # 饼图/环形图(echarts)
|
||||
│
|
||||
└── pages/ # 页面组件
|
||||
├── auth/ # 登录/注册
|
||||
├── home/ # 首页 + 设备绑定
|
||||
├── health/ # 健康数据(Hub、记录列表、录入、趋势图、日历)
|
||||
├── medication/ # 用药管理(列表、编辑、详情)
|
||||
├── services/ # 服务(Hub、医生列表、聊天、报告、复查)
|
||||
├── exercise-diet/ # 运动饮食
|
||||
├── notifications/ # 通知列表
|
||||
└── profile/ # 个人中心、设置、静态页面
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 架构分析
|
||||
|
||||
### 4.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Pages(页面) │
|
||||
│ 每个页面是一个 React 组件,负责 UI 渲染 │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│ 调用
|
||||
┌──────────────▼──────────────────────────────┐
|
||||
│ Services(服务层) │
|
||||
│ 封装业务逻辑,每个域一个文件 │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│ 调用
|
||||
┌──────────────▼──────────────────────────────┐
|
||||
│ Mock(模拟数据层) │
|
||||
│ 返回假数据,模拟后端 API 响应 │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 状态管理架构
|
||||
|
||||
```
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ auth.store.ts │ │ notification.store.ts │
|
||||
│ (zustand + persist) │ │ (zustand) │
|
||||
│ │ │ │
|
||||
│ - user │ │ - notifications[] │
|
||||
│ - token │ │ - unreadCount │
|
||||
│ - isAuthenticated │ │ - loading │
|
||||
│ - login() │ │ - fetchNotifications│
|
||||
│ - register() │ │ - markRead() │
|
||||
│ - logout() │ │ - markAllRead() │
|
||||
│ - updateProfile() │ │ │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
### 4.3 路由架构
|
||||
|
||||
```
|
||||
/ (root)
|
||||
├── /login → LoginPage(登录页)
|
||||
├── /register → RegisterPage(注册页)
|
||||
│
|
||||
├── [AuthGuard] ← 登录验证
|
||||
│ ├── [AppLayout] ← 带底部 TabBar 的布局
|
||||
│ │ ├── /home → HomePage(首页)
|
||||
│ │ ├── /health → HealthHubPage(健康 Hub)
|
||||
│ │ ├── /services → ServicesHubPage(服务 Hub)
|
||||
│ │ └── /profile → ProfilePage(我的)
|
||||
│ │
|
||||
│ └── [StackLayout] ← 无底部栏的布局
|
||||
│ ├── /home/device-binding → 设备绑定
|
||||
│ ├── /health/records → 健康记录列表
|
||||
│ ├── /health/records/add → 手动录入
|
||||
│ ├── /health/trends/:type → 趋势图
|
||||
│ ├── /health/calendar → 健康日历
|
||||
│ ├── /health/medications → 用药列表
|
||||
│ ├── /services/consultation → 医生列表
|
||||
│ ├── /services/consultation/chat/:doctorId → 聊天
|
||||
│ ├── /services/reports → 报告列表
|
||||
│ ├── /services/follow-ups → 复查列表
|
||||
│ ├── /profile/settings → 设置
|
||||
│ └── /notifications → 通知列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Bug 清单
|
||||
|
||||
### 5.1 严重 Bug(会导致崩溃或白屏)
|
||||
|
||||
#### 🔴 BUG-001:ProfilePage 在未登录时崩溃
|
||||
|
||||
**文件**:`src/pages/profile/ProfilePage.tsx:45`
|
||||
**问题**:当 `user` 为 `null` 时,`user?.medicalHistory.join(',')` 会抛出 `TypeError`。
|
||||
**原因**:可选链 `?.` 只在 `user` 为 `null/undefined` 时短路,但 `medicalHistory` 本身也可能是 `undefined`。`.join()` 在 `undefined` 上调用直接报错。
|
||||
|
||||
```tsx
|
||||
// ❌ 错误
|
||||
{user?.medicalHistory.join(',')}
|
||||
|
||||
// ✅ 正确
|
||||
{user?.medicalHistory?.join(',') || '无'}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 🔴 BUG-002:ChatPage 缺少 doctorId 时崩溃
|
||||
|
||||
**文件**:`src/pages/services/ChatPage.tsx:42`
|
||||
**问题**:`doctorId!` 使用了 TypeScript 的非空断言。如果用户直接访问 `/services/consultation/chat/`(没有 doctorId),页面直接崩溃。
|
||||
|
||||
```tsx
|
||||
// ❌ 错误
|
||||
await consultationService.sendMessage(doctorId!, consultation.id, text);
|
||||
|
||||
// ✅ 正确
|
||||
if (!doctorId) { toast('医生信息缺失'); return; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 🔴 BUG-003:FollowUpEditPage 服务调用失败时按钮永久 Loading
|
||||
|
||||
**文件**:`src/pages/services/FollowUpEditPage.tsx:19-24`
|
||||
**问题**:`handleSubmit` 没有 `try/catch`。如果 `addFollowUp()` 抛出异常,`setLoading(false)` 永远不会执行,按钮永远显示"提交中..."
|
||||
|
||||
```tsx
|
||||
// ❌ 错误
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
await followupService.addFollowUp({...}); // 抛异常就卡死
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// ✅ 正确
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await followupService.addFollowUp({...});
|
||||
} catch {
|
||||
toast('提交失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 🔴 BUG-004:路由修复前 — 所有子页面白屏(已修复)
|
||||
|
||||
**文件**:`src/router/index.tsx:61`
|
||||
**问题(已修复)**:第二个路由组渲染 `<AuthGuard>{null}</AuthGuard>`,没有 `<Outlet />`,导致 20+ 个堆栈页面全部白屏。
|
||||
**修复**:将其改为 `<AuthGuard><StackLayout /></AuthGuard>`,`StackLayout` 内部渲染 `<Outlet />`。
|
||||
|
||||
---
|
||||
|
||||
### 5.2 中等问题(功能不正常,但不会崩溃)
|
||||
|
||||
#### 🟡 BUG-005:健康趋势分析始终显示"稳定"
|
||||
|
||||
**文件**:`src/services/health.service.ts:51-54`
|
||||
**问题**:`getLatestStats()` 中对 `weekRecords`(对象数组)做了 `.reduce((a,b) => a+b, 0)`,而不是对数值数组。这导致 `olderAvg` 和 `newerAvg` 始终是 `NaN`,趋势永远返回 `'stable'`。
|
||||
|
||||
```tsx
|
||||
// ❌ 错误 — weekRecords 是 HealthRecord 对象数组,不能相加
|
||||
const olderAvg = olderValues.reduce((a, b) => a + b, 0) / olderValues.length;
|
||||
|
||||
// ✅ 正确 — 应该用 values(数值数组)
|
||||
const olderAvg = olderValues.reduce((a, b) => a + b, 0) / olderValues.length;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-006:健康日历使用写死的假数据
|
||||
|
||||
**文件**:`src/pages/health/HealthCalendarPage.tsx:7-9`
|
||||
**问题**:直接从 `mock/` 目录 import 假数据,不经过 service 层。无论用户录入了多少健康数据、吃了什么药,日历永远显示同样的假数据。
|
||||
|
||||
```tsx
|
||||
// ❌ 绕过 service 层,直接引用 mock
|
||||
import { mockHealthRecords } from '@/mock/health-records';
|
||||
import { mockMedicationRecords } from '@/mock/medications';
|
||||
|
||||
// ✅ 应该通过 service 获取
|
||||
healthService.getRecords({...})
|
||||
medicationService.getMedicationRecords(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-007:复查列表卡片点击跳转到错误页面
|
||||
|
||||
**文件**:`src/pages/services/FollowUpListPage.tsx:39`
|
||||
**问题**:点击复查卡片跳转到 `/health/medications`(用药列表),而不是复查详情页。明显是复制粘贴错误。
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-008:用药详情页获取全部数据而非按 ID 查询
|
||||
|
||||
**文件**:`src/pages/medication/MedicationDetailPage.tsx:17-19`
|
||||
**问题**:请求**所有**药品和**所有**服药记录,然后在客户端 `.find()` 筛选。如果药品数量很大,这会造成不必要的性能开销。应该按 ID 精确查询。
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-009:手动录入页 Loading 状态永远不会激活
|
||||
|
||||
**文件**:`src/pages/health/ManualEntryPage.tsx:24`
|
||||
**问题**:`loading` 状态声明了但从未设成 `true`,用户快速点击提交按钮可以重复提交。
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-010:报告上传失败时仍显示"上传成功"
|
||||
|
||||
**文件**:`src/pages/services/ReportUploadPage.tsx:21`
|
||||
**问题**:`handleSubmit` 没有 `try/catch`,不管 `uploadReport()` 是否成功,都会显示"上传成功"并返回上一页。
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-011:短信验证码从未真正发送
|
||||
|
||||
**文件**:`src/pages/auth/LoginPage.tsx` 和 `RegisterPage.tsx`
|
||||
**问题**:点击"发送验证码"直接 toast"验证码已发送",只启动了倒计时,实际上没有调用任何 API。smsCode 参数在 `auth.service.ts` 中标记为 `_smsCode`(未使用)。
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-012:AuthGuard 在页面刷新时会闪到登录页
|
||||
|
||||
**文件**:`src/router/AuthGuard.tsx`
|
||||
**问题**:zustand 的 `persist` 中间件是异步从 localStorage 恢复数据的。在恢复完成前,`isAuthenticated` 为 `false`,导致 AuthGuard 短暂显示登录页,等数据恢复后又跳回来。用户体验很差。
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 BUG-013:useCountdown 内存泄漏
|
||||
|
||||
**文件**:`src/hooks/useCountdown.ts`
|
||||
**问题**:
|
||||
1. 组件卸载时 `setInterval` 不会被清除,持续在后台运行
|
||||
2. 多次调用 `start()` 会创建多个并发的倒计时,倒计时速度翻倍
|
||||
|
||||
---
|
||||
|
||||
### 5.3 低严重度(小问题)
|
||||
|
||||
- **mock/devices.ts**:小米体脂秤的设备类型写成了 `smartwatch`,应该是 `scale`(但 `DeviceType` 联合类型里没有 `scale`)
|
||||
- **mock/notifications.ts:80**:存在乱码字符 `"已完<E5B7B2><E5AE8C><EFBFBD>状态"`,应为 `"已完成状态"`
|
||||
- **utils/format.ts**:`formatRelative` 对未来的日期显示"刚刚",逻辑不对
|
||||
- **stores/auth.store.ts**:`register()` 接收 `code` 参数但不传给 service
|
||||
- **types/health.ts** vs **utils/constants.ts**:`MeasurementType` 类型在两个文件中分别定义,可能不同步
|
||||
|
||||
---
|
||||
|
||||
## 6. 代码质量问题
|
||||
|
||||
### 6.1 系统性问题
|
||||
|
||||
| 问题 | 影响范围 | 说明 |
|
||||
|------|----------|------|
|
||||
| **所有页面缺少 `.catch()` 错误处理** | 几乎所有页面 | Promise 被静默吞掉,出错时用户看不到任何反馈 |
|
||||
| **所有页面缺少 Loading 状态** | 几乎所有页面 | 数据加载时显示空状态,然后突然出现内容,体验差 |
|
||||
| **所有页面缺少 Error 状态** | 几乎所有页面 | API 调用失败时没有错误提示和重试按钮 |
|
||||
| **大量 inline 箭头函数** | 所有页面 | `onClick={() => ...}` 每次渲染创建新函数,影响 memo 优化 |
|
||||
| **没有 Error Boundary** | 全局 | 任何一个组件崩溃都会导致整个页面白屏 |
|
||||
| **URL 参数无验证** | 多个页面 | `as MeasurementType` 等方式把 URL 参数直接断言为类型,无运行时校验 |
|
||||
| **缺少 `<form>` 元素** | 所有表单页面 | 用 `<div>` + `onClick` 代替 `<form>` + `onSubmit`,失去原生表单行为 |
|
||||
| **确认框用 `window.confirm()`** | ProfilePage | 阻塞主线程,移动端体验差,样式不可定制 |
|
||||
|
||||
### 6.2 Mock 数据问题
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| **用户 ID 硬编码 `'u001'`** | 所有 service 文件都写死 `userId: 'u001'`,无法支持多用户 |
|
||||
| **Mock 数据被可变修改** | 所有"写"操作直接修改模块级数组,测试之间状态会互相污染 |
|
||||
| **无法模拟错误** | `mockApiResponse` 永远返回 `code: 200`,无法测试错误处理流程 |
|
||||
| **健康记录时间不真实** | 所有记录时间戳都相同(08:00:00),步数是 20:00:00 |
|
||||
| **饮食记录只有早餐** | `i % 3 === 0` 过滤导致只生成大约 5 条早餐记录,无午餐/晚餐 |
|
||||
| **ChatPage 里有硬编码的假回复** | 生产代码里写了 `getDoctorReply()`,用随机选择的模板回复 |
|
||||
|
||||
### 6.3 类型安全问题
|
||||
|
||||
- `PieChart.tsx` 中 tooltip 用 `as Record<string, unknown>[]` 强制转换,丢失类型检查
|
||||
- 多处 URL 参数用 `as` 断言跳过类型校验
|
||||
- `ChatPage` 用了非空断言 `doctorId!`
|
||||
- `main.tsx` 用了非空断言 `document.getElementById('root')!`
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全性问题
|
||||
|
||||
| 等级 | 问题 | 说明 |
|
||||
|------|------|------|
|
||||
| 🔴 高 | Token 存 localStorage | 登录 token 通过 zustand persist 存 localStorage,任何同源 JS 都能读取(XSS 攻击面) |
|
||||
| 🟡 中 | 短信验证码无服务端校验 | `sendSmsCode` 接受 `_phone` 但从未真正验证,客户端倒计时可绕过 |
|
||||
| 🟡 中 | 无 CSRF 保护 | Mock 层没有实现任何防护,后续接入真实后端需补充 |
|
||||
| 🟡 中 | LineChart tooltip XSS 隐患 | tooltip HTML 用模板字符串拼接数据值,若数据含 `<` 等字符可能被注入 |
|
||||
| 🟢 低 | 用户隐私数据明文存储 | `User` 类型包含手机号、生日、病史,存 localStorage 无加密 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 性能问题
|
||||
|
||||
| 等级 | 问题 | 文件 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 🔴 高 | 用药详情获取全部数据 | MedicationDetailPage | 请求所有药品和记录后在客户端 filter,应服务端按 ID 查 |
|
||||
| 🔴 高 | 健康统计每次重新计算 | health.service.ts | `getLatestStats` 遍历全部记录做统计,无缓存 |
|
||||
| 🟡 中 | 多次重复 `diff` 调用 | format.ts | `formatRelative` 中 3 次调用 `dayjs.diff()`,可一次完成 |
|
||||
| 🟡 中 | Inline 函数导致无效渲染 | 全项目 | 所有 `onClick={() => ...}` 在每次 render 创建新函数引用 |
|
||||
| 🟡 中 | 数组浅拷贝开销 | 各 service | 每次读取数据都 `[...mockArray]` 创建新数组 |
|
||||
| 🟢 低 | 咨询消息全量加载 | 类型定义 | `Consultation` 的 `messages` 是完整数组,没有分页 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 无障碍(Accessibility)问题
|
||||
|
||||
### 9.1 重大问题
|
||||
|
||||
| 问题 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| **通知设置页开关不可交互** | `staticPages.tsx:19-21` | 开关是纯装饰性 `<div>`,无 `onClick`、无 `role="switch"`、无 `aria-checked` |
|
||||
| **报告上传区域不可交互** | `ReportUploadPage.tsx:39-43` | 上传区域是 `<div>`,没有 `<input type="file">`,没有 `role="button"`,没有键盘事件 |
|
||||
| **Input 组件 label 不关联 input** | `Input.tsx:11` | `<label>` 没有 `htmlFor` 属性,点击标签不会聚焦输入框 |
|
||||
| **Input 组件错误不关联 input** | `Input.tsx:13` | 错误信息没有 `aria-describedby`,屏幕阅读器不会读错误 |
|
||||
|
||||
### 9.2 普适性问题
|
||||
|
||||
| 问题 | 影响范围 |
|
||||
|------|----------|
|
||||
| Emoji 图标无 `aria-label` | 所有使用 `💓💊👨⚕️📋📅🏃🔔💙` 等 emoji 做图标的地方 |
|
||||
| Tab 切换无 `role="tab"` / `aria-selected` | 所有 Tab 切换页面 |
|
||||
| 导航按钮无 `aria-label` | 所有快捷操作按钮 |
|
||||
| 健康日历无键盘导航 | `HealthCalendarPage` — 没有 `role="grid"`、没有 `tabindex` |
|
||||
| 聊天消息无 `aria-live` | `ChatPage` — 新消息不会被屏幕阅读器自动播报 |
|
||||
| `<button>` 缺少 `type="button"` | 多处按钮默认 `type="submit"`,若被 `<form>` 包裹可能触发意外提交 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 改进建议
|
||||
|
||||
### 10.1 立即修复(影响核心功能)
|
||||
|
||||
1. **修复 ProfilePage 崩溃** → `user?.medicalHistory?.join(',')`
|
||||
2. **修复健康趋势永远显示"稳定"** → `health.service.ts` 改用数值数组计算
|
||||
3. **修复健康日历写死假数据** → 通过 service 层获取数据
|
||||
4. **修复复查列表跳转错误** → 改为正确路径
|
||||
5. **修复 ChatPage 非空断言** → 加 guard 判断
|
||||
6. **修复 FollowUpEditPage try/catch** → 加异常处理
|
||||
|
||||
### 10.2 短期优化(提升体验)
|
||||
|
||||
7. 所有数据获取加 `.catch()` 错误处理
|
||||
8. 所有异步页面加 Loading 骨架屏
|
||||
9. 加 Error Boundary 防止全局崩溃
|
||||
10. 修复 `useCountdown` 的内存泄漏
|
||||
11. 修复 AuthGuard 的 hydration 闪烁问题
|
||||
12. 用 `<form>` + `onSubmit` 替代 `<div>` + `onClick`
|
||||
13. URL 参数加运行时校验
|
||||
|
||||
### 10.3 中期改进(架构升级)
|
||||
|
||||
14. 统一 `MeasurementType` 定义(从 constants 派生,类型文件引用)
|
||||
15. 重构 Toast 组件为 Context/Provider 模式
|
||||
16. Service 层抽离接口,方便后续替换为真实 HTTP 调用
|
||||
17. Mock 层加入错误模拟能力
|
||||
18. 图表组件的无障碍替代文本
|
||||
19. 移除生产代码中的假数据(日历、问诊回复等)
|
||||
|
||||
### 10.4 长期规划(生产就绪)
|
||||
|
||||
20. 接入真实后端 API(HTTP Client 替换 Mock 层)
|
||||
21. Token 改用 httpOnly Cookie 存储
|
||||
22. 添加短信验证码服务端校验 + 发送频率限制
|
||||
23. TypeScript 严格模式(`strict: true`)
|
||||
24. 单元测试 + E2E 测试
|
||||
25. PWA 离线支持(Service Worker)
|
||||
26. i18n 国际化(目前中文硬编码)
|
||||
27. CI/CD 自动构建部署
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:修复前后路由对比
|
||||
|
||||
```tsx
|
||||
// ❌ 修复前 — 第二个路由组渲染 null,所有子页面白屏
|
||||
{
|
||||
path: '/',
|
||||
element: <AuthGuard>{null}</AuthGuard>,
|
||||
children: [ /* 20+ 页面全部无法渲染 */ ],
|
||||
}
|
||||
|
||||
// ✅ 修复后 — 新增 StackLayout 组件渲染 <Outlet />
|
||||
{
|
||||
path: '/',
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<StackLayout /> // 只渲染 <Outlet />,无底部栏
|
||||
</AuthGuard>
|
||||
),
|
||||
children: [ /* 正常渲染 */ ],
|
||||
}
|
||||
```
|
||||
|
||||
## 附录 B:关键文件依赖关系图
|
||||
|
||||
```
|
||||
main.tsx
|
||||
└── App.tsx
|
||||
└── router/index.tsx
|
||||
├── AuthGuard.tsx → hooks/useAuth.ts → stores/auth.store.ts
|
||||
├── AppLayout.tsx → TabBar.tsx
|
||||
├── StackLayout.tsx (新增)
|
||||
└── pages/** (25 个页面组件)
|
||||
└── services/*.service.ts
|
||||
└── mock/index.ts → mock/*.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **审核结论**:项目架构基本合理,组件拆分清晰,类型定义完整。已修复一个导致 20+ 页面白屏的路由 Bug。剩余主要问题是缺少错误处理、Loading 状态、以及部分页面存在假数据硬编码。建议优先处理 10.1 节中的 6 个严重 Bug。
|
||||
Reference in New Issue
Block a user