# 健康管家(健康管家)— 技术审核报告
> **项目名称**: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.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 # 堆栈页面布局(无底栏,仅
)
│ │ ├── 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`
**问题(已修复)**:第二个路由组渲染 `
{null}`,没有 `
`,导致 20+ 个堆栈页面全部白屏。
**修复**:将其改为 `
`,`StackLayout` 内部渲染 `
`。
---
### 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**:存在乱码字符 `"已完���状态"`,应为 `"已完成状态"`
- **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 参数直接断言为类型,无运行时校验 |
| **缺少 `