This commit is contained in:
2026-04-09 18:02:31 +08:00
7 changed files with 1704 additions and 1 deletions
+23
View File
@@ -9,3 +9,26 @@
- 墨刀导入兼容约束(禁止外部引用、简洁DOM、内联SVG)
每次生成或修改 HTML 原型文件前,必须先确认符合上述约束。
# 模块文档规范
每个一级功能模块目录下必须创建 `doc/` 子目录,维护以下两个文档:
## doc/CHANGELOG.md(变更记录)
- 记录该模块原型的所有变更,按时间倒序排列
- 每条记录包含:变更日期、变更类型(init/feat/fix/refactor)、涉及文件列表及具体变更说明
- **每次提交代码前,必须自动整理本次变更内容并追加到对应模块的 `doc/CHANGELOG.md` 中**
## doc/PRD.md(产品需求文档)
- 作为该模块的产品需求说明,面向研发人员,简洁为主
- 每次新增或修改原型页面后,必须同步更新 `doc/PRD.md` 中对应的内容
- 文档结构要求:
- **模块概述**:简述模块功能,附页面导航结构树 + 状态流转图(Mermaid)
- **页面清单**:列出所有页面和弹窗
- **逐页说明**:每个页面/弹窗从几个维度描述:
- 功能说明:页面用途和核心元素
- 业务规则:数据逻辑、状态流转、约束条件
- 交互说明:以文字列表形式描述各元素的交互行为和跳转目标,跳转目标使用中文页面名称(如"跳转到专家主页"),不使用文件名
- 表单字段表:如若存在表单提交的页面以表格列出字段名、类型、必填、规则
@@ -0,0 +1,43 @@
# 健康咨询 - 变更记录
> 本文档记录「健康咨询」模块原型的所有变更,按时间倒序排列。
---
## [2026-04-09] fix: 修复返回键路由问题,新增科室/疾病专家数量展示
**涉及文件(7个):**
- `find-expert.html` — 科室二级分类新增专家数量角标,疾病列表新增专家数量展示
- `expert-profile.html` — 修复返回按钮路由逻辑,根据 `?from` 参数精确返回来源页
- `expert-list.html` — 修复返回按钮跳转路径
- `index.html` — 优化专家卡片跳转携带来源参数
- `rate-doctor.html` — 修复返回按钮路由,支持 `?from=text-chat` 参数
- `record-detail.html` — 修复对话入口跳转路径
- `text-chat.html` — 修复结束咨询后跳转路径
---
## [2026-04-08] init: 健康咨询模块初始化
**新增文件(18个):**
| 文件 | 说明 |
|------|------|
| `index.html` | 模块首页:功能入口 + 推荐专家 |
| `find-expert.html` | 找专家:按医院/科室/疾病三维度查找 |
| `expert-list.html` | 专家列表:分类下的专家列表,支持排序和搜索 |
| `expert-profile.html` | 专家主页:个人资料 + 咨询入口 |
| `expert-reviews.html` | 用户评价:评分统计 + 评价列表 |
| `hospital-detail.html` | 医院详情:医院信息 + 本院专家 |
| `my-records.html` | 我的咨询:图文/视频咨询记录列表 |
| `record-detail.html` | 咨询详情:单条记录详情 + 继续咨询 |
| `text-chat.html` | 图文咨询:实时聊天界面 |
| `assistant-chat.html` | AI 小助手:智能问诊对话 |
| `video-booking.html` | 视频预约:选择日期时段 |
| `video-success.html` | 预约成功:预约结果展示 |
| `rate-doctor.html` | 评价医生:星级评分 + 文字评价 |
| `consult-notice-dialog.html` | 弹窗 - 咨询须知 |
| `end-consult-dialog.html` | 弹窗 - 结束咨询确认 |
| `select-person-dialog.html` | 弹窗 - 选择/新增咨询人 |
| `select-report-dialog.html` | 弹窗 - 选择体检报告 |
| `rate-dialog.html` | 弹窗 - 评价医生 |
+451
View File
@@ -0,0 +1,451 @@
# 健康咨询 - 产品需求文档(PRD)
> **所属端**:员工端 APP
> **模块路径**`employee-app/health-consult/`
> **最后更新**2026-04-09
---
## 一、模块概述
为油田员工提供在线健康咨询服务,支持 **AI 智能问诊**、**图文咨询**、**视频咨询** 三种形式。员工可按医院、科室、疾病维度查找专家,发起咨询并管理历史记录。
### 页面导航结构
```
健康咨询首页
├── 找专家
│ ├── [按医院] → 医院详情 → 专家主页
│ ├── [按科室] → 专家列表 → 专家主页
│ └── [按疾病] → 专家列表 → 专家主页
├── AI 小助手
└── 我的咨询 → 咨询详情 → 图文咨询聊天
专家主页
├── [图文咨询] → 咨询须知弹窗 → 图文咨询聊天 → 结束咨询确认弹窗 → 评价医生
├── [视频咨询] → 咨询须知弹窗 → 视频咨询预约 → 预约成功
└── 查看全部评价 → 用户评价列表
```
### 咨询状态流转
```mermaid
stateDiagram-v2
[*] --> 进行中: 发起图文咨询
[*] --> 待开始: 预约视频咨询
待开始 --> 进行中: 到达预约时间
进行中 --> 已完成: 结束咨询
已完成 --> 已评价: 提交评价
```
### 页面清单
| # | 文件 | 类型 | 说明 |
|---|------|------|------|
| 1 | `index.html` | 页面 | 模块首页 |
| 2 | `find-expert.html` | 页面 | 找专家(搜索 + 分类) |
| 3 | `expert-list.html` | 页面 | 专家列表 |
| 4 | `expert-profile.html` | 页面 | 专家主页 |
| 5 | `expert-reviews.html` | 页面 | 用户评价列表 |
| 6 | `hospital-detail.html` | 页面 | 医院详情 |
| 7 | `my-records.html` | 页面 | 我的咨询记录 |
| 8 | `record-detail.html` | 页面 | 咨询详情 |
| 9 | `text-chat.html` | 页面 | 图文咨询聊天 |
| 10 | `assistant-chat.html` | 页面 | AI 小助手聊天 |
| 11 | `video-booking.html` | 页面 | 视频咨询预约 |
| 12 | `video-success.html` | 页面 | 预约成功 |
| 13 | `rate-doctor.html` | 页面 | 评价医生 |
| 14 | `consult-notice-dialog.html` | 弹窗 | 咨询须知 |
| 15 | `end-consult-dialog.html` | 弹窗 | 结束咨询确认 |
| 16 | `select-person-dialog.html` | 弹窗 | 选择/新增咨询人 |
| 17 | `select-report-dialog.html` | 弹窗 | 选择体检报告 |
| 18 | `rate-dialog.html` | 弹窗 | 评价医生(弹窗版) |
---
## 二、页面详细说明
### 2.1 模块首页(index.html
**功能说明**
模块入口页,提供三大功能快捷入口(找专家、AI 小助手、我的咨询)和推荐专家列表。专家卡片展示姓名、职称、医院科室、擅长标签、评分、咨询次数、回复率。
**业务规则**
- 推荐专家由后端根据评分、咨询量等维度综合排序
- 每张专家卡片展示不超过 3 个擅长标签
**交互说明**
- 点击「找专家」跳转到找专家页
- 点击「AI 小助手」跳转到 AI 小助手聊天页
- 点击「我的咨询」跳转到我的咨询记录页
- 点击「查看全部」跳转到找专家页
- 点击专家卡片或卡片上的「咨询」按钮,跳转到专家主页
- 导航栏返回 → 员工端首页
---
### 2.2 找专家(find-expert.html
**功能说明**
三维度查找专家:按医院、按科室、按疾病。顶部搜索栏支持关键词模糊搜索。
- **全部医院**:医院卡片列表,展示名称、等级、距离、专家数、地址
- **全部科室**:左侧一级分类 + 右侧二级科室网格,每项展示科室名和专家数
- **全部疾病**:横向分类 Tab + 疾病列表,每项展示疾病名和专家数
**业务规则**
- 搜索基于 ElasticSearch 模糊匹配,可搜医院、科室、疾病、医生姓名
- 科室体系:6 个一级分类(内科、外科、妇产科、儿科、眼耳鼻喉、皮肤科),每级含多个二级科室
- 疾病体系:6 大类(心血管、内分泌、骨骼肌肉、神经系统、消化系统、呼吸系统)
- 医院距离基于用户当前定位计算
**交互说明**
- 顶部 Tab 切换「全部医院 / 全部科室 / 全部疾病」三个面板
- 点击医院卡片跳转到医院详情页
- 科室面板:点击左侧一级分类联动切换右侧二级科室;点击二级科室项跳转到专家列表页
- 疾病面板:点击横向分类 Tab 切换疾病列表;点击疾病项跳转到专家列表页
- 导航栏返回 → 健康咨询首页
---
### 2.3 专家列表(expert-list.html
**功能说明**
展示特定分类(科室/疾病)下的专家列表。头部展示分类名称和专家总数。支持排序(好评优先 / 热度优先 / 回复率)和按姓名搜索过滤。
**业务规则**
- 默认按好评优先排序
- 搜索为前端实时过滤,匹配医生姓名
- 无结果时展示空状态提示
**交互说明**
- 点击排序按钮切换排序方式,高亮当前选中项
- 搜索输入框输入关键词实时过滤卡片列表
- 点击专家卡片跳转到专家主页
- 导航栏返回 → 找专家页
---
### 2.4 专家主页(expert-profile.html
**功能说明**
专家个人详情页。展示头像、姓名、职称、科室、所属医院,统计数据(综合评价、24h 回复率、总咨询量),医生简介、擅长方向标签,以及最新 3 条用户评价。提供图文咨询和视频咨询两个入口。
**业务规则**
- 点击任一咨询按钮,先弹出「咨询须知」弹窗,用户点击「同意并开始咨询」后才进入对应流程
- 图文咨询确认后直接进入聊天;视频咨询确认后进入预约选时
- 返回按钮根据来源页精确返回(从首页进入则返回首页,从专家列表进入则返回专家列表,其他情况返回上一页)
**交互说明**
- 点击「图文咨询」→ 弹出咨询须知弹窗 → 确认后跳转到图文咨询聊天页
- 点击「视频咨询」→ 弹出咨询须知弹窗 → 确认后跳转到视频咨询预约页
- 点击「查看全部评价」跳转到用户评价列表页
- 点击分享按钮调用系统分享
- 导航栏返回 → 精确返回来源页
---
### 2.5 用户评价列表(expert-reviews.html
**功能说明**
展示专家的完整评价数据。顶部为评分统计卡片:综合评分、5 星~1 星分布进度条、三维度评分(专业性 / 回复速度 / 服务态度)。下方为评价列表,每条含用户头像、姓名、日期、星级、评价文字、评价标签。
**业务规则**
- 评价按时间倒序排列
- 评价标签由系统预设:专业、耐心、回复及时、负责任、个性化、实用
**交互说明**
- 导航栏返回 → 专家主页
---
### 2.6 医院详情(hospital-detail.html
**功能说明**
展示医院信息(实景图、名称、等级、地址、距离、导航按钮)、医院简介(支持展开/收起),以及本院专家列表(排序 + 搜索,逻辑同专家列表页)。
**业务规则**
- 导航按钮调用地图导航(原型仅模拟提示)
- 简介默认收起,超长文本截断
**交互说明**
- 点击导航按钮提示"正在为您导航..."(原型演示)
- 点击展开/收起按钮切换简介文本显示
- 排序按钮切换专家列表排序方式
- 点击专家卡片跳转到专家主页
- 导航栏返回 → 找专家页
---
### 2.7 我的咨询(my-records.html
**功能说明**
展示用户的所有咨询记录。通过 Tab 栏切换「图文咨询 / 视频咨询」。记录卡片展示医生头像、姓名、科室、状态标签、咨询人、消息摘要、时间。
**业务规则**
- 咨询状态及对应样式:`进行中`(蓝色)、`已完成`(绿色)、`待开始`(橙色)
- 已完成的图文咨询如已评价则显示"已评价"标记
- 视频咨询 `待开始` 状态显示预约时间,`已完成` 显示通话时长
**交互说明**
- Tab 栏切换图文/视频记录列表
- 点击记录卡片跳转到咨询详情页
- 导航栏返回 → 健康咨询首页
---
### 2.8 咨询详情(record-detail.html
**功能说明**
展示单条咨询记录详情:医生信息栏 + 状态标签、咨询信息(类型、时间、状态、时长)、对话入口卡片、底部"继续咨询"按钮。
**业务规则**
- 仅「进行中」状态显示"继续咨询"按钮
- 进行中的咨询时长显示 `-`
**交互说明**
- 点击对话入口卡片或「继续咨询」按钮,跳转到图文咨询聊天页
- 导航栏返回 → 我的咨询记录页
---
### 2.9 图文咨询聊天(text-chat.html
**功能说明**
与医生的实时图文聊天界面。聊天头部展示医生头像、姓名、在线状态和「结束咨询」按钮。底部输入区含 + 按钮(展开功能面板)、消息输入框、发送按钮。功能面板提供 5 项操作:图片、拍摄、体检报告、选择档案、结束咨询。
**业务规则**
- 图文咨询有效期为 24 小时
- 消息发送后 HTML 转义防 XSS
- 结束咨询需通过二次确认弹窗
- 本页无导航栏返回按钮,只能通过结束咨询离开
**交互说明**
- 点击发送按钮或按 Enter 发送消息,追加到消息列表
- 点击 + 按钮展开/收起功能面板
- 功能面板点击「体检报告」打开选择体检报告弹窗
- 功能面板点击「选择档案」打开选择咨询人弹窗
- 点击头部「结束咨询」按钮或功能面板中的「结束咨询」,弹出结束咨询确认弹窗,确认后跳转到评价医生页
---
### 2.10 AI 小助手(assistant-chat.html
**功能说明**
AI 智能问诊聊天界面。头部展示机器人头像和"健康小助手"。底部输入区含 + 按钮、消息输入框、发送按钮。功能面板提供 4 项操作:图片、拍摄、体检报告、选择档案。
**业务规则**
- AI 回复为系统自动生成,不可人工干预
- 不替代专业医生诊断,仅作初步健康咨询参考
**交互说明**
- 点击发送按钮发送消息,AI 自动回复
- 点击 + 按钮展开/收起功能面板
- 导航栏返回 → 健康咨询首页
---
### 2.11 视频咨询预约(video-booking.html
**功能说明**
视频咨询预约页面。展示医生信息卡片和预约须知提示。提供日期横向选择器(未来 6 天)和时段网格(上午 6 个 / 下午 6 个),底部确认栏显示已选时段和「立即预约」按钮。
**业务规则**
- 已约满时段置灰不可选,显示"约满"标记
- 可用时段显示剩余号源数
- 必须同时选择日期和时段才能提交预约
- 视频咨询不收取额外费用,如停诊将短信通知
**交互说明**
- 点击日期切换选中日期,时段状态随之更新
- 点击可用时段高亮选中,底部确认栏同步更新
- 点击「立即预约」跳转到预约成功页
- 导航栏返回 → 专家主页
---
### 2.12 预约成功(video-success.html
**功能说明**
视频咨询预约成功结果页。展示成功图标、提示文案(提前 15 分钟推送提醒)、预约信息卡片(医生、医院、科室、类型、时间、费用),以及两个操作按钮。
**业务规则**
- 无导航栏返回按钮,仅通过底部按钮离开
- 费用信息以蓝色高亮显示
**交互说明**
- 点击「查看我的咨询」跳转到我的咨询记录页
- 点击「返回首页」跳转到健康咨询首页
---
### 2.13 评价医生(rate-doctor.html
**功能说明**
咨询结束后对医生进行评价。展示医生信息和"本次咨询已结束"提示,提供星级评分和文字评价输入。
**业务规则**
- 默认 5 星,可下调
- 文字评价为选填
- 提交后跳转到我的咨询记录页
- 返回按钮精确返回来源页(从聊天页进入则返回聊天页,其他情况返回我的咨询记录页)
**交互说明**
- 点击星级图标切换评分
- 点击「提交评价」提交并跳转到我的咨询记录页
- 导航栏返回 → 精确返回来源页
#### 评价表单字段
| 字段 | 类型 | 必填 | 规则 |
|------|------|------|------|
| 星级评分 | 星级选择(1~5) | 是 | 默认 5 星 |
| 评价内容 | 多行文本 | 否 | 无字数限制 |
---
## 三、弹窗说明
### 3.1 咨询须知弹窗(consult-notice-dialog.html
**功能说明**
发起咨询前的告知弹窗,底部滑出样式。所有咨询入口(图文/视频)必须先展示此弹窗,用户同意后才能进入实际咨询流程。
**须知内容:**
1. 本平台提供在线健康咨询服务,不替代线下就医
2. 请如实描述症状和病史,以便医生准确判断
3. 医生将在 24 小时内回复(图文)/ 按预约时间进行(视频)
4. 如遇紧急情况,请立即拨打 120
**交互说明**
- 点击「取消」关闭弹窗,回到专家主页
- 点击「同意并开始咨询」:图文咨询跳转到图文咨询聊天页,视频咨询跳转到视频咨询预约页
- 点击遮罩层关闭弹窗
---
### 3.2 结束咨询确认弹窗(end-consult-dialog.html
**功能说明**
结束图文咨询的二次确认弹窗,底部滑出样式。提示文案:「确认结束本次咨询吗?结束后将跳转至评价页面。」
**交互说明**
- 点击「继续咨询」关闭弹窗
- 点击「确认结束」跳转到评价医生页
---
### 3.3 选择咨询人弹窗(select-person-dialog.html
**功能说明**
选择已有咨询人或新增咨询人,底部全屏滑出。包含两个视图切换:
- **列表视图**:展示已有咨询人卡片(本人 + 家属),点击选中
- **新增表单**:录入新咨询人的基本信息、生活习惯、身体情况
**交互说明**
- 点击咨询人卡片选中并关闭弹窗
- 点击「新增咨询人」按钮切换到新增表单视图
- 表单内点击返回按钮回到列表视图
- 点击「保存」保存新咨询人并回到列表
- 点击关闭按钮关闭弹窗
#### 新增咨询人表单字段
| 字段 | 类型 | 必填 | 规则 |
|------|------|------|------|
| 姓名 | 文本 | 是 | — |
| 性别 | 按钮组单选 | 是 | 男 / 女 |
| 出生日期 | 日期选择 | 是 | 不晚于当天 |
| 与本人关系 | 文本 | 是 | 如:配偶、父亲等 |
| 是否吸烟 | 按钮组单选 | 否 | 否 / 偶尔 / 是 |
| 是否饮酒 | 按钮组单选 | 否 | 否 / 偶尔 / 是 |
| 饮食喜好 | 按钮组单选 | 否 | 清淡 / 均衡 / 偏油腻 / 偏辛辣 |
| 睡眠质量 | 按钮组单选 | 否 | 良好 / 一般 / 较差 |
| 身高 | 数字(cm) | 否 | 正数 |
| 体重 | 数字(kg) | 否 | 正数 |
| 过敏史 | 文本 | 否 | 如:青霉素过敏 |
| 遗传史 | 文本 | 否 | 如:高血压家族史 |
| 既往史 | 文本 | 否 | 如:2020 年阑尾炎手术 |
| 其他疾病 | 文本 | 否 | 如:慢性胃炎 |
---
### 3.4 选择体检报告弹窗(select-report-dialog.html
**功能说明**
从历史体检报告中选择发送给医生,底部全屏滑出。展示报告卡片列表,每张含报告标题、日期、来源医院和「发送」按钮。
**业务规则**
- 已发送的报告按钮变为"已发送"(绿色),不可重复发送
- 报告数据来自健康体检模块
**交互说明**
- 点击「发送」发送报告给医生,按钮变为"已发送"
- 点击关闭按钮关闭弹窗
---
### 3.5 评价医生弹窗(rate-dialog.html
**功能说明**
弹窗形式的评价功能,底部滑出样式。与评价医生页功能相同,展示形式不同。
**业务规则**
- 必须选择星级才能提交,否则弹出提示
- 文字评价选填
**交互说明**
- 点击「取消」关闭弹窗
- 点击「提交评价」校验评分 → 提交 → 关闭弹窗
+469
View File
@@ -0,0 +1,469 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>健康长庆升级 - 高保真交互原型导航</title>
<style>
:root {
--primary: #1677FF;
--primary-light: #E6F7FF;
--primary-dark: #0958D9;
--success: #52C41A;
--warning: #FAAD14;
--danger: #FF4D4F;
--purple: #722ED1;
--green: #52C41A;
--green-light: #f6ffed;
--green-border: #b7eb8f;
--text-primary: #1a1a2e;
--text-secondary: #5a607f;
--text-placeholder: #999999;
--border: #e8ecf1;
--background: #f0f2f8;
--card-bg: #ffffff;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
--shadow-md: 0 4px 16px rgba(22,119,255,0.10), 0 2px 6px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 30px rgba(22,119,255,0.15), 0 4px 10px rgba(0,0,0,0.06);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--background); font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Segoe UI', sans-serif; color: var(--text-primary); min-height: 100vh; }
/* ===== Header ===== */
.header { position: relative; background: linear-gradient(135deg, #0b3d91 0%, var(--primary) 40%, #4facfe 100%); color: #fff; padding: 40px 48px 20px; overflow: hidden; }
.header::before { content: ''; position: absolute; top: -60px; right: -40px; width: 260px; height: 260px; background: radial-gradient(circle, rgba(255,255,255,0.12) 0%, transparent 70%); border-radius: 50%; }
.header::after { content: ''; position: absolute; bottom: -80px; left: 20%; width: 360px; height: 360px; background: radial-gradient(circle, rgba(255,255,255,0.06) 0%, transparent 70%); border-radius: 50%; }
.header__inner { position: relative; z-index: 1; max-width: 1400px; margin: 0 auto; text-align: center; }
.header__title { font-size: 28px; font-weight: 800; letter-spacing: 1px; text-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.header__sub { font-size: 14px; color: rgba(255,255,255,0.7); margin-top: 8px; }
/* ===== Tab 导航(嵌入 Header 底部) ===== */
.tab-nav { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 20px 48px 0; position: relative; z-index: 1; }
.tab-nav__item { padding: 10px 28px; font-size: 15px; font-weight: 600; color: rgba(255,255,255,0.7); cursor: pointer; border-radius: 28px; transition: all .25s ease; user-select: none; display: flex; align-items: center; gap: 8px; border: 1.5px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.08); backdrop-filter: blur(6px); }
.tab-nav__item:hover { color: #fff; background: rgba(255,255,255,0.18); border-color: rgba(255,255,255,0.4); }
.tab-nav__item--active { color: var(--primary-dark); background: #fff; border-color: #fff; box-shadow: 0 4px 16px rgba(0,0,0,0.15); }
.tab-nav__item--active .tab-nav__badge { background: var(--primary); color: #fff; }
.tab-nav__badge { font-size: 11px; padding: 2px 9px; border-radius: 10px; background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.9); font-weight: 700; transition: all .25s; }
.tab-nav__icon { width: 18px; height: 18px; flex-shrink: 0; }
/* ===== 内容区 ===== */
.main-content { max-width: 1400px; margin: 0 auto; padding: 28px 48px 48px; }
.tab-content { display: none; animation: fadeUp .35s ease; }
.tab-content--active { display: block; }
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
/* ===== 模块网格 ===== */
.module-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; }
.module-grid--center { justify-content: center; display: flex; flex-wrap: wrap; gap: 20px; }
.module-grid--center .module-card { width: 320px; flex-shrink: 0; }
/* ===== 角色分组 ===== */
.role-group { margin-bottom: 28px; text-align: center; }
.role-group:last-child { margin-bottom: 0; }
.role-group__title { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 16px; padding: 6px 16px 6px 12px; background: var(--card-bg); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); }
.role-icon { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 13px; color: #fff; flex-shrink: 0; }
.role-icon--expert { background: linear-gradient(135deg, #1677FF, #4facfe); }
.role-icon--assistant { background: linear-gradient(135deg, #722ED1, #b37feb); }
.role-icon--operator { background: linear-gradient(135deg, #fa541c, #ff7a45); }
.role-icon--worker { background: linear-gradient(135deg, #52c41a, #95de64); }
.role-icon--onsite { background: linear-gradient(135deg, #faad14, #ffc53d); }
.role-icon--admin { background: linear-gradient(135deg, #eb2f96, #ff85c0); }
.role-icon--shared { background: linear-gradient(135deg, #13c2c2, #5cdbd3); }
/* ===== 呼吸灯动画 ===== */
@keyframes breathe {
0%, 100% { border-color: rgba(82,196,26,0.35); box-shadow: 0 0 0 0 rgba(82,196,26,0); }
50% { border-color: rgba(82,196,26,0.7); box-shadow: 0 0 12px 2px rgba(82,196,26,0.18); }
}
/* ===== 模块卡片 ===== */
.module-card { position: relative; background: var(--card-bg); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: 20px; transition: all .25s ease; border: 1.5px solid var(--border); cursor: pointer; z-index: 1; }
.module-card--pop-open { z-index: 200; }
.module-card:hover { box-shadow: var(--shadow-lg); transform: translateY(-3px); }
.module-card--ready { border-color: var(--green-border); animation: breathe 3s ease-in-out infinite; }
.module-card--ready:hover { border-color: var(--green); animation: none; box-shadow: 0 8px 24px rgba(82,196,26,0.2); }
.module-card--empty { opacity: 0.55; border-style: dashed; border-color: #d0d5dd; cursor: default; }
.module-card--empty:hover { transform: none; box-shadow: var(--shadow-sm); }
.module-card__header { display: flex; align-items: center; justify-content: space-between; }
.module-card__name { font-size: 15px; font-weight: 700; color: var(--text-primary); }
.module-card__tag { position: relative; font-size: 11px; padding: 3px 10px; border-radius: 12px; font-weight: 600; letter-spacing: 0.3px; }
.module-card__tag--ready { background: var(--green-light); color: #389e0d; border: 1px solid var(--green-border); cursor: pointer; transition: all .15s; }
.module-card__tag--ready:hover { background: #d9f7be; transform: scale(1.05); }
.module-card__tag--pending { background: #fffbe6; color: #d48806; border: 1px solid #ffe58f; }
.module-card__empty-hint { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-placeholder); margin-top: 10px; }
/* ===== 悬浮页面列表 ===== */
.page-popover { display: none; position: absolute; top: 100%; right: 0; margin-top: 6px; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: 0 8px 30px rgba(0,0,0,0.15); padding: 8px 0; z-index: 100; min-width: 200px; max-height: 320px; overflow-y: auto; animation: popIn .2s ease; }
.page-popover--visible { display: block; }
@keyframes popIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
.page-popover__title { font-size: 12px; color: var(--text-placeholder); padding: 4px 14px 8px; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
.page-popover__item { display: flex; align-items: center; gap: 8px; padding: 7px 14px; font-size: 13px; color: var(--text-primary); text-decoration: none; transition: background .15s; }
.page-popover__item:hover { background: var(--primary-light); color: var(--primary-dark); }
.page-popover__item svg { flex-shrink: 0; }
.page-popover__item--dialog { color: #ad4e00; }
.page-popover__item--dialog:hover { background: #fff7e6; color: #ad4e00; }
/* ===== 卡片底部 PRD 链接 ===== */
.module-card__footer { display: flex; justify-content: flex-start; margin-top: 10px; }
.module-card__prd { font-size: 12px; color: var(--primary); cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: var(--radius-sm); transition: all .15s; }
.module-card__prd:hover { background: var(--primary-light); color: var(--primary-dark); }
.module-card__prd svg { flex-shrink: 0; }
/* ===== PRD Tab 页 ===== */
.prd-page__global { margin-bottom: 28px; }
.prd-global-card { background: linear-gradient(135deg, #f6ffed 0%, #e6f7ff 100%); border: 1.5px solid var(--green-border); border-radius: var(--radius-lg); padding: 20px 24px; cursor: pointer; transition: all .25s; display: flex; align-items: center; gap: 16px; }
.prd-global-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
.prd-global-card__icon { width: 44px; height: 44px; border-radius: 12px; background: linear-gradient(135deg, var(--primary), #4facfe); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.prd-global-card__info { flex: 1; }
.prd-global-card__title { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.prd-global-card__desc { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }
.prd-section { margin-bottom: 28px; }
.prd-section__title { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 14px; padding: 6px 16px; background: var(--card-bg); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); display: inline-flex; align-items: center; gap: 8px; }
.prd-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.prd-item { background: var(--card-bg); border: 1.5px solid var(--border); border-radius: var(--radius-md); padding: 16px 18px; cursor: pointer; transition: all .2s; display: flex; align-items: center; gap: 12px; }
.prd-item:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--primary); }
.prd-item--disabled { opacity: 0.45; cursor: default; border-style: dashed; }
.prd-item--disabled:hover { box-shadow: none; transform: none; border-color: var(--border); }
.prd-item__icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 18px; }
.prd-item__icon--ready { background: var(--green-light); color: #389e0d; }
.prd-item__icon--empty { background: #f5f5f5; color: #bbb; }
.prd-item__info { flex: 1; min-width: 0; }
.prd-item__name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.prd-item__path { font-size: 11px; color: var(--text-placeholder); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prd-item__status { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; flex-shrink: 0; }
.prd-item__status--ready { background: var(--green-light); color: #389e0d; border: 1px solid var(--green-border); }
.prd-item__status--empty { background: #fffbe6; color: #d48806; border: 1px solid #ffe58f; }
</style>
</head>
<body>
<div class="header">
<div class="header__inner">
<div class="header__title">健康长庆升级 · 高保真交互原型</div>
<div class="header__sub">点击模块卡片跳转至对应模块主页,点击页数徽章查看全部页面</div>
</div>
<div class="tab-nav" id="tab-nav"></div>
</div>
<div class="main-content">
<div id="tab-contents"></div>
</div>
<script>
/* ===== SVG 图标 ===== */
const ICONS = {
web: '<svg class="tab-nav__icon" viewBox="0 0 18 18" fill="none"><rect x="1" y="2" width="16" height="12" rx="2" stroke="currentColor" stroke-width="1.5"/><line x1="5" y1="16" x2="13" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="9" y1="14" x2="9" y2="16" stroke="currentColor" stroke-width="1.5"/></svg>',
app: '<svg class="tab-nav__icon" viewBox="0 0 18 18" fill="none"><rect x="4" y="1" width="10" height="16" rx="2" stroke="currentColor" stroke-width="1.5"/><line x1="7.5" y1="14" x2="10.5" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
emergency: '<svg class="tab-nav__icon" viewBox="0 0 18 18" fill="none"><path d="M9 1L2 6.5V16h5v-4h4v4h5V6.5L9 1z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9 7v3M9 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
page: '<svg width="14" height="14" viewBox="0 0 12 12" fill="none"><path d="M3 1h4l3 3v7H3V1z" stroke="currentColor" stroke-width="1.1"/><path d="M7 1v3h3" stroke="currentColor" stroke-width="1.1"/></svg>',
dialog: '<svg width="14" height="14" viewBox="0 0 12 12" fill="none"><rect x="1.5" y="2" width="9" height="8" rx="1.5" stroke="currentColor" stroke-width="1.1"/><line x1="4" y1="5" x2="8" y2="5" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/><line x1="4" y1="7.5" x2="6.5" y2="7.5" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/></svg>',
empty: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.2" stroke-dasharray="3 2"/><path d="M8 5.5v3M8 10v.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>',
prd: '<svg class="tab-nav__icon" viewBox="0 0 18 18" fill="none"><path d="M4 1h7l4 4v12H4V1z" stroke="currentColor" stroke-width="1.5"/><path d="M11 1v4h4" stroke="currentColor" stroke-width="1.5"/><line x1="7" y1="9" x2="13" y2="9" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><line x1="7" y1="12" x2="11" y2="12" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>',
prdSmall: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2.5 1h5l2.5 2.5v7.5h-7.5V1z" stroke="currentColor" stroke-width="1"/><path d="M7.5 1v2.5h2.5" stroke="currentColor" stroke-width="1"/></svg>',
role: { expert:'👨‍⚕', assistant:'🤖', operator:'📋', worker:'👷', onsite:'🏥', admin:'⚙', shared:'🔗' }
};
const SITE_MAP = [
{ id:'web-admin', label:'Web 管理端', icon:'web', color:'#69b1ff', modules:[
{ name:'系统管理', dir:'web-admin/system-management', pages:[] },
{ name:'健康体检', dir:'web-admin/health-checkup', pages:[] },
{ name:'体重管理', dir:'web-admin/weight-management', pages:[] },
{ name:'营养管理', dir:'web-admin/nutrition', pages:[] },
{ name:'运动管理', dir:'web-admin/exercise', pages:[] },
{ name:'健康监测', dir:'web-admin/health-monitor', pages:[] },
{ name:'健康评估', dir:'web-admin/health-assessment', pages:[] },
{ name:'知识普及', dir:'web-admin/knowledge', pages:[] },
{ name:'健康数据', dir:'web-admin/health-data', pages:[] },
{ name:'健康档案', dir:'web-admin/health-record', pages:[] },
{ name:'心脑血管病预防', dir:'web-admin/cardiovascular', pages:[] },
{ name:'糖尿病预防', dir:'web-admin/diabetes', pages:[] },
{ name:'癌症预防', dir:'web-admin/cancer', pages:[] },
{ name:'专家咨询', dir:'web-admin/health-consult', pages:[] },
{ name:'应急就医', dir:'web-admin/emergency-dispatch', pages:[] },
{ name:'一线医疗', dir:'web-admin/frontline-medical', pages:[] }
]},
{ id:'employee-app', label:'员工端 APP', icon:'app', color:'#95de64', modules:[
{ name:'AI', dir:'employee-app/ai', pages:[] },
{ name:'应用主页', dir:'employee-app/home', pages:[] },
{ name:'健康体检', dir:'employee-app/health-checkup', pages:[] },
{ name:'体重管理', dir:'employee-app/weight-management', pages:[] },
{ name:'营养管理', dir:'employee-app/nutrition', pages:[] },
{ name:'运动管理', dir:'employee-app/exercise', pages:[] },
{ name:'健康监测', dir:'employee-app/health-monitor', pages:[] },
{ name:'健康评估', dir:'employee-app/health-assessment', pages:[] },
{ name:'心脑血管病预防', dir:'employee-app/cardiovascular', pages:[] },
{ name:'糖尿病预防', dir:'employee-app/diabetes', pages:[] },
{ name:'癌症预防', dir:'employee-app/cancer', pages:[] },
{ name:'健康咨询', dir:'employee-app/health-consult', prd:'employee-app/health-consult/doc/PRD.md', pages:[
{ file:'index.html', label:'咨询首页' }, { file:'expert-list.html', label:'专家列表' },
{ file:'expert-profile.html', label:'专家主页' }, { file:'find-expert.html', label:'找专家' },
{ file:'text-chat.html', label:'图文咨询' }, { file:'video-booking.html', label:'视频预约' },
{ file:'video-success.html', label:'预约成功' }, { file:'expert-reviews.html', label:'专家评价' },
{ file:'hospital-detail.html', label:'医院详情' }, { file:'my-records.html', label:'咨询记录' },
{ file:'record-detail.html', label:'记录详情' }, { file:'rate-doctor.html', label:'评价医生' },
{ file:'assistant-chat.html', label:'小助手对话' },
{ file:'consult-notice-dialog.html', label:'咨询须知弹窗', dialog:true },
{ file:'end-consult-dialog.html', label:'结束咨询弹窗', dialog:true },
{ file:'select-person-dialog.html', label:'选择就诊人弹窗', dialog:true },
{ file:'select-report-dialog.html', label:'选择报告弹窗', dialog:true },
{ file:'rate-dialog.html', label:'评价弹窗', dialog:true }
]},
{ name:'应急就医', dir:'employee-app/emergency-medical', pages:[] },
{ name:'一线医疗', dir:'employee-app/frontline-medical', pages:[] },
{ name:'健康档案', dir:'employee-app/health-record', pages:[] },
{ name:'知识普及', dir:'employee-app/knowledge', pages:[] },
{ name:'个人中心', dir:'employee-app/profile', pages:[] }
]},
{ id:'emergency-app', label:'健康应急 APP', icon:'emergency', color:'#ffc069',
roles:[
{ role:'咨询专家', id:'expert', modules:[
{ name:'工作台', dir:'emergency-app/expert/workbench', pages:[
{file:'index.html',label:'工作台首页'},{file:'text-chat.html',label:'图文咨询'},
{file:'text-consult-done.html',label:'咨询完成'},{file:'video-appt.html',label:'视频预约'},
{file:'video-call.html',label:'视频通话'},{file:'video-done.html',label:'视频完成'},
{file:'video-ongoing.html',label:'视频进行中'},{file:'employee-profile.html',label:'员工档案'}
]},
{ name:'知识库', dir:'emergency-app/expert/knowledge', pages:[
{file:'index.html',label:'知识库首页'},{file:'detail.html',label:'知识详情'}
]},
{ name:'我的', dir:'emergency-app/expert/profile', pages:[
{file:'index.html',label:'个人中心'},{file:'edit-profile.html',label:'编辑资料'},
{file:'change-pwd.html',label:'修改密码'},{file:'schedule.html',label:'排班设置'},
{file:'stats.html',label:'咨询统计'},{file:'reviews.html',label:'我的评价'}
]}
]},
{ role:'咨询小助手', id:'assistant', modules:[
{ name:'工作台', dir:'emergency-app/assistant/workbench', pages:[
{file:'index.html',label:'工作台首页'},{file:'text-chat.html',label:'图文咨询'},
{file:'consult-done.html',label:'咨询完成'},{file:'employee-profile.html',label:'员工档案'},
{file:'end-consult-dialog.html',label:'结束咨询弹窗',dialog:true},
{file:'push-expert.html',label:'推送专家'}
]},
{ name:'知识库', dir:'emergency-app/assistant/knowledge', pages:[] },
{ name:'我的', dir:'emergency-app/assistant/profile', pages:[] }
]},
{ role:'应急操作人员', id:'operator', modules:[
{name:'工作台',dir:'emergency-app/operator/workbench',pages:[]},
{name:'知识库',dir:'emergency-app/operator/knowledge',pages:[]},
{name:'我的',dir:'emergency-app/operator/profile',pages:[]}
]},
{ role:'应急工作人员', id:'worker', modules:[
{name:'工作台',dir:'emergency-app/worker/workbench',pages:[]},
{name:'知识库',dir:'emergency-app/worker/knowledge',pages:[]},
{name:'我的',dir:'emergency-app/worker/profile',pages:[]}
]},
{ role:'应急驻场人员', id:'onsite', modules:[
{name:'工作台',dir:'emergency-app/onsite/workbench',pages:[]},
{name:'知识库',dir:'emergency-app/onsite/knowledge',pages:[]},
{name:'我的',dir:'emergency-app/onsite/profile',pages:[]}
]},
{ role:'管理员', id:'admin', modules:[
{name:'工作台',dir:'emergency-app/admin/workbench',pages:[]},
{name:'知识库',dir:'emergency-app/admin/knowledge',pages:[]},
{name:'我的',dir:'emergency-app/admin/profile',pages:[]}
]}
],
shared:[
{ name:'AI助手', dir:'emergency-app/ai-assistant', pages:[{file:'index.html',label:'AI助手'}] },
{ name:'消息', dir:'emergency-app/message', pages:[
{file:'index.html',label:'消息列表'},{file:'contacts.html',label:'通讯录'},{file:'im-chat.html',label:'聊天'}
]}
]
}
];
/* ===== 渲染逻辑 ===== */
function countPages(t) {
if (t.modules) return t.modules.reduce((s, m) => s + m.pages.length, 0);
let n = 0;
if (t.roles) t.roles.forEach(r => r.modules.forEach(m => n += m.pages.length));
if (t.shared) t.shared.forEach(m => n += m.pages.length);
return n;
}
/* Tab 导航 */
const tabNav = document.getElementById('tab-nav');
const tabContents = document.getElementById('tab-contents');
const totalTabs = SITE_MAP.length + 1; /* +1 for PRD tab */
SITE_MAP.forEach((t, i) => {
tabNav.innerHTML += `<div class="tab-nav__item${i===0?' tab-nav__item--active':''}" data-idx="${i}">${ICONS[t.icon]}${t.label}<span class="tab-nav__badge">${countPages(t)}</span></div>`;
});
/* PRD Tab */
const prdTabIdx = SITE_MAP.length;
tabNav.innerHTML += `<div class="tab-nav__item" data-idx="${prdTabIdx}">${ICONS.prd}产品需求文档</div>`;
/* 生成卡片 — 点击卡片跳转主页,点击徽章弹出页面列表 */
let openPopover = null;
function cardHTML(mod, uid) {
const has = mod.pages.length > 0;
const cls = has ? 'module-card module-card--ready' : 'module-card module-card--empty';
const mainHref = has ? `${mod.dir}/index.html` : '';
const onclick = has ? `onclick="window.open('${mainHref}','_blank')"` : '';
let tagHtml = '';
if (has) {
/* 生成悬浮列表 */
let popItems = mod.pages.map(p => {
const dcls = p.dialog ? ' page-popover__item--dialog' : '';
const icon = p.dialog ? ICONS.dialog : ICONS.page;
return `<a class="page-popover__item${dcls}" href="${mod.dir}/${p.file}" target="_blank">${icon}${p.label}</a>`;
}).join('');
tagHtml = `<span class="module-card__tag module-card__tag--ready" data-pop="${uid}" onclick="event.stopPropagation();togglePop('${uid}')">${mod.pages.length} 页</span><div class="page-popover" id="pop-${uid}" onclick="event.stopPropagation()"><div class="page-popover__title">页面列表</div>${popItems}</div>`;
} else {
tagHtml = '<span class="module-card__tag module-card__tag--pending">待开发</span>';
}
const body = has ? '' : `<div class="module-card__empty-hint">${ICONS.empty}暂无原型页面</div>`;
/* PRD 文档链接(右下角) */
let prdHtml = '';
if (mod.prd) {
prdHtml = `<div class="module-card__footer"><a class="module-card__prd" onclick="event.stopPropagation();openMdPreview('${mod.prd}','${mod.name} PRD')">${ICONS.prdSmall}PRD文档</a></div>`;
}
return `<div class="${cls}" ${onclick}><div class="module-card__header"><span class="module-card__name">${mod.name}</span>${tagHtml}</div>${body}${prdHtml}</div>`;
}
function togglePop(uid) {
const el = document.getElementById('pop-' + uid);
if (openPopover && openPopover !== el) {
openPopover.classList.remove('page-popover--visible');
openPopover.closest('.module-card').classList.remove('module-card--pop-open');
}
el.classList.toggle('page-popover--visible');
const card = el.closest('.module-card');
if (el.classList.contains('page-popover--visible')) {
card.classList.add('module-card--pop-open');
openPopover = el;
} else {
card.classList.remove('module-card--pop-open');
openPopover = null;
}
}
document.addEventListener('click', () => {
if (openPopover) {
openPopover.closest('.module-card').classList.remove('module-card--pop-open');
openPopover.classList.remove('page-popover--visible');
openPopover = null;
}
});
/* 生成 Tab 内容 */
let uidCounter = 0;
SITE_MAP.forEach((t, i) => {
const div = document.createElement('div');
div.className = 'tab-content' + (i === 0 ? ' tab-content--active' : '');
div.dataset.idx = i;
let html = '';
if (t.modules) {
html = '<div class="module-grid">' + t.modules.map(m => cardHTML(m, 'u' + uidCounter++)).join('') + '</div>';
} else {
t.roles.forEach(r => {
const icon = ICONS.role[r.id] || '';
html += `<div class="role-group"><div class="role-group__title"><span class="role-icon role-icon--${r.id}">${icon}</span>${r.role}</div><div class="module-grid module-grid--center">`;
html += r.modules.map(m => cardHTML(m, 'u' + uidCounter++)).join('');
html += '</div></div>';
});
html += `<div class="role-group"><div class="role-group__title"><span class="role-icon role-icon--shared">${ICONS.role.shared}</span>全角色共享</div><div class="module-grid module-grid--center">`;
html += t.shared.map(m => cardHTML(m, 'u' + uidCounter++)).join('');
html += '</div></div>';
}
div.innerHTML = html;
tabContents.appendChild(div);
});
/* Tab 切换 */
tabNav.addEventListener('click', e => {
const item = e.target.closest('.tab-nav__item');
if (!item) return;
const idx = item.dataset.idx;
tabNav.querySelectorAll('.tab-nav__item').forEach(el => el.classList.remove('tab-nav__item--active'));
tabContents.querySelectorAll('.tab-content').forEach(el => el.classList.remove('tab-content--active'));
item.classList.add('tab-nav__item--active');
tabContents.querySelector(`[data-idx="${idx}"]`).classList.add('tab-content--active');
});
/* ===== PRD Tab 内容生成 ===== */
(function buildPrdTab() {
const div = document.createElement('div');
div.className = 'tab-content';
div.dataset.idx = prdTabIdx;
let html = '';
/* 顶部:共性需求说明 */
html += `<div class="prd-page__global">
<div class="prd-global-card" onclick="openMdPreview('共性需求说明.md','共性需求说明')">
<div class="prd-global-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 2h9l5 5v15H6V2z" stroke="#fff" stroke-width="1.8"/><path d="M15 2v5h5" stroke="#fff" stroke-width="1.8"/><line x1="9" y1="11" x2="17" y2="11" stroke="#fff" stroke-width="1.4" stroke-linecap="round"/><line x1="9" y1="14.5" x2="15" y2="14.5" stroke="#fff" stroke-width="1.4" stroke-linecap="round"/><line x1="9" y1="18" x2="13" y2="18" stroke="#fff" stroke-width="1.4" stroke-linecap="round"/></svg>
</div>
<div class="prd-global-card__info">
<div class="prd-global-card__title">共性需求说明</div>
<div class="prd-global-card__desc">全平台通用的业务规则、交互规范和共性功能说明</div>
</div>
</div>
</div>`;
/* 收集所有模块的 PRD 信息 */
function collectModules(siteEntry) {
const result = [];
if (siteEntry.modules) {
siteEntry.modules.forEach(m => {
result.push({ name: m.name, dir: m.dir, prd: m.prd || null });
});
}
if (siteEntry.roles) {
siteEntry.roles.forEach(r => {
r.modules.forEach(m => {
result.push({ name: r.role + ' - ' + m.name, dir: m.dir, prd: m.prd || null });
});
});
}
if (siteEntry.shared) {
siteEntry.shared.forEach(m => {
result.push({ name: '共享 - ' + m.name, dir: m.dir, prd: m.prd || null });
});
}
return result;
}
SITE_MAP.forEach(t => {
const mods = collectModules(t);
const readyCount = mods.filter(m => m.prd).length;
html += `<div class="prd-section">
<div class="prd-section__title">${ICONS[t.icon] || ''}${t.label}<span class="tab-nav__badge" style="margin-left:6px">${readyCount}/${mods.length}</span></div>
<div class="prd-grid">`;
mods.forEach(m => {
if (m.prd) {
html += `<div class="prd-item" onclick="openMdPreview('${m.prd}','${m.name} PRD')">
<div class="prd-item__icon prd-item__icon--ready">📄</div>
<div class="prd-item__info">
<div class="prd-item__name">${m.name} PRD</div>
<div class="prd-item__path">${m.prd}</div>
</div>
<span class="prd-item__status prd-item__status--ready">已就绪</span>
</div>`;
} else {
html += `<div class="prd-item prd-item--disabled">
<div class="prd-item__icon prd-item__icon--empty">📄</div>
<div class="prd-item__info">
<div class="prd-item__name">${m.name} PRD</div>
<div class="prd-item__path">${m.dir}/doc/PRD.md</div>
</div>
<span class="prd-item__status prd-item__status--empty">待编写</span>
</div>`;
}
});
html += '</div></div>';
});
div.innerHTML = html;
tabContents.appendChild(div);
})();
/* ===== Markdown 预览(新标签页打开) ===== */
function openMdPreview(filePath, title) {
window.open('md-viewer.html?file=' + encodeURIComponent(filePath) + '&title=' + encodeURIComponent(title || ''), '_blank');
}
</script>
</body>
</html>
+119
View File
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文档预览</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--primary: #1677FF;
--primary-light: #E6F7FF;
--text-primary: #1a1a2e;
--text-secondary: #5a607f;
--text-placeholder: #999;
--border: #e8ecf1;
--bg: #ffffff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Segoe UI', sans-serif; background: #f5f7fa; color: var(--text-primary); }
/* 顶部导航栏 */
.topbar { position: sticky; top: 0; z-index: 100; background: var(--bg); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.topbar__back { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: var(--primary); cursor: pointer; text-decoration: none; padding: 4px 10px; border-radius: 6px; transition: background .15s; border: none; background: none; }
.topbar__back:hover { background: var(--primary-light); }
.topbar__title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
.topbar__path { font-size: 12px; color: var(--text-placeholder); margin-left: auto; }
/* 内容区 */
.content { max-width: 900px; margin: 0 auto; padding: 32px 40px 60px; background: var(--bg); min-height: calc(100vh - 50px); }
.loading { display: flex; align-items: center; justify-content: center; height: 300px; color: var(--text-placeholder); font-size: 14px; }
.error { color: #ff4d4f; }
/* Markdown 渲染样式 */
.content h1 { font-size: 24px; font-weight: 800; margin: 0 0 16px; padding-bottom: 10px; border-bottom: 2px solid var(--border); }
.content h2 { font-size: 20px; font-weight: 700; margin: 32px 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.content h3 { font-size: 17px; font-weight: 600; margin: 24px 0 8px; }
.content h4 { font-size: 15px; font-weight: 600; margin: 20px 0 6px; }
.content p { font-size: 14px; line-height: 1.8; margin: 8px 0; }
.content ul, .content ol { padding-left: 20px; margin: 8px 0; }
.content table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
.content th { background: #fafafa; font-weight: 600; text-align: left; padding: 8px 12px; border: 1px solid var(--border); }
.content td { padding: 8px 12px; border: 1px solid var(--border); }
.content code { background: #f5f5f5; padding: 2px 6px; border-radius: 4px; font-size: 13px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace; }
.content pre { background: #f6f8fa; border-radius: 8px; padding: 16px; overflow-x: auto; margin: 12px 0; }
.content pre code { background: none; padding: 0; font-size: 13px; line-height: 1.6; }
.content blockquote { border-left: 4px solid var(--primary); padding: 8px 16px; margin: 12px 0; background: var(--primary-light); border-radius: 0 4px 4px 0; }
.content blockquote p { color: var(--text-secondary); margin: 0; }
.content strong { color: var(--text-primary); }
.content hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
</style>
</head>
<body>
<div class="topbar">
<button class="topbar__back" onclick="window.close();history.back()">← 返回</button>
<span class="topbar__title" id="doc-title">文档预览</span>
<span class="topbar__path" id="doc-path"></span>
</div>
<div class="content" id="content">
<div class="loading">加载中...</div>
</div>
<script>
const params = new URLSearchParams(location.search);
const filePath = params.get('file');
const title = params.get('title');
if (title) {
document.getElementById('doc-title').textContent = title;
document.title = title + ' - 文档预览';
}
if (filePath) {
document.getElementById('doc-path').textContent = filePath;
fetch(filePath)
.then(res => {
if (!res.ok) throw new Error('文件未找到 (' + res.status + ')');
return res.text();
})
.then(md => {
/* 检测是否为自带 HTML 结构的文件(如共性需求说明.md) */
const isRawHtml = /^\s*<(?:style|div|!doctype|html)/i.test(md);
if (isRawHtml) {
/* 文件混合了 HTML 结构和 Markdown 内容,需先解析 Markdown 再渲染 */
let processed = md;
if (typeof marked !== 'undefined') {
/* 保留 <style>/<div> 等 HTML 标签,只对标签外的 Markdown 文本做解析 */
processed = md.replace(/(^[\s\S]*?<div id="main-content">)([\s\S]*?)(<\/div>\s*$)/i, function(_, before, mdBody, after) {
return before + marked.parse(mdBody) + after;
});
/* 同时解析 toc-container 内的 Markdown 列表 */
processed = processed.replace(/(<div id="toc-container">)([\s\S]*?)(<\/div>)/, function(_, before, tocBody, after) {
return before + marked.parse(tocBody) + after;
});
}
/* 用 iframe 隔离渲染,避免宿主页样式冲突 */
document.querySelector('.topbar').style.display = 'none';
const wrap = document.getElementById('content');
wrap.style.cssText = 'max-width:none;margin:0;padding:0;min-height:100vh';
const iframe = document.createElement('iframe');
iframe.style.cssText = 'width:100%;height:100vh;border:none';
iframe.srcdoc = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>' + processed + '</body></html>';
wrap.innerHTML = '';
wrap.appendChild(iframe);
} else {
const content = document.getElementById('content');
if (typeof marked !== 'undefined') {
content.innerHTML = marked.parse(md);
} else {
content.innerHTML = '<pre style="white-space:pre-wrap;font-size:14px;line-height:1.8">' + md.replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</pre>';
}
}
})
.catch(err => {
document.getElementById('content').innerHTML = '<div class="loading error">加载失败:' + err.message + '<br><small style="color:var(--text-placeholder);margin-top:8px;display:block">请确认文件路径:' + filePath + '</small></div>';
});
} else {
document.getElementById('content').innerHTML = '<div class="loading">未指定文件路径</div>';
}
</script>
</body>
</html>
+554
View File
@@ -0,0 +1,554 @@
<style>
/* 浏览器预览时左侧固定目录导航 */
@media screen {
#toc-container {
position: fixed;
left: 0;
top: 0;
width: 280px;
height: 100vh;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
border-right: 1px solid #e0e0e0;
box-sizing: border-box;
font-size: 14px;
z-index: 1000;
}
#toc-container ul { list-style: none; padding-left: 16px; }
#toc-container > ul { padding-left: 0; }
#toc-container a { color: #333; text-decoration: none; display: block; padding: 3px 0; }
#toc-container a:hover { color: #0366d6; }
#main-content { margin-left: 300px; max-width: 900px; padding: 20px; }
}
@media print { #toc-container { position: static; width: auto; } #main-content { margin-left: 0; } }
</style>
<div id="toc-container">
## 目录
- [一、文档概述](#一文档概述)
- [二、项目背景与目标](#二项目背景与目标)
- [(一)项目背景](#一项目背景)
- [(二)项目目标](#二项目目标)
- [(三)交付标准](#三交付标准)
- [三、项目需求分析](#三项目需求分析)
- [(一)用户画像分析](#一用户画像分析)
- [(二)项目交付成果分析](#二项目交付成果分析)
- [(三)核心业务分析](#三核心业务分析)
- [四、共性需求说明](#四共性需求说明)
- [(一)PC端共性需求](#一pc端共性需求)
- [1.布局与框架](#1布局与框架)
- [2.表单设计规范](#2表单设计规范)
- [3.搜索与筛选规范](#3搜索与筛选规范)
- [4.交互与操作](#4交互与操作)
- [5.数据展示](#5数据展示)
- [6.文件处理规范](#6文件处理规范)
- [7.反馈与提示](#7反馈与提示)
- [8.权限控制](#8权限控制)
- [(二)移动端共性需求](#二移动端共性需求)
- [1.布局与导航](#1布局与导航)
- [2.表单设计规范](#2表单设计规范)
- [3.交互与操作](#3交互与操作)
- [4.数据展示](#4数据展示)
- [5.图片与文件处理](#5图片与文件处理)
- [6.反馈与提示](#6反馈与提示)
- [7.网络异常处理](#7网络异常处理)
- [(三)跨端通用规范](#三跨端通用规范)
- [1.品牌与视觉一致性](#1品牌与视觉一致性)
- [2.数据安全与隐私保护](#2数据安全与隐私保护)
- [3.字典与枚举规范](#3字典与枚举规范)
- [4.无障碍规范](#4无障碍规范)
- [五、功能需求详细设计](#五功能需求详细设计)
- [(一)系统管理](#一系统管理)
- [(二)健康评估](#二健康评估)
- [(三)应急就医](#三应急就医)
- [(四)专家咨询](#四专家咨询)
- [六、非功能性需求](#六非功能性需求)
- [(一)性能要求](#一性能要求)
- [(二)兼容性要求](#二兼容性要求)
- [七、技术实现方案](#七技术实现方案)
- [(一)项目技术架构设计](#一项目技术架构设计)
- [(二)数据库设计](#二数据库设计)
- [(三)接口设计](#三接口设计)
- [(四)系统安全与可用性设计](#四系统安全与可用性设计)
- [(五)系统部署与运维设计](#五系统部署与运维设计)
</div>
<div id="main-content">
# 一、文档概述
本文档为“健康长庆升级项目”的产品需求规格说明书(以下简称PRD),旨在系统性地定义本次升级的业务目标、功能范围、用户角色及非功能性需求,为项目团队(包括产品、设计、研发、测试等角色)提供统一的需求基线。该文档适用于“健康长庆升级项目”的全生命周期,涵盖需求分析、系统设计、原型设计、测试验证、交付验收各阶段。
### 文档目的
- 明确本次升级的核心目标与建设范围,确保各方对项目方向达成一致理解;
- 详细描述各功能模块的业务逻辑、交互流程及数据要求,作为设计与开发的直接依据;
- 规范验收标准与质量指标,为后续测试与上线交付提供参考。
### 读者对象
- 项目管理人员:用于把控项目范围、进度与风险;
- 产品与设计人员:作为功能设计与交互设计的输入依据;
- 研发人员:用于理解业务逻辑,进行技术方案设计与编码实现;
- 测试人员:用于编写测试用例,验证系统功能与非功能性指标;
- 客户方相关人员:用于确认需求内容,参与评审与验收。
### 文档内容
- 项目背景与目标:阐述“健康长庆升级项目”的建设背景、升级动因及总体目标,明确本次项目的价值定位;
- 项目需求分析:拆解面向平台管理员、油田员工、健康咨询专家、应急就医服务人员等用户群体用户画像分析,概述本次项目的具体交付成果,对核心业务板块做简要说明;
- 共性需求说明:针对交付产物(PC端或移动端)通用能力进行统一说明,包括用户权限管理、通用样式布局、操作一致性、数据字典等跨模块的公共需求;
- 功能需求详细设计:围绕系统管理、健康咨询、应急就医、健康体检、档案管理、健康监测、一线医疗点、健康干预、体重管理等模块,逐一描述业务场景、功能流程、界面要求及业务规则;
- 非功能性需求:明确系统在性能、安全、可用性、兼容性、可扩展性等方面的质量要求与量化指标;
- 技术实现方案:阐述系统整体技术架构、技术选型、数据库设计、第三方系统对接(如智能手表)、系统安全与可用性、系统部署与运维等方案。
# 二、项目背景与目标
## (一)项目背景
原健康长庆项目是长庆油田公司内部使用的综合性员工健康管理服务平台,旨在整合健康咨询、应急就医、健康体检、档案管理、健康监测、系统管理、健康干预、健康评估、一线医疗点及体重管理等多元功能,为油田员工提供全周期的健康保障。
随着系统业务数据量的不断攀升、企业健康管理要求的不断提升及人工智能技术的快速发展,现有系统在功能完整性、交互体验、数据处理效率及智能服务能力等方面已无法完全满足日益增长的业务需求。为此,启动本次“健康长庆升级项目”,通过对原有系统的全面升级,进一步提升平台的业务支撑能力与用户使用体验。
## (二)项目目标
本项目立足于油田员工健康管理的实际业务场景,围绕“性能提升、功能升级与优化、智能化改造”三大方向展开,具体目标如下:
* 性能提升:优化系统架构与数据处理能力,提升系统在高并发场景下的响应速度与稳定性,保障健康监测数据、体检数据等关键信息的高效流转与存储。
* 功能优化:重构平台功能规划、针对现有功能模块进行梳理与重构,优化用户操作流程,提升业务闭环的完整性与易用性,覆盖平台管理员、油田员工、健康咨询专家、应急就医服务人员等核心用户角色的使用场景。
* 智能化改造:引入智能化技术与数据分析能力,强化健康预警、风险识别、个性化干预等应用场景,提升平台在主动健康管理方面的服务能力,实现从“被动响应”向“主动守护”的升级。
## (三)交付标准
为确保项目成果符合预期,本次升级项目将依据以下标准进行验收与交付。所有交付内容需经客户方及我方项目主观部门双重确认。
### 性能与稳定性标准
* 并发处理能力:系统需支持高峰时段(如全员体检季、健康数据同步、全员体重管理高峰期)不低于 3000用户同时在线,核心业务接口响应时间控制在 3秒以内。
* 数据同步时效:健康监测(手表)数据从设备端到平台端的同步延迟不超过 5分钟,确保健康监测的实时性。
* 系统可用性:系统在试运行期间可用性达到 99.6% 以上,无重大级生产事故。
### 功能完整性标准
* 模块全覆盖:完成对健康咨询、应急就医、健康体检、档案管理、健康监测、系统管理、一线医疗点、健康干预、体重管理九大核心模块的升级改造,所有功能均需通过预设的测试用例。
* 业务流程闭环:关键业务流程(如应急就医从发起、调度到反馈的全过程;健康干预从风险评估到方案执行的全链路;员工档案从建立到速查)需实现端到端的业务闭环,无逻辑断点。
* 权限准确率:平台管理员、油田员工、健康咨询专家、应急就医服务人员四类角色的数据隔离与操作权限准确率达到100%。
### 智能化改造标准
* 主动预警能力:基于健康监测与体检数据通过数据中台和AI赋能,实现对异常指标(如心率、血压、血糖等)的自动识别与预警推送,预警准确率不低于 80%。
* 数据辅助决策:支持生成多维度的健康数据分析报告(如团队健康画像、高风险人群分布),为健康干预策略的制定提供数据支撑。
### 文档与技术资产交付
* 开发文档:提供完整的《系统详细设计说明书》《软件产品需求文档》《接口文档》(包含与智能手表、智能体重秤、环境监测IOT设备等第三方系统的对接规范)。
* 部署运维文档:提供《部署手册》《服务器环境清单》《系统监控与运维手册》及《常见问题处理指南》,确保系统具备可持续运维能力。
* 测试报告:提供功能测试报告、性能测试报告及安全测试报告,明确各项测试结论。
# 三、项目需求分析
## (一)用户画像分析
| 用户角色 | 角色定义 | 核心使用场景 | 核心诉求 | 当前痛点 |
| --- | --- | --- | --- | --- |
| 油田员工 | 长庆油田一线员工及管理人员,平台的最终受益者 | 查看健康档案、监测数据、发起健康咨询、申请应急就医、参与健康干预活动、记录体重管理 | 操作便捷、信息及时、响应迅速、隐私安全 | 原系统操作流程较为烦琐;健康数据呈现不够直观;应急就医入口不够显眼;缺乏主动健康提醒 |
| 平台管理员 | 油田健康管理部门的后台运营人员 | 用户管理、权限配置、数据统计、系统监控、体检安排、健康干预活动发布 | 管理效率高、数据可视化、权限控制灵活 | 后台数据统计维度单一;批量操作能力弱;干预活动发布流程复杂;缺乏数据导出与报表功能 |
| 健康咨询专家 | 提供健康咨询服务的专业医师/健康管理师 | 接收员工咨询、回复问题、查看咨询历史、辅助健康评估 | 咨询界面清晰、历史记录可追溯、辅助工具完善 | 咨询消息通知不及时;无法快速查看咨询用户的健康档案;缺乏常用回复模板 |
| 应急就医服务人员 | 负责应急就医调度与协调的后台人员 | 接收应急就医申请、核实信息、协调资源、跟踪处理进度、反馈结果 | 响应高效、信息完整、流程闭环、异常可预警 | 应急流程缺乏自动化提醒;处理进度跟踪困难;与一线医疗点信息联动不足 |
| 医疗点工作人员 | 长庆油田各一线医疗点的医护人员,负责为油田员工提供现场医疗服务 | 根据员工的健康状况、记录员工现场就诊信息、周期性就行慢病随访、体检报告解读等 | 信息同步及时、操作简便、与上级机构协同顺畅 | 平台响应效率慢;就诊记录与平台数据同步困难,重复录入工作量大 |
## (二)项目交付成果分析
| 交付成果 | 终端定位 | 使用人群 | 核心功能要点 |
| --- | --- | --- | --- |
| 健康长庆平台(Web管理端) | 平台统一管理后台,支撑全系统配置与数据管理 | 平台管理员、健康管理部门人员 | 用户权限管理、组织架构维护、数据统计报表、体检安排、健康干预活动发布、应急就医调度监控、系统配置与日志审计 |
| 员工端Android版APP | 油田员工日常健康管理移动端入口(安卓设备) | 油田员工 | 健康档案查看、健康监测数据(手表)展示、发起健康咨询、申请应急就医、参与健康干预、体重管理、消息通知接收 |
| 员工端iOS版APP | 油田员工日常健康管理移动端入口(苹果设备) | 油田员工 | 功能与Android版一致,覆盖健康档案、健康监测、健康咨询、应急就医、健康干预、体重管理等核心模块 |
| 健康应急Android版APP | 应急场景专用移动端,服务于咨询专家、应急人群及咨询助手(安卓设备) | 健康咨询专家、应急就医服务人员、咨询小助手 | 咨询专家:接收咨询、回复问题、查看用户健康档案、快捷回复模板;应急人群:应急申请进度跟踪、资源调度查看;咨询小助手:辅助答疑、常见问题推送、数据统计 |
| 健康应急iOS版APP | 应急场景专用移动端,服务于咨询专家、应急人群及咨询助手(苹果设备) | 健康咨询专家、应急就医服务人员、咨询小助手 | 咨询专家:接收咨询、回复问题、查看用户健康档案、快捷回复模板;应急人群:应急申请进度跟踪、资源调度查看;咨询小助手:辅助答疑、常见问题推送、数据统计 |
| 应急大屏 | 应急指挥可视化看板,用于实时监控应急事件与资源调度 | 应急就医服务人员、平台管理员、应急指挥人员 | 应急事件实时地图展示、应急资源分布(医疗点、车辆)、事件处理进度追踪、异常事件预警、数据大屏可视化 |
| 医疗点pad端 | 一线医疗点专用移动终端,用于现场医疗服务与应急协同 | 一线医疗点工作人员 | 应急就医通知接收、现场就诊记录录入、医疗点资源(药品、设备)状态更新、与应急调度中心协同、员工健康档案快速查询 |
| 微信小程序 | 轻量化服务入口,满足员工便捷访问需求 | 油田员工 | 膳食营养监控、健康档案快速查看、健康监测数据摘要、消息通知、体重管理 |
| 服务监控平台 | 系统运行状态监控与运维管理后台,保障平台稳定运行 | 系统运维人员、技术支撑人员 | 服务健康状态监控、接口调用链路追踪、异常告警与通知、日志聚合分析、性能指标监控、资源使用情况监控、告警规则配置、运维报表统计 |
| 体检医生工作台 | 油田员工体检医院医生端工作台 | 医院体检医生 | 用于油田员工体检时医院医生进行信息核查和数据录入 |
## (三)核心业务分析
| 核心业务 | 业务概述 | 现状问题 | 升级方向 |
| --- | --- | --- | --- |
| 系统管理 | 用户管理、角色权限、组织架构、系统配置、数据字典,保障平台安全稳定运行 | 权限颗粒度粗、日志不完善、无历史版本留存与操作记录、配置分散 | RBAC精细化权限、完整操作、统一配置管理、结合数据中台构建数据档案 |
| 健康评估 | 基于交大健康干预算法模型、中新惠尔风险与心理评估模型生成健康风险评估报告,识别风险等级 | 评估模型单一、报告样式单一、结果准确度不高、缺少系统自动评估机制和风险预警机制 | 借助AI平台优化评估模型、可视化报告、实现评估结果自动预警和主动风险预测 |
| 应急就医 | 应急申请、资源调度、进度跟踪、结果反馈的全流程管理。 | 应急业务并未形成完整闭环,应急过程以人为干预为主缺少智能的应急指导 | 完善业务流程闭环、就近资源自动匹配、超时升级提醒 |
| 专家咨询 | 员工在线健康咨询,连接专家,支持图文咨询与视频咨询 | 消息回复时效差、无有效的消息提醒机制,平台咨询助手强依赖于人工介入,智能化程度低 | 实时推送消息提醒、增加智能咨询助手 |
| 健康体检 | 体检预约、报告管理、历年数据对比,支撑健康管理基础数据 | 预约流程烦琐、部分报告上传依赖人工处理、数据量庞大导致业务性能表现差 | 优化体检预约流程、打通数据自动上传通道、历年趋势可视化、数据自动同步档案 |
| 健康监测 | 智能手表采集心率、血压、血氧、睡眠等数据,实现动态跟踪 | 同步延迟高、仅展示无分析、缺异常告警 | 优化同步延迟、趋势图可视化、借助AI实现异常自动识别告警、扩展设备兼容性 |
| 一线医疗 | 管理一线员工健康档案、开展慢病管理、随访管理、健康宣教、日常诊疗等医疗服务 | 数据权限管理混乱、信息更新滞后、部分业务性能表现差、大部分业务依赖人工介入智能化程度低 | 信息实时维护更新、优化数据权限以及业务响应速度、通过大数据+AI赋能提高诊疗业务中的智能化程度 |
| 营养管理 | 员工日常用餐记录、膳食指导、营养分析、饮食建议、风险预测助力健康生活方式 | 系统故障频发、功能单一、数据实时性差、与健康数据关联弱 | 提升系统功能卡永兴、完善个性化膳食建议、营养数据分析业务、打通与健康数据联动的壁垒 |
| 体重管理 | 体重记录、目标设定、进度追踪、激励反馈,科学管理体重 | 仅支持记录、缺目标激励、智能化引导和建议较弱 | 目标设定与进度追踪、趋势图表、因人而异提供更科学的减重建议和指导意见 |
| 知识普及 | 健康资讯、科普文章、视频课程,提升健康素养 | 更新慢、缺分类搜索、形式单一、阅读数据未利用 | 多形式内容、分类搜索推荐、点赞评论互动、阅读行为记录、优化智能推荐体系 |
| 运动管理 | 运动数据记录、目标设定、运动计划推荐,促进规律运动 | 数据来源单一、缺目标计划、与健康模块联动弱 | 手动录入补充、目标设定与进度、个性化运动推荐、联动体重营养 |
| 心脑血管病预防 | 高风险人群识别、预警推送、干预方案、科普教育专项管理 | 评估准确性较差、评估与干预脱节、缺少高风险预警措施 | 自动识别高风险人群、风险预警推送、专项干预方案、科普专区 |
| 糖尿病预防 | 血糖监测、风险预警、饮食运动指导,服务高风险及前期人群 | 评估准确性较差、血糖监控难落实、建议缺乏针对性、管理分散 | 血糖记录与趋势分析、自动识别高风险、个性化饮食运动建议、专项干预计划 |
| 癌症预防 | 常见癌症科普、早期筛查提醒、风险评估与预防建议 | 内容覆盖不全、缺评估工具、筛查提醒缺失 | 科普知识库、风险评估问卷、筛查提醒推送、筛查结果入档 |
| 健康数据 | 汇聚系统中所有健康数据,提供可视化分析、报表导出、统计能力 | 数据分散、统计维度单一、导出功能弱 | 统一数据中心、多维度可视化分析、灵活报表导出 |
| 健康档案 | 统一归集体检、监测、咨询、干预等数据,形成个人健康时间轴 | 个别业务未留存历史数据、数据分散未统一视图、数据资产管理方式混乱、数据统计与导出不完善 | 补全历史数据留存环节、实现多源数据整合统一视图、提供灵活的数据统计与导出方案 |
| 待办通知 | 待处理事项、系统消息、预警推送、活动提醒,保障流程推进 | 通知方式单一、缺强提醒、待办分散 | 统一待办中心、多渠道推送(App/短信/微信)、消息分类状态管理、超时升级提醒 |
# 四、共性需求说明
本章节对平台各端(PC端、移动端)的通用交互规范、数据展示规则、权限控制及跨端一致性要求进行统一定义,所有功能模块的设计与开发须遵循本章节规范,不得与之冲突。
## (一)PC端共性需求
### 1.布局与框架
- 整体采用顶部导航&左侧导航栏 + 右侧内容区的经典后台布局,内容区支持自适应拉伸
- 无权限的菜单项、按钮直接隐藏
- 导入功能使用统一弹窗布局,支持模板下载、拖拽上传、文件预览
- 列表分页默认每页 10 条,支持切换每页条数(10 / 20 / 50/100
- 列表表格支持列宽拖动、根据每列内容合理规划列宽(如手机号身份证号等长度固定的字段)、列显隐配置,配置状态本地持久化
- 超长文本单行截断显示"...",鼠标悬浮展示完整内容(Tooltip)
- 无数据、加载中、接口异常、空白页使用统一样式组件
- 详情页与编辑页排版结构与新增页保持一致
### 2.表单设计规范
- 必填字段在标签前标注红色星号(*),选填字段不做标注
- 字段标签统一左对齐,同一表单内标签宽度保持一致
- 校验时机:输入框失焦时触发单字段校验,提交时触发全量校验
- 只读态字段使用纯文本展示,不使用禁用输入框
- 下拉选项超过 10 条时,支持关键词搜索过滤
- 日期/时间选择统一使用系统日期组件,不允许纯手动输入
- 表单提交后按钮进入 loading 状态,防止重复提交
- 表单离开未保存时,弹出"是否放弃修改?"二次确认弹窗
### 3.搜索与筛选规范
- 搜索框支持回车键触发搜索,与点击搜索按钮效果一致
- 多条件筛选区域默认展开,条件较多时可折叠收起
- 已选筛选条件以标签形式展示在筛选区下方,支持单独点击删除
- 筛选条件变更后需手动点击"查询"按钮触发,不自动触发(避免频繁请求)
- 提供"重置"按钮,一键清空所有筛选条件并恢复默认列表
- 列表页搜索/筛选状态不跨页面保留,刷新后恢复默认
### 4.交互与操作
- 删除、清空、重置、批量操作前必须弹出二次确认弹窗,明确说明操作影响
- 批量操作按钮在未选中数据时置灰禁用,选中后激活并显示已选数量
- 所有按钮做防连点控制,同一接口短时间内不可重复触发
- 接口请求期间按钮进入 loading 状态,内容区显示骨架屏或 loading 遮罩
- 导出规范:小数据量(≤1000条)即时导出;大数据量(>1000条)异步导出,生成任务后提示"请在导出任务中查看";按钮文案统一为"导出列表数据",导出成功后 Toast 提示
- 所有字段校验采用前端校验 + 后端校验双重保障
### 5.数据展示
- 时间展示统一格式:YYYY-MM-DD HH:mm:ss;日期类型统一格式:YYYY-MM-DD
- 金额类型保留两位小数;百分比保留一位小数
- 浮点型数据无特别说明时默认保留两位小数
- 手机号、固定电话、身份证号使用统一正则验证规则
- 表格中数字列右对齐,文本列左对齐
- 空值/无数据字段统一展示为"—",不展示空字符串或 null
### 6.文件处理规范
- 上传入口处明确标注支持的文件类型与大小上限
- 图片文件支持在线预览;PDF、Word、Excel 支持在线预览(不强制下载)
- 大文件上传(>5MB)显示上传进度条,支持取消上传
- 下载文件统一通过浏览器下载,文件命名格式:模块名_数据名_时间戳
- 批量下载超过 10 个文件时,自动打包为 ZIP 后下载
### 7.反馈与提示
- 操作成功/失败:使用顶部居中 Toast 提示,3 秒后自动消失
- 关键操作(删除、提交、发布):使用 Confirm 确认框,需用户主动确认
- 接口报错:统一提示"系统繁忙,请稍后重试",详细错误信息仅输出至控制台
- 网络断开:全局顶部展示"网络连接已断开"横幅提示,恢复后自动消失
- 操作日志:新增、修改、删除、导出等关键操作自动记录操作日志,支持审计追溯
### 8.权限控制
- 应用权限:根据登录用户角色动态显示主页应用模块,无权限模块不可见
- 菜单权限:根据用户角色动态渲染菜单,无权限菜单项直接隐藏
- 按钮权限:页面内操作按钮根据权限控制显示/隐藏或启用/禁用
- 数据权限:所有业务数据默认按组织架构层级与登录用户管理范围进行隔离,禁止数据越权访问(如员工健康数据、单位活动、监测设备信息等)
- 接口权限:核心业务接口必须加权限校验,普通员工 Token 无法访问后台统计接口,管理员 Token 无法访问 APP 专用接口
## (二)移动端共性需求
### 1.布局与导航
- 整体采用单列布局,内容区上下滑动
- 员工端 APP、健康应急 APP 使用底部 Tab 栏导航,Tab 不超过 5 个,当前选中项高亮
- 顶部导航栏显示页面标题,左侧返回按钮(一级页面无返回),右侧可放置更多操作入口
- 适配主流手机分辨率(iOSiPhone 12 及以上;Android:主流全面屏),支持安全区域适配
- 支持左滑返回上一页;列表支持下拉刷新、上拉加载更多
### 2.表单设计规范
- 表单采用单列纵向排列,标签置于输入框上方
- 必填字段标注红色星号(*
- 输入框获取焦点时键盘自动弹起,内容区自动上移,避免键盘遮挡输入框
- 点击空白区域收起键盘
- 选择类控件使用底部弹出 Picker 或 ActionSheet,不使用 PC 式下拉框
- 长表单建议分步骤展示,每步不超过 5 个字段,顶部显示步骤进度条
- 表单提交后按钮进入 loading 状态,防止重复提交
### 3.交互与操作
- 可点击区域不小于 44×44pt,确保手指触控友好
- 主按钮使用圆角大按钮,宽度占满或居中,置于页面底部悬浮或跟随内容流
- 删除、清空等危险操作使用底部弹出 ActionSheet 或居中模态框,需二次确认
- 键盘弹起时底部悬浮按钮自动上移,避免被键盘遮挡
- 列表支持下拉刷新,刷新时顶部显示 loading 动画
### 4.数据展示
- 图表采用移动端适配的简化版本,支持手势缩放查看详情
- 无数据时显示统一占位图 + 提示文案,提供引导操作
- 空值统一展示为"—"
### 5.图片与文件处理
- 图片上传支持拍照和相册选取两种方式,入口明确区分
- 上传前自动压缩,单张图片大小不超过 2MB
- 图片预览支持双指缩放、左右滑动切换多张图片
- 文件下载显示进度,完成后提示"下载完成,点击打开"
- 上传失败时提示具体原因(如"文件格式不支持"、"文件超出大小限制"
### 6.反馈与提示
- 操作反馈使用 Toast 轻提示,1.5~2 秒自动消失,不阻断操作
- 异步请求使用全屏或局部 Loading,避免用户重复点击
- 待办事项、未读消息在入口处显示红点或数字角标
- 关键操作(如问卷提交)成功后,给予明确的成功状态页,而非仅 Toast
### 7.网络异常处理
- 请求超时时间统一设置为 10 秒,超时后提示"服务器繁忙,请稍后在再试"并提供重试按钮
- 网络断开时,页面顶部展示"当前无网络连接"横幅,恢复后自动消失并刷新数据
- 弱网环境下优先展示缓存数据,并标注"网络不佳数据可能存在延迟"
- 关键操作(如应急就医发起)在网络恢复后自动重试,并通知用户操作结果
- 首屏关键数据优先加载,非关键内容懒加载;图片使用缩略图 + 懒加载策略
## (三)跨端通用规范
### 1.品牌与视觉一致性
- PC 端与移动端采用统一的品牌色、字体、圆角、阴影等视觉语言
- 全平台功能命名、按钮文案、提示文案保持一致(如"应急就医"不出现"紧急就医"等变体)
- 时间显示格式统一:近 24 小时内显示"X 小时前",超过 24 小时显示具体日期
- 错误提示文案统一维护,避免不同模块出现不同表述
- 同一用户在不同端操作,数据实时同步,无延迟感知
### 2.数据安全与隐私保护
- 所有数据传输使用 HTTPS 加密,禁止明文传输敏感信息
- 敏感字段(手机号、身份证号、健康指标数据)在列表中默认脱敏,详情页按权限展示完整信息
- 会话超时时间设置为 30 分钟无操作后自动登出,跳转登录页并提示"登录已过期,请重新登录"
- 移动端禁止对健康敏感数据页面进行截图分享(系统级限制,如条件不允许则在界面层提示禁止截图)
- 用户登出后本地缓存的敏感数据立即清除
### 3.字典与枚举规范
- 所有状态值、类型值统一在后台数据字典中维护,前端不硬编码枚举含义
- 前后端枚举值保持严格一致,接口文档中明确枚举定义
- 字典数据由后端统一下发,前端缓存使用;字典变更时主动通知前端刷新缓存
- 列表筛选、表单选项中的枚举值均从字典接口动态获取
### 4.无障碍规范
- 正文字体最小不低于 12px(PC),移动端正文不低于 14px
- 文字与背景色对比度不低于 4.5:1(WCAG AA 标准)
- 所有图标按钮需配备文字说明或 aria-label 属性
- 表单字段需关联 label 标签,支持屏幕阅读器识别
- 关键操作结果(成功/失败)需通过文字或图标明确告知,不能仅依赖颜色区分
# 五、功能需求详细设计
## (一)系统管理
健康长庆升级项目产品需求文档-系统管理.docx
## (二)健康评估
健康长庆升级项目产品需求文档-健康评估.docx
## (三)应急就医
## (四)专家咨询
# 六、非功能性需求
## (一)性能要求
### 业务指标
| 指标 | 定义 | 简称 | 本系统标准 |
| --- | --- | --- | --- |
| 请求响应时间 | 指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响应结束,整个过程所耗费的时间。 | Response Time: RT | 平均响应在1s以下;<br>统计查询接口3s以下 |
| 系统处理能力 | 指系统在利用系统硬件平台和软件平台进行信息处理的能力。<br>系统处理能力通过系统每秒钟能够处理的交易数量来评价。 | HPS(每秒点击次数)<br>TPS(系统每秒处理交易数)<br>QPS(系统每秒处理查询次数) | HPS10000次/秒<br>TPS3000笔/秒<br>QPS5000次/秒 |
| 并发用户数 | 指在同一时刻内,登录系统并进行业务操作的用户数量。在测试中,采用虚拟用户来模拟现实中用户进行业务操作。 | Virtual User: VU | 正常负载下支持用户并发数3000+ |
| 错误率 | 指系统在负载情况下,失败交易的概率。 | Failure Ratio: FR | 错误率低于千分之六,即成功率高于99.6% |
### 服务器资源指标
| 指标 | 定义 | 简称 | 本系统标准 |
| --- | --- | --- | --- |
| CPU | 中央处理器是一块超大规模的集成电路,是一台计算机的运算核心(Core)和控制核心( Control Unit)。它的功能主要是解释计算机指令以及处理计算机软件中的数据。 | Central Processing UnitCPU | CPU sys%小于30%<br>CPU wait%小于5%。<br>CPU Load小于CPU 核数。 |
| 内存 | 内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。 | Memory就是内存的简称 | 本系统据其优秀的系统架构设计,合理的运用内存,在一般情况下不会利用到SWAP空间,自身的内存完全能够支撑项目的运行 |
| 磁盘吞吐量 | 指在无磁盘故障的情况下单位时间内通过磁盘的数据量。 | Disk Throughput | 在分布式架构下,一般磁盘繁忙率要低于70% |
| 网络吞吐量 | 指在无网络故障的情况下单位时间内通过的网络的数据数量。单位为Byte/s。用于衡量系统对于网络设备或链路传输能力的需求。 | Network Throughput | 网络吞吐量指标主要有每秒有多少兆流量进出,一般情况下不能超过设备或链路最大传输能力的70% |
### 前端应用指标
| 指标 | 二级指标 | 解释 | 本系统指标 |
| --- | --- | --- | --- |
| 页面展示 | 首次显示时间 | 在浏览器地址栏输入URL按回车到用户看到网页的第一个视觉标志为止 | 秒级别响应 |
| | OnLoad事件时间 | 浏览器触发onLoad事件的时间,当原始文档和所有引用的内容完全下载后才会触发这个事件 | 秒级别响应 |
| | 完全载入的时间 | 所有onLoad JavaScript 处理程序执行完毕,所有动态的或延迟加载的内容都通过这些处理程序触发的时间 | 秒级别响应 |
| 页面数量 | 页面大小 | 整个页面大小 | 所有页面Body大小为KB级别 |
| | 请求数量 | 从网站下载资源时所有网络请求的总数,尽量少 | |
| 网络所花时间 | DNS时间 | DNS查找时间 | 秒级别响应 |
| | 连接时间 | 浏览器与Web服务器建立TCP/IP连接的时间 | 秒级别响应 |
| | 服务器时间 | 服务器处理时间 | 秒级别响应 |
| | 传输时间 | 内容传输所用时间 | 秒级别响应 |
| | 等待时间 | 等待某个资源释放的时间 | 秒级处理 |
### 稳定性、可靠性指标
| 指标 | 解释 | 本系统标准 |
| --- | --- | --- |
| 稳定性指标 | 系统按照最大容量的80%或标准压力(系统的预期日常压力)情况下运行,能够稳定运行的最短时间 | 本系统能提供7*24运行的系统稳定运行能力。 |
| 集群 | 集群中某个节点出现故障时,系统是否有业务中断情况出现<br>在集群中新增一个节点时,是否需要重启系统<br>当故障节点恢复后,加入集群,是否需要重启系统<br>当故障节点恢复后,加入集群,系统是否有业务中断情况出现<br>节点切换需要多长时间 | 当系统出现故障时,故障节点会自动从集群中剔除,业务无需中断,当节点恢复时自动加入集群,实现无感知切换,纳秒级切换。 |
| 备份和恢复 | 备份是否成功及其消耗时间<br>备份是否使用脚本自动化完成<br>恢复是否成功及其消耗时间<br>恢复是否使用脚本自动化完成 | 备份策略采用全量和增量的方式进行,以小时为单位,周期性循环备份,并以天或周为单位周期性循环实现全量备份,为数据提供基础底层保障。 |
## (二)兼容性要求
| 应用名称 | 系统性质 | 操作系统/浏览器内核兼容性要求 | 操作系统版本/browser版本要求 | 终端适配要求 | 备注 |
| --- | --- | --- | --- | --- | --- |
| 客户端APP | 移动APP | 1. 需同时兼容 Android、iOS、HarmonyOS 三类主流手机操作系统。<br>2. 需进行充分兼容性测试并输出测试报告。 | Android 系统:需兼容 Android 10.0 及以上版本,推荐 Android 12 及以上。 | 1. 机型适配:需兼容市面主流屏幕类型(全面屏、打孔屏、折叠屏等)。<br>2. 屏幕分辨率:1080×2400 及以上,支持安全区域适配。<br>3. 运行内存:4GB 及以上。 | 1. 测试需覆盖所有功能点。<br>2. 品牌、系统、分辨率兼容性测试可使用第三方测评平台,需出具测试报告。 |
| | | | iOS 系统:需兼容 iOS 16 及以上版本,推荐 iOS 17 及以上。 | 1. 适配 iPhone 12 及以上机型。<br>2. 屏幕分辨率:1170×2532 及以上。<br>3. 支持 Face ID 安全区域适配。 | |
| | | | HarmonyOS 系统:需兼容 HarmonyOS 4.0 及以上版本,推荐 HarmonyOS 6.0。 | 1. 适配华为、荣耀品牌近 3 年内主流机型。<br>2. 屏幕分辨率:1080×2340 及以上。<br>3. 运行内存:4GB 及以上。 | |
| 客户端WEB | B/S应用 | 1. 主要适配内核:BlinkChromium)、Gecko、WebKit。<br>2. 不再支持 IE 浏览器(Trident 内核已废弃)。<br>3. 需适配 PC 端主流浏览器。 | Chromium 内核:Chrome 100+Edge 100+360极速浏览器(Chromium内核)13+;QQ浏览器 14+;搜狗浏览器 12+Opera 85+。 | 1. 显示器分辨率:最低 1366×768,推荐 1920×1080 及以上,支持 2K2560×1440)。<br>2. 显示器尺寸:13in34in。 | 1. 推荐使用 Chrome 浏览器以获得最佳体验。<br>2. 不再支持 IE 浏览器,访问时提示升级。<br>3. 兼容性测试以浏览器版本为主,需同时注明内核版本。<br>4. 终端适配以分辨率为准,显示器尺寸为参考值。 |
| | | | Gecko 内核:Firefox 100+。<br>WebKit 内核:Safari 16+(仅适用于 macOS 用户)。<br>企业微信内置浏览器(Chromium 内核):需兼容企业微信 PC 端及移动端内置浏览器,版本跟随企业微信最新版本。 | | |
# 七、技术实现方案
## (一)项目技术架构设计
## (二)数据库设计
## (三)接口设计
## (四)系统安全与可用性设计
## (五)系统部署与运维设计
</div>
+45 -1
View File
@@ -12,6 +12,7 @@
```
prototype-user-app/
├── index.html # 全局导航页(项目入口)
├── web-admin/ # Web 管理端原型
├── employee-app/ # 员工端 APP 原型
├── emergency-app/ # 健康应急 APP 原型
@@ -23,7 +24,18 @@ prototype-user-app/
└── README.md # 项目说明
```
### 1.2 各端内部按核心功能模块分目录
### 1.2 全局导航页(index.html
根目录 `index.html` 作为项目统一入口,部署到服务器后供研发人员浏览所有原型页面。
- 通过 Tab 切换三大终端(Web 管理端 / 员工端 APP / 健康应急 APP
- 以模块卡片展示各业务模块,点击卡片跳转至该模块主页(index.html)
- 点击卡片右上角页数徽章,悬浮展示该模块所有页面和弹窗的跳转链接
- 已有原型的模块卡片带绿色边框呼吸灯动画,待开发模块为虚线灰色卡片
- 数据驱动:所有模块和页面信息维护在 `SITE_MAP` JS 对象中
- **新增原型页面时,必须同步在 `index.html``SITE_MAP` 中添加对应条目**
### 1.3 各端内部按核心功能模块分目录
**员工端 APPemployee-app/17 个模块**
```
@@ -344,3 +356,35 @@ window.location.href = '../health-consult/consult-list.html';
| 应急就医 | 应急申请、资源调度、进度跟踪的全流程管理 | 员工端 APP / 健康应急 APP / Web 管理端 |
| 一线医疗 | 一线医疗点服务与管理 | 员工端 APP / Web 管理端 |
| AI | 智能健康助手,AI问答与健康建议 | 员工端 APP |
---
## 七、模块文档规范
### 7.1 文档要求
每个一级功能模块目录下必须创建 `doc/` 子目录,维护以下两个文档:
| 文件 | 用途 | 更新时机 |
|------|------|----------|
| `doc/CHANGELOG.md` | 变更记录 | 每次提交代码前自动整理并追加 |
| `doc/PRD.md` | 产品需求文档 | 每次新增/修改原型页面后同步更新 |
### 7.2 CHANGELOG.md 格式规范
- 按时间倒序排列,最新变更在最前
- 每条记录格式:`## [YYYY-MM-DD] type: 变更摘要`
- type 取值:`init`(初始化)、`feat`(新增)、`fix`(修复)、`refactor`(重构)
- 列出涉及的文件及具体变更说明
### 7.3 PRD.md 编写规范
面向研发人员的产品需求文档,要求简洁、条理清晰,包含以下结构:
1. **模块概述**:一句话说明模块功能 + 页面导航结构树 + 状态流转图(Mermaid)
2. **页面清单**:表格列出所有页面和弹窗
3. **逐页说明**:每个页面/弹窗包含三个维度
- **功能说明**:页面用途和核心 UI 元素
- **业务规则**:数据逻辑、状态约束、边界条件
- **交互说明**:以文字列表形式描述各元素的交互行为和跳转目标,跳转目标使用中文页面名称(如"跳转到专家主页"),不使用文件名
4. **表单字段表**:含表单提交的页面,以表格列出字段名、类型、是否必填、校验规则