feat: 新增模块文档规范体系及导航页PRD文档入口

- 新增 doc/CHANGELOG.md 和 doc/PRD.md 模块文档规范,更新项目背景和CLAUDE.md约束
- 导航页新增"产品需求文档"Tab,展示各模块PRD编写状态
- 模块卡片左下角新增PRD文档快捷链接
- 新增 md-viewer.html 用于Markdown文件预览,支持HTML混合内容iframe隔离渲染
- 健康咨询模块完成首份PRD文档和变更记录

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
冯普
2026-04-09 17:41:45 +08:00
co-authored by Claude Opus 4.6
parent fd39307aaf
commit 82f811c1fc
7 changed files with 947 additions and 2 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
**功能说明**
弹窗形式的评价功能,底部滑出样式。与评价医生页功能相同,展示形式不同。
**业务规则**
- 必须选择星级才能提交,否则弹出提示
- 文字评价选填
**交互说明**
- 点击「取消」关闭弹窗
- 点击「提交评价」校验评分 → 提交 → 关闭弹窗
+128 -2
View File
@@ -105,6 +105,37 @@ body { background: var(--background); font-family: -apple-system, BlinkMacSystem
.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>
@@ -127,6 +158,8 @@ const ICONS = {
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:'🔗' }
};
@@ -161,7 +194,7 @@ const SITE_MAP = [
{ name:'心脑血管病预防', dir:'employee-app/cardiovascular', pages:[] },
{ name:'糖尿病预防', dir:'employee-app/diabetes', pages:[] },
{ name:'癌症预防', dir:'employee-app/cancer', pages:[] },
{ name:'健康咨询', dir:'employee-app/health-consult', 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:'视频预约' },
@@ -251,9 +284,13 @@ function countPages(t) {
/* 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;
@@ -277,7 +314,12 @@ function cardHTML(mod, uid) {
}
const body = has ? '' : `<div class="module-card__empty-hint">${ICONS.empty}暂无原型页面</div>`;
return `<div class="${cls}" ${onclick}><div class="module-card__header"><span class="module-card__name">${mod.name}</span>${tagHtml}</div>${body}</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) {
@@ -338,6 +380,90 @@ tabNav.addEventListener('click', e => {
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>
+151
View File
@@ -0,0 +1,151 @@
# 健康长庆系统升级改造项目报价单
**日期:** 2026年4月9日
---
## 一、项目概况
本项目基于"解构-迁移模式(Demolition Pattern",采用"逐步解构 + 数据迁移 + 停机切换"实施路线,预计总工期 **12 个月**,完成健康长庆系统从"分布式单体"架构向 Java 21 + Spring Boot 3 + Docker 微服务架构的全面升级改造。
---
## 二、报价汇总
| 序号 | 模块编号 | 模块名称 | 费用(万元) |
|:----:|:--------:|:--------:|:----------:|
| **一** | **第一阶段** | **基础建设与顶层设计** | |
| 1 | M1 | 基础设施与容器化底座搭建 | 3.60 |
| 2 | M2 | 领域驱动设计与技术规范确立 | 3.60 |
| **小计** | | | **7.20** |
| **二** | **第二阶段** | **健康管理后台系统** | |
| 3 | M3 | 系统管理 | 6.12 |
| 4 | M4 | 健康体检 | 25.02 |
| 5 | M5 | 体重管理 | 9.54 |
| 6 | M6 | 营养管理 | 7.74 |
| 7 | M7 | 运动管理 | 5.76 |
| 8 | M8 | 健康监测 | 19.08 |
| 9 | M9 | 健康评估 | 8.10 |
| 10 | M10 | 知识普及 | 15.84 |
| 11 | M11 | 健康数据 | 12.60 |
| 12 | M12 | 健康档案 | 8.28 |
| 13 | M13 | 心脑血管病预防 | 11.88 |
| 14 | M14 | 糖尿病预防 | 9.36 |
| 15 | M15 | 癌症预防 | 9.00 |
| 16 | M16 | 专家咨询 | 10.80 |
| 17 | M17 | 应急就医 | 9.90 |
| 18 | M18 | 一线医疗 | 15.66 |
| 19 | M19 | 待办(异常数据)通知 | 0.36 |
| **小计** | | | **185.04** |
| **三** | **第二阶段** | **健康长庆APP** | |
| 20 | M20 | AI | 1.80 |
| 21 | M21 | 应用 | 4.14 |
| 22 | M22 | 健康体检 | 13.32 |
| 23 | M23 | 体重管理 | 8.28 |
| 24 | M24 | 营养管理 | 4.14 |
| 25 | M25 | 运动管理 | 5.22 |
| 26 | M26 | 健康监测 | 6.48 |
| 27 | M27 | 健康评估 | 8.28 |
| 28 | M28 | 心脑血管病预防 | 10.08 |
| 29 | M29 | 糖尿病预防 | 6.84 |
| 30 | M30 | 癌症预防 | 6.30 |
| 31 | M31 | 健康咨询 | 6.48 |
| 32 | M32 | 应急就医 | 4.14 |
| 33 | M33 | 一线医疗 | 5.40 |
| 34 | M34 | 健康档案 | 12.42 |
| 35 | M35 | 知识普及 | 8.64 |
| 36 | M36 | 个人中心 | 3.60 |
| **小计** | | | **115.56** |
| **四** | **第二阶段** | **健康应急APP、医护人员PAD、应急大屏、微信小程序** | |
| 37 | M37 | 健康应急APP | 27.90 |
| 38 | M38 | 医护人员PAD | 5.04 |
| 39 | M39 | 应急大屏 | 10.08 |
| 40 | M40-M43 | 微信小程序 | 23.22 |
| **小计** | | | **66.24** |
| **五** | **第三阶段** | **压测与集成** | |
| 41 | M44-M45 | 集成测试 | 10.80 |
| **小计** | | | **10.80** |
| **六** | **第四阶段** | **生产割接与保障期** | |
| 42 | M46 | 生产割接与保障期 | 1.80 |
| **小计** | | | **1.80** |
---
## 三、费用汇总
| 项目 | 金额(万元) | 费用占比 |
|:----:|:----------:|:--------:|
| 第一阶段:基础建设与顶层设计 | 7.20 | 1.86% |
| 第二阶段:健康管理后台系统(M3-M19 | 185.04 | 47.86% |
| 第二阶段:健康长庆APPM20-M36 | 115.56 | 29.89% |
| 第二阶段:健康应急APP/PAD/大屏/小程序(M37-M43 | 66.24 | 17.13% |
| 第三阶段:压测与集成(M44-M45 | 10.80 | 2.79% |
| 第四阶段:生产割接与保障期(M46 | 1.80 | 0.47% |
| **合计** | **386.64** | **100.00%** |
---
## 四、报价说明
### 4.1 技术栈
| 类别 | 技术选型 |
|:----:|:--------:|
| 后端框架 | Java 21 + Spring Boot 3 |
| 容器化 | Docker / KubernetesK8s |
| 消息队列 | RocketMQ 5.x |
| 缓存 | Redis 7 Cluster |
| 数据库 | PostgreSQL |
| 搜索引擎 | Elasticsearch |
| CI/CD | Jenkins + GitLab + Harbor |
| 监控 | Prometheus + Grafana + EFK |
| 链路追踪 | OpenTelemetry + Zipkin |
| 注册中心 | Nacos 2.x |
| 前端框架 | Element Plus |
### 4.2 硬件资源需求(供参考,不含在报价内)
| 序号 | 服务器用途 | 区域 | CPU(核) | 内存(G) | 存储(G) |
|:----:|:----------:|:----:|:---------:|:---------:|:---------:|
| 1 | 微服务集群节点1 | DMZ | 32 | 128 | 1,024 |
| 2 | 微服务集群节点2 | DMZ | 32 | 64 | 1,024 |
| 3 | 微服务集群节点3 | DMZ | 32 | 64 | 1,024 |
| 4 | 注册中心(Nacos等) | DMZ | 16 | 64 | 1,024 |
| 5 | 消息与缓存(Redis/RocketMQ | 办公区 | 16 | 64 | 1,024 |
| 6 | 搜索存储(ES/MongoDB | 办公区 | 16 | 64 | 2,048 |
| 7 | 数据服务器(PostgreSQL | 办公区 | 16 | 32 | 8,192 |
| 8 | 数据备份服务器 | 办公区 | 16 | 32 | 8,192 |
| 9 | 文件存储服务器 | 办公区 | 16 | 64 | 8,192 |
| 10 | DevOps自动运维平台 | 办公区 | 16 | 32 | 1,024 |
| 11 | 监控平台(Prometheus等) | 办公区 | 16 | 32 | 2,048 |
| **合计** | | | **224** | **640** | **34,816** |
### 4.3 项目里程碑计划
| 阶段 | 时间节点 | 主要交付物 |
|:----:|:--------:|:----------:|
| 第一阶段 | 第 1 ~ 2 月 | 基础设施搭建、技术规范文档、领域模型架构图 |
| 第二阶段 | 第 3 ~ 10 月 | 后台系统、APP、PAD、小程序全部功能模块 |
| 第三阶段 | 第 11 月 | 全面压测、集成测试、上线部署 |
| 交付阶段 | 第 12 月 | 系统文档、保驾护航、运维移交 |
### 4.4 报价说明
1. **估算依据**:本报价依据《健康长庆系统升级改造详细规划》中的里程碑计划,对各模块进行详细工作量拆解,工作量单位为人天,共计 2,148 人天。
2. **费用范围**:本报价仅含软件开发人工成本,不含服务器硬件采购、网络设备、操作系统及数据库等基础软件授权、第三方 API 调用及云服务资源等费用。
3. **税费**:本报价为含税价,税率以双方合同约定为准。
4. **差旅费**:如需驻场或异地出差,差旅及住宿费用另行按实结算,不含在本报价内。
5. **变更管理**:若需求范围在项目执行过程中发生变更,双方应签署变更确认单,费用据实调整。
6. **有效期**:本报价单自出具之日起 **30 个自然日**内有效,逾期需重新确认。
7. **付款方式**:建议按里程碑节点分期付款,具体比例及时间节点以双方签订合同为准。
---
## 五、报价总额
> **项目总报价:叁佰捌拾陆万陆仟肆佰元整(¥ 386.64 万元)**
---
*本报价单最终以双方签订合同为准。*
+32
View File
@@ -356,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. **表单字段表**:含表单提交的页面,以表格列出字段名、类型、是否必填、校验规则