# 健康管家(健康管家)— 技术审核报告 > **项目名称**: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 参数直接断言为类型,无运行时校验 | | **缺少 `
` 元素** | 所有表单页面 | 用 `
` + `onClick` 代替 `` + `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[]` 强制转换,丢失类型检查 - 多处 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` | 开关是纯装饰性 `
`,无 `onClick`、无 `role="switch"`、无 `aria-checked` | | **报告上传区域不可交互** | `ReportUploadPage.tsx:39-43` | 上传区域是 `
`,没有 ``,没有 `role="button"`,没有键盘事件 | | **Input 组件 label 不关联 input** | `Input.tsx:11` | `