fix: 更新体重管理页面,清理旧文件,新增文档和启动脚本

This commit is contained in:
liuyunqin
2026-07-08 18:57:37 +08:00
parent 559eee84d1
commit 281e756a78
54 changed files with 1986 additions and 820 deletions
+563
View File
@@ -0,0 +1,563 @@
# 管理员端(健康应急APP)产品需求文档
## 模块概述
管理员端为健康应急APP的管理与配置角色提供移动端工作平台,负责监控体重管理活动的执行进度、追踪员工体检完成情况、查看应急工单处理状态以及穿戴设备异常告警记录。管理员通过数据看板掌握各业务线运行状况,并对体重管理目标进行审核把关。
### 页面导航结构
```
管理员端
├── 工作台(底部Tab
│ ├── 体重管理详情 → 每日打卡
│ │ ├── 饮食记录详情 → 员工饮食详情
│ │ ├── 运动记录详情 → 员工运动详情
│ │ └── 体重记录 → 员工体重详情
│ ├── 体重管理详情 → 减重排名
│ ├── 体重管理详情 → 目标审核
│ │ └── 目标审核页
│ ├── 体检进度追踪
│ ├── 应急求助工单 → 应急工单详情
│ └── 穿戴设备预警记录
├── 知识库(底部Tab
│ ├── 知识文章(分类Tab)→ 知识详情
│ └── 规章制度(PDF列表)
├── AI助手(底部Tab
├── 消息(底部Tab
│ ├── 消息列表
│ ├── 通讯录
│ └── 即时通讯
└── 我的(底部Tab
├── 修改个人信息
├── 修改密码
└── 退出登录(确认弹窗)
```
### 目标审核状态流转图
```mermaid
stateDiagram-v2
[*] --> 待审核 : 员工提交减重目标
待审核 --> 已通过 : 管理员审核通过
待审核 --> 已驳回 : 管理员审核驳回
已通过 --> 待签收 : 员工签收确认
```
### 应急工单状态流转图
```mermaid
stateDiagram-v2
[*] --> 进行中 : 操作人员派单
进行中 --> 已完成 : 驻场人员完成处理
```
## 页面清单
| 序号 | 文件名 | 页面名称 | 所属模块 |
|------|--------|----------|----------|
| 1 | workbench/index.html | 工作台首页 | 工作台 |
| 2 | workbench/weight-detail.html | 体重管理详情 | 工作台·体重管理 |
| 3 | workbench/eat-detail.html | 饮食记录详情 | 工作台·体重管理 |
| 4 | workbench/diet-employee-detail.html | 员工饮食详情 | 工作台·体重管理 |
| 5 | workbench/exercise-detail.html | 运动记录详情 | 工作台·体重管理 |
| 6 | workbench/exercise-employee-detail.html | 员工运动详情 | 工作台·体重管理 |
| 7 | workbench/weight-record.html | 体重记录 | 工作台·体重管理 |
| 8 | workbench/weight-employee-detail.html | 员工体重详情 | 工作台·体重管理 |
| 9 | workbench/goal-review.html | 目标审核 | 工作台·体重管理 |
| 10 | workbench/checkup-progress.html | 体检进度追踪 | 工作台·体检管理 |
| 11 | workbench/emergency-orders.html | 应急求助工单 | 工作台·应急管理 |
| 12 | workbench/emergency-order-detail.html | 应急工单详情 | 工作台·应急管理 |
| 13 | workbench/wearable-alerts.html | 穿戴设备预警记录 | 工作台·预警管理 |
| 14 | knowledge/index.html | 知识库首页 | 知识库 |
| 15 | knowledge/detail.html | 知识详情 | 知识库 |
| 16 | ../ai-assistant/index.html | AI 助手 | AI 助手 |
| 17 | ../message/index.html | 消息列表 | 消息 |
| 18 | ../message/contacts.html | 通讯录 | 消息 |
| 19 | ../message/im-chat.html | 即时通讯 | 消息 |
| 20 | profile/index.html | 个人中心主页 | 我的 |
| 21 | profile/edit-profile.html | 修改个人信息 | 我的 |
| 22 | profile/change-pwd.html | 修改密码 | 我的 |
## 逐页说明
---
## 一、工作台模块
### 1. 工作台首页
![页面截图](screenshots/index.jpg)
**功能说明:** 管理员工作台入口,顶部展示用户信息(头像、姓名、角色、部门),下方以功能卡片形式呈现四大管理入口:体重管理详情、体检进度追踪、应急求助工单、穿戴设备预警记录。
**业务规则:**
- 用户信息区:头像、姓名(王建国)、角色(健康管理员)、所属部门(安全环保部·健康管理科)
- 功能卡片共 4 个,两行两列排列:
1. 体重管理详情(蓝色图标)
2. 体检进度追踪(绿色图标)
3. 应急求助工单(橙色图标,角标数字"3"表示进行中工单数)
4. 穿戴设备预警记录(红色图标)
- 底部 5 个 Tab:工作台(当前激活,角标"3")、知识库、AI 助手、消息(角标"2")、我的
**交互说明:**
- 点击"体重管理详情"卡片:跳转到体重管理详情页
- 点击"体检进度追踪"卡片:跳转到体检进度追踪页
- 点击"应急求助工单"卡片:跳转到应急求助工单页
- 点击"穿戴设备预警记录"卡片:跳转到穿戴设备预警记录页
- 点击头像:跳转到个人中心主页
- 底部导航栏切换:跳转到对应 Tab 页面
---
## 二、体重管理业务线
### 2. 体重管理详情
![页面截图](screenshots/weight-detail.jpg)
**功能说明:** 体重管理活动总览页面,支持切换不同活动期次,展示活动整体统计数据,并通过三个主 Tab(每日打卡、减重排名、目标审核)分类查看管理数据。
**业务规则:**
- 顶部活动选择器:下拉切换不同活动期次(如"2026年Q2体重管理活动"),切换后刷新活动统计数据
- 活动统计区:已进行天数 / 总天数、预期参与人数、实际参与人数
- 三个主 Tab
- 每日打卡(默认):按日查看饮食、运动、体重的打卡统计数据
- 减重排名:4 个子 Tab(减重总量排名 / 减重率排名 / 综合评分排名 / 行动评分排名),各显示前 20 名
- 目标审核:4 个子 Tab(待审核 / 已通过 / 待签收 / 已驳回),显示员工减重目标申请
- 每日打卡面板:日期导航(左右箭头 + 日历选择器)、三张统计卡片(饮食已打卡XX人/未打卡XX人、运动已打卡XX人/未打卡XX人、体重已打卡XX人/未打卡XX人)
- 减重排名:前三名展示金/银/铜牌勋章 + 员工脱敏姓名 + 部门 + 数值
- 目标审核卡片:展示员工姓名、初始/目标数据对比、操作按钮
**交互说明:**
- 点击活动下拉框:展开活动列表,选择后切换数据
- 点击每日打卡 Tab:切换到打卡统计面板
- 点击减重排名 Tab:切换到排名面板
- 点击目标审核 Tab:切换到审核面板
- 点击饮食"查看详情":跳转到饮食记录详情页
- 点击运动"查看详情":跳转到运动记录详情页
- 点击体重"查看详情":跳转到体重记录页
- 日期导航左右箭头:切换前一天/后一天
- 点击日历图标:弹出日历选择面板
- 点击排名子 Tab:切换不同排名维度
- 点击审核子 Tab(待审核/已通过/待签收/已驳回):切换审核状态面板
- 点击目标审核卡片的"审核"按钮:跳转到目标审核页
- 点击返回:返回工作台首页
---
### 3. 饮食记录详情
![页面截图](screenshots/eat-detail.jpg)
**功能说明:** 按日查看员工的饮食打卡情况,分"已记录"和"未记录"两个 Tab 展示员工列表。
**业务规则:**
- 日期导航:支持左右箭头切换日期、日历面板选择日期
- 两个 Tab:已记录(角标数字显示已打卡人数)、未记录(角标数字显示未打卡人数)
- 员工卡片信息:头像(取姓名首字)、姓名、性别、年龄、部门、身高
- 已记录员工卡片右侧有"记录详情 >"链接
**交互说明:**
- 点击已记录员工的"记录详情":跳转到员工饮食详情页
- 点击日历图标:弹出日历选择面板
- 日期左右箭头:切换日期
- 点击 Tab 切换:已记录 / 未记录列表
- 点击返回:返回体重管理详情页
---
### 4. 员工饮食详情
![页面截图](screenshots/diet-employee-detail.jpg)
**功能说明:** 查看单个员工的当日饮食详细记录,包括各餐次的摄入热量、食物明细及记录来源。同时展示该员工的初始/目标/当前身体数据对比。
**业务规则:**
- 员工信息:姓名(张晓红)、性别、年龄、部门、身高
- 数据对比表(三列):初始数据 / 目标数据 / 当前数据,包含体重、BMI、体脂率、腰围
- 变化指标:体重变化、BMI 变化、体脂率变化、腰围变化(绿色数值表示下降)
- 当日饮食记录:按餐次展示(早餐、午餐、加餐、晚餐),每条记录显示热量(kcal)、时间、记录来源标签
- 记录来源标签颜色:蓝色(智能记录)、紫色(手动记录)
**交互说明:**
- 点击返回:返回饮食记录详情页
---
### 5. 运动记录详情
![页面截图](screenshots/exercise-detail.jpg)
**功能说明:** 按日查看员工的运动打卡情况,分"已记录"和"未记录"两个 Tab 展示员工列表。
**业务规则:**
- 日期导航:支持左右箭头切换日期、日历面板选择日期
- 两个 Tab:已记录(角标数字)、未记录(角标数字)
- 员工卡片信息:头像、姓名、性别、年龄、部门
- 已记录员工卡片右侧有"记录详情 >"链接
**交互说明:**
- 点击已记录员工的"记录详情":跳转到员工运动详情页
- 点击日历图标:弹出日历选择面板
- 日期左右箭头:切换日期
- 点击 Tab 切换:已记录 / 未记录列表
- 点击返回:返回体重管理详情页
---
### 6. 员工运动详情
![页面截图](screenshots/exercise-employee-detail.jpg)
**功能说明:** 查看单个员工的当日运动详细记录,包括运动类型、时长、消耗热量及记录来源。同时展示员工的初始/目标/当前身体数据对比。
**业务规则:**
- 员工信息:姓名(陈伟)、性别、年龄
- 数据对比表:初始数据 / 目标数据 / 当前数据(体重、BMI、体脂率、腰围)
- 变化指标:绿色显示下降数值
- 当日运动记录:跑步(45min/320kcal/穿戴设备)、骑行(30min/180kcal/手动记录)、游泳(60min/480kcal/器材记录)、步行(8,520步/150kcal/微信运动)
- 记录来源标签颜色:蓝色(穿戴设备/器材)、紫色(手动/微信运动)
**交互说明:**
- 点击返回:返回运动记录详情页
---
### 7. 体重记录
![页面截图](screenshots/weight-record.jpg)
**功能说明:** 按日查看员工的体重打卡情况,分"已记录"和"未记录"两个 Tab 展示员工列表。
**业务规则:**
- 日期导航:支持左右箭头切换日期、日历面板选择日期
- 两个 Tab:已记录(角标数字)、未记录(角标数字)
- 员工卡片信息:头像、姓名、性别、年龄、部门
- 已记录员工卡片右侧有"记录详情 >"链接
**交互说明:**
- 点击已记录员工的"记录详情":跳转到员工体重详情页
- 点击日历图标:弹出日历选择面板
- 日期左右箭头:切换日期
- 点击 Tab 切换:已记录 / 未记录列表
- 点击返回:返回体重管理详情页
---
### 8. 员工体重详情
![页面截图](screenshots/weight-employee-detail.jpg)
**功能说明:** 查看单个员工的体重历史记录,每条记录包含体重、体脂率、腰围及记录来源,同时展示该员工的初始/目标/当前身体数据对比。
**业务规则:**
- 员工信息:姓名(张晓红)、性别、年龄、部门
- 数据对比表:初始数据 / 目标数据 / 当前数据(体重、BMI、体脂率、腰围)
- 变化指标:绿色显示下降数值
- 体重记录列表:按时间倒序,每条显示时间、体重、体脂率、腰围、来源标签
- 记录来源标签颜色:蓝色(蓝牙秤/身高体重秤)、紫色(体脂仪/手动/穿戴设备)
**交互说明:**
- 点击返回:返回体重记录页
---
### 9. 目标审核
![页面截图](screenshots/goal-review.jpg)
**功能说明:** 管理员审核员工提交的体重管理减重目标申请,可选择通过或驳回,驳回时需填写驳回原因。已审核的目标以只读方式展示审核结果。
**业务规则:**
- 通过 URL 参数 `status` 区分页面模式:pending(待审核)/ approved(已通过)/ signed(待签收)/ rejected(已驳回)
- 待审核模式:
- 展示员工基本信息卡片(姓名、性别、年龄、身高)
- 数据对比卡片:初始数据(体重/BMI/体脂率/腰围)、目标数据(绿色)、推荐数据范围(蓝色参考)
- 活动时间范围卡片:开始日期 → 结束日期
- 底部操作区:单选按钮(通过 / 驳回)、驳回时展开驳回原因文本框、提交审核按钮
- 已通过/待签收/已驳回模式:
- 以只读方式展示审核结果,包含状态标签、审核人、审核时间
- 已驳回额外展示驳回原因
- 无操作按钮
**交互说明:**
- 点击"通过"单选按钮:隐藏驳回原因文本框
- 点击"驳回"单选按钮:展开驳回原因文本框(placeholder: "请输入驳回原因..."
- 点击"提交审核":弹出确认弹窗
- 确认弹窗:标题根据操作变化(通过审核 / 驳回申请),显示简要确认信息,取消按钮关闭弹窗,确认按钮执行操作
- 确认后弹出成功弹窗:"审核完成",点击"返回列表"跳转回体重管理详情页
- 点击返回:返回体重管理详情页
**审核操作表单:**
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 审核结果 | 单选按钮 | 是 | 必选"通过"或"驳回" |
| 驳回原因 | 多行文本 | 驳回时必填 | 驳回时不可为空 |
---
## 三、体检管理业务线
### 10. 体检进度追踪
![页面截图](screenshots/checkup-progress.jpg)
**功能说明:** 按体检计划期次查看员工的体检预约和完成进度,通过四个分段 Tab(已预约、已检查、未预约、未检查)统计并展示各状态的员工列表。
**业务规则:**
- 顶部体检计划选择器:下拉切换不同体检计划(如"2026年度体检""2025年度体检""2025职业病体检"),切换后刷新各状态统计数据
- 统计区:预期人数、已预约人数、已检查人数、未预约人数、未检查人数
- 四个分段 Tab:已预约(角标数字)、已检查(角标数字)、未预约(角标数字)、未检查(角标数字)
- 员工卡片信息:头像(姓名首字)、姓名 + 工号、所属单位 + 部门、联系电话
- 数据通过 JS 模拟数据动态渲染
**交互说明:**
- 点击体检计划下拉框:展开计划列表,选择后切换数据
- 点击分段 Tab:切换不同状态的员工列表
- 点击返回:返回工作台首页
---
## 四、应急管理业务线
### 11. 应急求助工单
![页面截图](screenshots/emergency-orders.jpg)
**功能说明:** 查看所有应急求助工单的处理情况,分"进行中"和"已完成"两个 Tab 展示,支持按月度和求助方式筛选。管理员以只读监控视角查看工单流转。
**业务规则:**
- 两个主 Tab:进行中(角标数字"3")、已完成(角标数字"12"
- 筛选栏:月份下拉(全部 / 2026-06 / 2026-05 / 2026-04)、求助方式下拉(全部 / 应急视频 / 应急电话 / 拨120)
- 工单卡片信息:求助方式标签(颜色区分:视频粉色、电话橙色、120红色)、时间、求助员工信息(头像、姓名、工号、性别、年龄、单位、部门、电话)、工单详情行(应急中心、操作人员、专业人员)
- 数据通过 JS 模拟,筛选条件联动更新两个 Tab 的列表
**交互说明:**
- 点击进行中/已完成 Tab:切换工单列表
- 点击月份下拉:按月份筛选工单
- 点击求助方式下拉:按求助方式筛选工单
- 点击工单卡片:跳转到应急工单详情页
- 点击返回:返回工作台首页
---
### 12. 应急工单详情
![页面截图](screenshots/emergency-order-detail.jpg)
**功能说明:** 查看单条应急工单的完整信息,以可折叠卡片形式展示基本信息、救助信息、派单信息、救护车支持信息四个维度的详情。管理员视角为纯只读页面。
**业务规则:**
- 四个可折叠卡片(默认展开):
1. 基本信息:工单编号、应急中心、求助员工姓名/工号、单位、部门、求助方式、求助时间、位置信息(含地图图标)、操作人员、专业人员、工单状态(已完成绿色标签)、完成时间
2. 救助信息:救助意见(文本)、救助结果(文本)、相关照片(3 张占位图)、补充小结(多条带圆点标记的列表)
3. 派单信息:派单时间、驻场人员、派单状态(已完成)、结束时间、派单说明、驻场说明
4. 救护车支持:车牌号、机构、驾驶员、驾驶员电话、护送人员、护送人员电话、目的地、备注
- 纯只读展示,无编辑或操作按钮
**交互说明:**
- 点击卡片标题行:折叠/展开卡片内容
- 点击返回:返回应急求助工单列表页
---
## 五、预警管理业务线
### 13. 穿戴设备预警记录
![页面截图](screenshots/wearable-alerts.jpg)
**功能说明:** 查看所有员工穿戴设备的异常告警记录,支持按月份和告警类型筛选。
**业务规则:**
- 筛选栏:月份下拉(全部 / 2026-06 / 2026-05 / 2026-04)、告警类型下拉(全部 / SOS 求助 / 体温异常 / 心率异常 / 血氧异常 / 压力异常 / 跌倒检测)
- 告警卡片信息:告警类型标签(颜色区分类型)、时间戳、异常数值(含单位)、员工信息(头像、姓名、工号、性别、年龄、单位、部门)、位置信息(含定位图标)
- 告警类型共 6 种:SOS 求助(红色)、体温异常(橙色)、心率异常(红色)、血氧异常(紫色)、压力异常(蓝色)、跌倒检测(红色)
- 数据通过 JS 模拟,筛选条件联动更新列表
**交互说明:**
- 点击月份下拉:按月份筛选告警记录
- 点击告警类型下拉:按类型筛选告警记录
- 点击返回:返回工作台首页
---
## 六、知识库模块
### 14. 知识库首页
![页面截图](screenshots/knowledge-index.jpg)
**功能说明:** 专业知识参考库,分"知识文章"和"规章制度"两个大类,知识文章下再按二级分类 Tab 组织。
**业务规则:**
- 两大主 Tab:知识文章(默认)/ 规章制度
- 知识文章二级分类 Tab(4 个):体检知识(4 篇)、药品知识(2 篇)、疾病知识(2 篇)、健康知识(2 篇)
- 知识文章列表:每篇文章展示图标、标题、简介描述、右箭头
- 规章制度列表:5 条 PDF 文档条目(员工健康管理办法、职业病防治规则、体检异常处理流程、健康咨询服务规范、应急救援操作标准)
- 文章数据通过 JS 对象动态渲染
**交互说明:**
- 点击知识文章/规章制度主 Tab:切换大类
- 点击二级分类 Tab:切换知识分类
- 点击文章条目:跳转到知识详情页(通过 `?key=<文章标识>` 传递参数)
- 点击规章制度条目:打开对应 PDF(原型中为占位链接)
---
### 15. 知识详情
![页面截图](screenshots/knowledge-detail.jpg)
**功能说明:** 展示单篇知识文章的详细内容,包括文章基本信息、摘要和要点列表。
**业务规则:**
- 通过 URL 参数 `key` 确定展示哪篇文章,共有 10 篇文章数据
- 文章内容:图标、标题、分类、发布日期、摘要段、要点列表(彩色标题 + 灰色圆角说明卡片)
- 体检知识(4 篇):血常规解读、心电图判读、肝功能解读、血糖解读
- 药品知识(2 篇):降压药物、他汀类药物
- 疾病知识(2 篇):高血压、偏头痛
- 健康知识(2 篇):营养指导、运动与心血管健康
**交互说明:**
- 点击返回:返回知识库首页
---
## 七、AI 助手
### 16. AI 助手
![页面截图](screenshots/../ai-assistant/screenshots/index.jpg)
**功能说明:** 智能健康问答助手,支持文字输入提问,提供快捷问题入口。全角色共享页面。
**业务规则:**
- 欢迎语和三个快捷问题入口(偏头痛鉴别诊断、高血压用药建议、血脂异常处理方案)
- AI 对话模拟:用户发送消息后,AI 延迟回复占位文字
- 通过 `sessionStorage.appRole` 识别当前角色为 admin,设置正确的 Tab 导航路径
**交互说明:**
- 输入文字后点击发送或按回车:发送消息,显示用户消息气泡(紫色右对齐)
- 点击快捷问题标签:自动填入输入框并发送
- 底部 Tab 切换:跳转到对应角色页面
---
## 八、消息模块
### 17. 消息列表
![页面截图](screenshots/../message/screenshots/index.jpg)
**功能说明:** 即时通讯消息汇总列表,展示最近对话的联系人及消息预览。全角色共享页面。
**业务规则:**
- 6 条消息会话:咨询小助手(未读"2")、李主任(专业人员)、张护士长(专业人员)、陈运营(操作人员)、刘驻场(驻场人员)、系统管理员
- 每条展示:头像、姓名、角色标签(颜色区分)、时间、消息预览
- 通过 `sessionStorage.appRole` 识别当前角色,设置正确的底部 Tab 导航路径
**交互说明:**
- 点击消息条目:跳转到即时通讯页
- 点击右上角"通讯录":跳转到通讯录页
- 底部 Tab 切换:跳转到对应角色页面
---
### 18. 通讯录
![页面截图](screenshots/../message/screenshots/contacts.jpg)
**功能说明:** 组织内联系人列表,按角色分组展示,支持搜索过滤。全角色共享页面。
**业务规则:**
- 6 个联系人分组:小助手、咨询专家、健康管理员、操作人员、专业人员、驻场人员
- 每组可折叠/展开,展示成员姓名、角色标签、所属机构
- 搜索框支持按姓名实时过滤,空分组自动隐藏
**交互说明:**
- 点击分组标题:折叠/展开该分组
- 在搜索框输入文字:实时过滤联系人
- 点击联系人:跳转到即时通讯页
- 点击返回:返回消息列表页
---
### 19. 即时通讯
![页面截图](screenshots/../message/screenshots/im-chat.jpg)
**功能说明:** 单聊页面,支持文字和语音输入,可发送图片。全角色共享页面。
**业务规则:**
- 导航栏标题显示当前对话对象名称
- 预置示例对话(与"咨询小助手"的聊天记录)
- 支持文字输入、语音输入(按住说话)、多媒体附件(拍照/图库)
- 发送的消息为蓝色右对齐气泡,接收的消息为白色左对齐气泡
**交互说明:**
- 在输入框输入文字后点击发送:消息以蓝色气泡展示在右侧
- 点击语音切换按钮:切换为按住说话模式
- 点击"+"按钮:展开多媒体面板(拍照/图库选项,原型中为占位)
- 点击返回:返回消息列表页
---
## 九、个人中心模块
### 20. 个人中心主页
![页面截图](screenshots/profile-index.jpg)
**功能说明:** 管理员个人信息展示和功能菜单导航。
**业务规则:**
- 用户信息区:头像、姓名(王建国)、角色(健康管理员)、所属部门(安全环保部·健康管理科)
- 菜单项:修改个人信息、修改密码、关于我们
- 退出登录按钮(红色文字)
**交互说明:**
- 点击"修改个人信息":跳转到修改个人信息页
- 点击"修改密码":跳转到修改密码页
- 点击"退出登录":弹出二次确认弹窗("确认退出当前账号?"),取消关闭弹窗,确认退出登录
- 底部 Tab 切换:跳转到对应页面
---
### 21. 修改个人信息
![页面截图](screenshots/profile-edit-profile.jpg)
**功能说明:** 编辑管理员头像和姓名。
**交互说明:**
- 点击头像:弹出底部操作面板(拍照 / 从相册选择 / 取消)
- 点击"保存":验证姓名非空,显示"保存成功"提示(1.5 秒自动消失)
- 点击返回:返回个人中心主页
**表单字段表:**
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 头像 | 图片选择 | 否 | 支持拍照或从相册选择 |
| 姓名 | 单行文本 | 是 | 不能为空 |
---
### 22. 修改密码
![页面截图](screenshots/profile-change-pwd.jpg)
**功能说明:** 修改管理员登录密码。
**交互说明:**
- 点击"保存修改":弹出修改成功确认弹窗("密码修改成功,下次登录请使用新密码"),点击确定后跳转回个人中心
**表单字段表:**
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 当前密码 | 密码输入 | 是 | 不能为空 |
| 新密码 | 密码输入 | 是 | 不能为空 |
| 确认新密码 | 密码输入 | 是 | 必须与新密码一致 |
+165
View File
@@ -0,0 +1,165 @@
/**
* 管理员端(健康应急APP)页面截图脚本
* 使用 Puppeteer 批量截取 HTML 原型页面,输出 JPEG(质量 85%)到 doc/screenshots/
*
* 用法: node doc/screenshot.js
*/
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');
const MODULE_DIR = __dirname.replace(/\\doc$/, ''); // admin/
const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots');
// 所有需要截图的页面(相对于模块目录)
const PAGES = [
// 工作台
'workbench/index.html',
'workbench/weight-detail.html',
'workbench/eat-detail.html',
'workbench/diet-employee-detail.html',
'workbench/exercise-detail.html',
'workbench/exercise-employee-detail.html',
'workbench/weight-record.html',
'workbench/weight-employee-detail.html',
'workbench/goal-review.html',
'workbench/checkup-progress.html',
'workbench/emergency-orders.html',
'workbench/emergency-order-detail.html',
'workbench/wearable-alerts.html',
// 知识库
'knowledge/index.html',
'knowledge/detail.html',
// 个人中心
'profile/index.html',
'profile/edit-profile.html',
'profile/change-pwd.html',
];
async function screenshotPage(browser, htmlFile) {
const page = await browser.newPage();
const filePath = path.join(MODULE_DIR, htmlFile);
const fileUrl = `file:///${filePath.replace(/\\/g, '/')}`;
const viewportWidth = 390;
const viewportHeight = 844;
await page.setViewport({ width: viewportWidth, height: viewportHeight, deviceScaleFactor: 2 });
try {
await page.goto(fileUrl, { waitUntil: 'networkidle0', timeout: 15000 });
// 等待页面渲染完成
await new Promise(r => setTimeout(r, 500));
// 注入 CSS:移除 phone-shell 限制,展开全部内容
await page.evaluate(() => {
const style = document.createElement('style');
style.id = 'screenshot-unwrap';
style.textContent = `
/* 移除手机壳固定高度和装饰 */
.phone-shell {
height: auto !important;
min-height: auto !important;
max-height: none !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow: visible !important;
}
/* 滚动区域全部展开 */
.scroll-content,
[class*="scroll"] {
overflow: visible !important;
max-height: none !important;
height: auto !important;
}
/* 页面外层背景改为白色 */
body {
background: #fff !important;
padding: 0 !important;
margin: 0 !important;
}
/* 隐藏下拉菜单、遮罩等弹窗遮挡元素 */
.dropdown-menu, .dropdown-mask, .calendar-mask,
.plan-dropdown, .plan-mask {
display: none !important;
}
/* 底部安全区缩到最小 */
.bottom-safe {
height: 8px !important;
}
`;
document.head.appendChild(style);
});
// 等待样式生效
await new Promise(r => setTimeout(r, 300));
// 获取实际内容高度
const bodyHandle = await page.$('.phone-shell');
const contentHeight = await page.evaluate((el) => {
if (el) return el.scrollHeight;
return document.body.scrollHeight;
}, bodyHandle);
const finalHeight = Math.max(contentHeight || viewportHeight, 400);
await page.setViewport({ width: viewportWidth, height: Math.min(finalHeight + 40, 12000), deviceScaleFactor: 2 });
// 使用简洁文件名,与 PRD 引用一致
// workbench/index.html → index.jpg, knowledge/index.html → knowledge-index.jpg
let outName = htmlFile.replace('.html', '.jpg');
if (outName.startsWith('workbench/')) {
outName = outName.replace('workbench/', '');
} else if (outName.startsWith('knowledge/')) {
outName = outName.replace('knowledge/', 'knowledge-');
} else if (outName.startsWith('profile/')) {
outName = outName.replace('profile/', 'profile-');
}
const outPath = path.join(SCREENSHOTS_DIR, outName);
await page.screenshot({
path: outPath,
type: 'jpeg',
quality: 85,
fullPage: true,
});
console.log(`${htmlFile}${outName} (${viewportWidth}×${finalHeight})`);
} catch (err) {
console.error(`${htmlFile}: ${err.message}`);
} finally {
await page.close();
}
}
(async () => {
console.log('管理员端(健康应急APP)- 页面截图');
console.log(`模块目录: ${MODULE_DIR}`);
console.log(`输出目录: ${SCREENSHOTS_DIR}`);
console.log(`${PAGES.length} 个页面\n`);
// 确保输出目录存在
if (!fs.existsSync(SCREENSHOTS_DIR)) {
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
}
const browser = await puppeteer.launch({
headless: true,
executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
for (let i = 0; i < PAGES.length; i++) {
const pageName = PAGES[i];
console.log(`[${i + 1}/${PAGES.length}] ${pageName}`);
await screenshotPage(browser, pageName);
}
console.log(`\n✅ 全部完成!共 ${PAGES.length} 张截图,保存在 ${SCREENSHOTS_DIR}`);
} catch (err) {
console.error('执行出错:', err);
process.exit(1);
} finally {
await browser.close();
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

+874
View File
@@ -0,0 +1,874 @@
# 体重管理(员工端)产品需求文档
## 模块概述
体重管理员工端为员工提供体重管理全流程能力,涵盖每日吃/动/体重打卡、历史数据查看、吃动平衡分析、膳食与运动建议、体重报告与排行榜、以及目标承诺多步骤流程(基本信息填写→健康指标录入→目标设定→承诺签订)。
### 页面导航结构
```
体重管理(员工端)
├── 目标承诺(多步骤流程)
│ ├── 目标管理 → 活动详情
│ ├── 基本信息
│ ├── 指标慢病
│ ├── 禁忌证
│ ├── 签订承诺
│ └── 我的承诺
├── 体重管理主页(核心中枢页)
├── 吃打卡 → 摄入记录
├── 动打卡 → 消耗记录
├── 体重打卡 → 体重记录
├── 吃动平衡历史
├── 膳食建议
│ └── 膳食偏好设置
├── 运动建议
├── 体重报告
└── 排行榜
```
### 状态流转图
```mermaid
stateDiagram-v2
[*] --> 目标管理: 员工进入
目标管理 --> 设置个人目标: 选择个人目标模式
目标管理 --> 选择单位活动: 加入单位活动
设置个人目标 --> 体重管理主页: 进入体重管理
选择单位活动 --> 待审核: 提交审核
待审核 --> 审核通过: 管理员审核通过
待审核 --> 已驳回: 管理员驳回
已驳回 --> 待审核: 修改后重新提交
审核通过 --> 签订承诺书: 签订承诺书
签订承诺书 --> 体重管理主页: 进入体重管理
体重管理主页 --> 每日打卡: 吃/动/体重
每日打卡 --> 体重记录: 查看体重历史
每日打卡 --> 摄入记录: 查看摄入历史
每日打卡 --> 消耗记录: 查看消耗历史
体重记录 --> 详细报告: 点击设备来源记录
体重管理主页 --> 吃动平衡历史: 查看吃动平衡
体重管理主页 --> 膳食建议: 查看建议
体重管理主页 --> 运动建议: 查看建议
体重管理主页 --> 体重报告: 查看报告
体重管理主页 --> 排行榜: 查看排名
```
---
## 页面清单
### 目标承诺(多步骤流程)
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 1 | 目标承诺 | goal-commitment.html | 页面 |
| 2 | 目标管理 | goal-management.html | 页面 |
| 3 | 基本信息 | basic-info.html | 页面 |
| 4 | 指标慢病 | indicators-diseases.html | 页面 |
| 5 | 禁忌证 | contraindications.html | 页面 |
| 6 | 签订承诺 | promise-signing.html | 页面 |
| 7 | 我的承诺 | my-promise.html | 页面 |
| 8 | 活动详情 | activity-detail.html | 页面 |
### 体重管理主页
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 9 | 体重管理主页 | weight-main.html | 页面 |
### 每日打卡
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 10 | 吃打卡 | eat-checkin.html | 页面 |
| 11 | 动打卡 | exercise-checkin.html | 页面 |
| 12 | 体重打卡 | weight-checkin.html | 页面 |
### 历史记录
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 13 | 体重记录 | weight-history.html | 页面 |
| 14 | 详细报告 | report-detail.html | 页面 |
| 15 | 摄入记录 | eat-history.html | 页面 |
| 16 | 消耗记录 | exercise-history.html | 页面 |
| 17 | 吃动平衡历史 | balance-history.html | 页面 |
### 建议与报告
| 序号 | 页面/弹窗 | 文件 | 类型 |
|------|-----------|------|------|
| 18 | 膳食建议 | diet-advice.html | 页面 |
| 19 | 膳食偏好设置 | diet-questionnaire.html | 页面 |
| 20 | 运动建议 | exercise-advice.html | 页面 |
| 21 | 体重报告 | weight-report.html | 页面 |
| 22 | 排行榜 | weight-ranking.html | 页面 |
---
## 逐页说明
---
## 一、目标承诺(多步骤流程)
### 1. 目标承诺(goal-commitment.html
![页面截图](screenshots/goal-commitment.jpg)
**功能说明:** 目标承诺流程的总入口页面,展示体重管理承诺书内容和多步骤操作引导(填写基本信息→完善指标慢病→设定目标→签订承诺),包含活动审核状态流转和驳回原因查看。
**页面状态:** 通过右上角三点菜单切换 7 种状态演示:未选目标-无活动、未选目标-有活动、已选个人目标、单位活动-待审核、单位活动-审核通过、单位活动-已驳回、单位活动-已签署承诺书。
**主要UI元素:**
- 顶部横幅Banner(青绿色渐变):"体重管理 让健康行动走进生活",跑步小人插画
- 步骤卡片(白色圆角卡片):
- 活动名称/目标和起止日期
- 步骤进度条和天数统计
- 目标指标(初始体重→目标体重 / 体脂率变化等)
- 审核状态行(含状态标签和驳回原因查看入口)
- 引导语
- 四个步骤入口(箭头串联):
1. 填写基本信息(跳转到基本信息页)
2. 完善指标慢病(跳转到指标慢病页)
3. 设定目标(跳转到目标管理页)
4. 签订承诺(跳转到签订承诺页)/ 查看承诺书(跳转到我的承诺页)
- 承诺书正文区:说明文字、承诺条款
- 落款区(承诺人签名、日期)
- 底部"确认"按钮
**交互说明:**
- 点击步骤入口跳转到对应页面
- 点击返回按钮跳转到应用主页
**各状态显示说明:**
| 状态 | 活动名称 | 目标统计 | 提示文案 | 审核状态行 | 签署步骤 | 活动说明数据 |
|------|:--:|:--:|------|------|------|:--:|
| 未选目标-无活动 | ❌ | ❌ | "请先前往目标管理页面确定目标" | ❌ | 签署承诺 | ❌ |
| 未选目标-有活动 | ✅ | ❌ | "请前往目标管理页面确认数据并提交审核" | ❌ | 签署承诺 | ❌ |
| 已选个人目标 | ❌ | ✅ | ❌ | ❌ | 签署承诺 | ✅ |
| 待审核 | ✅ | ✅ | ❌ | 🟡 管理员审核中,请耐心等待… | 签署承诺 | ❌ |
| 审核通过 | ✅ | ✅ | ❌ | 🟢 管理员已审核通过,请尽快签订承诺书参加活动 | 签署承诺 | ❌ |
| 已驳回 | ✅ | ✅ | ❌ | 🔴 管理员审核驳回,[查看原因],请重新调整目标 | 签署承诺 | ❌ |
| 已签署承诺书 | ✅ | ✅ | ❌ | ❌ | ✅ 我的承诺 | ✅ |
- **审核驳回状态**:点击"查看原因"打开驳回原因弹窗,含驳回正文、审核人、审核时间,可点击"去调整"跳转目标管理页
- **已签署状态**:第四步"签署承诺"变为"我的承诺"(绿色 + ✓图标),跳转到我的承诺页(只读查看已签署的承诺书)
- **未选目标状态**:步骤区下方显示引导提示文案,目标统计区隐藏
**业务规则:**
- 如果有指标慢病异常时,“指标慢病”步骤入口显示黄色警示图标和不同样式
- 审核状态:待审核(黄色)、审核通过(绿色)、已驳回(红色,含驳回原因)
- 驳回后可修改重新提交
- 审核通过后可签订承诺书
---
### 2. 目标管理(goal-management.html
![页面截图](screenshots/goal-management.jpg)
**功能说明:** 目标设定的核心页面,支持选择单位活动或自定个人目标两种模式,填写初始数据并设定目标体重/体脂/腰围,提交审核。
**页面状态:** 通过右上角三点菜单切换 7 种状态演示:未选目标-无活动、未选目标-有活动、已选个人目标、单位活动-待审核、单位活动-审核通过、单位活动-已驳回、单位活动-已签署承诺书。
**主要UI元素:**
- 活动目标卡片(可选):活动选择下拉、"查看活动详情"链接
- 个人目标卡片(可选):计划名称输入框、"自动起名"按钮
- 个人信息卡片:身高、性别、年龄,含"完善基本信息"链接
- 初始数据录入卡片:初始体重/体脂/腰围(支持录入方式:身高秤/蓝牙秤/自记)
- 人群分类卡片:自动计算标签("减重人群"/“控重人群”)
- "开始计算"按钮
- 目标设定卡片:目标体重/目标体脂/目标腰围,每项含"修改"按钮(打开标尺弹窗)、系统推荐值、警示提示
- 任务概览卡片:减重目标/时间/减重要求/饮食要求/运动要求
- 操作按钮(按状态变化):
- 未提交:"提交审核"
- 待审核:-
- 已驳回:"重新提交"(可编辑修改后重新提交)
- 审核通过:"重新提交"(可编辑修改后重新提交)
**交互说明:**
- 点击活动选择下拉打开活动底部弹窗,选择活动后,显示“查看活动详情”按钮
- 点击"查看活动详情"跳转到活动详情页面
- 如果身高、性别、年龄信息不完善,则显示“个人信息”卡片,点击"完善基本信息"跳转到基本信息页面
- 点击初始数据录入模块的身高秤/蓝牙称按钮,获取设备最新的一条记录进行显示,点击自记按钮,打开标尺弹窗(体重/体脂/腰围)进行手动录入
- 点击目标设定各项的"修改"打开标尺弹窗(体重/体脂/腰围)
- 点击"开始计算"生成并显示任务概览卡片
- 点击底部按钮(提交审核/重新提交)触发对应操作
- 审核驳回状态:点击状态栏"查看原因"打开驳回原因弹窗
- 点击返回按钮返回目标承诺页
**各状态显示说明:**
| 状态 | 目标模式 | 活动卡片 | 个人卡片 | 信息卡片 | 编辑权限 | 底部按钮 | 状态栏 |
|------|:--:|:--:|:--:|:--:|------|------|------|
| 未选目标-无活动 | 活动 | ✅ | ✅ | ✅ | 可编辑 | 提交审核 | ❌ |
| 未选目标-有活动 | 活动 | ✅ | ✅ | ✅ | 可编辑 | 提交审核 | ❌ |
| 已选个人目标 | 个人 | ❌ | ✅ | ❌ | 锁定 | 修改目标 | ❌ |
| 待审核 | 活动 | ✅ | ❌ | ❌ | 只读 | ❌ | 🟡 管理员审核中,请耐心等待… |
| 审核通过 | 活动 | ✅ | ❌ | ❌ | 可编辑 | 重新提交 | 🟢 管理员已审核通过,请尽快签订承诺书参加活动 |
| 已驳回 | 活动 | ✅ | ❌ | ❌ | 可编辑 | 重新提交 | 🔴 管理员审核驳回,[查看原因],请重新调整目标 |
| 已签署承诺书 | 活动 | ✅ | ❌ | ❌ | 只读 | ❌ | ✅ 已签署承诺书并参与活动 |
- **个人目标模式**:初始为锁定态,活动卡片隐藏、开始计算按钮隐藏、数据源按钮置灰;点击"修改目标"解锁后可编辑全部字段、活动卡片重新显示,底部按钮切换为"确定目标"
- **待审核/已签署**:全局只读,卡片不可选、所有输入框和数据源按钮锁定,"修改"按钮不可点击
- **审核通过/已驳回**:可编辑模式,允许修改目标值并重新提交,区别仅在于状态栏提示文案和驳回原因查看入口
- **未选目标状态**:活动与个人两种模式可通过点击卡片手动切换,"活动详情"链接根据是否选择活动控制显隐
**业务规则:**
- 单位活动模式:活动天数不可修改
- 个人目标模式:自我管理天数默认一个月,可更改
- 审核状态为驳回时,显示驳回原因入口
- 审核通过后可修改目标并重新提交,状态栏提示"管理员已审核通过,请尽快签订承诺书参加活动"
---
### 3. 基本信息(basic-info.html
![页面截图](screenshots/basic-info.jpg)
**功能说明:** 展示和编辑员工的个人基本信息,包括身高、体力活动等级等可编辑字段,其余字段为只读。
**主要UI元素:**
- 信息表单卡片:无数据时,显示“添加”按钮,可点击添加信息
- 姓名
- 性别
- 出生年月
- 身高(cm
- 体重(kg
- BMI:(自动计算)
- 体脂率(%
- 腰围(cm
- 体力活动:"添加"(打开活动等级选择弹窗)
- 基础代谢率(kcal)(自动计算)
- 身份证号
- 手机号
**交互说明:**
- 如身高、体重、体质路、腰围点击添加标尺弹窗
- 如体力活动行打开活动等级底部弹窗(五选一:久坐不运动/久坐运动或久站/体力劳动者/久站+运动/健身+体力劳动)
- 点击返回按钮返回上一页
**业务规则:**
---
### 4. 指标慢病(indicators-diseases.html
![页面截图](screenshots/indicators-diseases.jpg)
**功能说明:** 展示员工的健康重点指标(体重/血压/血糖/尿酸/血脂)和慢性疾病史,支持指标测量录入和疾病信息修改。
**主要UI元素:**
- 双Tab:重点指标 / 慢性疾病
**重点指标 Tab**
- 体重卡片:体重、BMI 、含"测量"按钮
- 血压卡片:收缩压/舒张压、状态标签、参考年份、"测量"按钮
- 血糖卡片:空腹血糖值、状态标签、参考范围、参考年份、"测量"按钮
- 尿酸卡片:尿酸值、状态标签、参考范围、参考年份、"测量"按钮
- 血脂卡片:总胆固醇值,可展开查看甘油三脂/高密度脂蛋白/低密度脂蛋白(异常项红色高亮)
**慢性疾病 Tab**
- 8种慢性病卡片列表(原发性高血压/冠状动脉硬化性心脏病/脑血管病后遗症/慢性再生障碍性贫血/风湿性心脏病/慢性活动性肝炎肝硬化/慢性阻塞性肺疾病/肺源性心脏病),各显示"确诊时间:无"
- 每项含"修改"按钮
**交互说明:**
- 点击重点指标/慢性疾病Tab切换
- 点击各指标"测量"按钮打开对应测量标尺弹窗(体重kg/血压mmHg/血糖mmol/L/尿酸mmol/L/血脂四项)
- 点击慢性疾病"修改"按钮打开确诊信息弹窗(未确诊/已确诊切换,已确诊需填写确诊医院和年月)
- 点击返回按钮返回上一页
**业务规则:**
- 异常指标红色高亮显示
- 重点指标从体检数据中获取,慢性疾病从五类人群数据中获取,在该页面进行手动修改数据时,会新建一张表
单独存放这些数据,后续进行查询时,从两个来源地方取最新
---
### 5. 禁忌证(contraindications.html
![页面截图](screenshots/contraindications.jpg)
**功能说明:** 展示体重管理活动的禁忌证清单,供员工自评。页面为只读静态内容。
**主要UI元素:**
- 禁忌证卡片
- 每项含序号圆形图标和文字说明
**禁忌证内容:**
按原项目实际内容进行展示
**交互说明:**
- 页面为纯展示,无交互操作
- 点击返回按钮返回上一页
---
### 6. 签订承诺(promise-signing.html
![页面截图](screenshots/promise-signing.jpg)
**功能说明:** 横屏签名页面,员工阅读并签署体重管理活动承诺书,支持手写签名。
**主要UI元素:**
- 竖排标题(左侧边栏):"体重管理活动承诺签订"
- 承诺条款正文区:3条承诺事项
- 签名画布(虚线边框区域):"请清晰书写您的签名"占位提示
- 底部操作按钮:放弃签订承诺书 / 重写 / 完成
**交互说明:**
- 在签名画布区域手写签名
- 点击"重写"清空签名
- 点击"完成"保存签名并跳转到目标承诺页(type=signed
- 点击"放弃签订承诺书"弹出确认弹窗
- 点击返回按钮返回上一页
**业务规则:**
- 页面为横屏布局
- 签名不可为空
---
### 7. 我的承诺(my-promise.html
![页面截图](screenshots/my-promise.jpg)
**功能说明:** 查看已签署的体重管理活动承诺书,证书样式展示。
**主要UI元素:**
- 标题:"体重管理活动承诺书"
- 3条承诺事项正文
- 签名落款区(承诺人姓名、签署日期)
**交互说明:**
- 页面为只读展示
- 点击返回按钮返回上一页
---
### 8. 活动详情(activity-detail.html
![页面截图](screenshots/activity-detail.jpg)
**功能说明:** 展示体重管理活动的详细信息,包括基本信息、奖励标准、计分规则和附件下载。
**主要UI元素:**
- 活动信息卡片:
- 状态标签
- 活动名称
- 活动时间范围
- 参与人数
- 组织部门
- 负责人+联系电话按钮
- 奖励等级标准卡片
- 综合分规则卡片(公式说明)
- 行动分规则卡片(吃/动/体重打卡计分规则)
- 附件链接:"附件《长庆油田体重管理三年行动计划.docx》"
**交互说明:**
- 点击"联系电话"底部弹窗显示显示负责人电话,再次点击可跳转到拨打页面
- 点击附件链接直接打开预览
- 点击返回按钮返回上一页
---
## 二、体重管理主页(核心中枢)
### 9. 体重管理主页(weight-main.html
![页面截图](screenshots/weight-main.jpg)
**功能说明:** 体重管理模块的核心中枢页面,聚合展示体重数据、每日打卡入口、减重进度、吃动平衡、体重记录趋势、排行榜和建议入口。
**页面状态:** 通过右上角三点菜单切换 3 种业务模式演示:已参加活动、已设置个人目标、未参加未设置未组织。各模式下的 UI 显隐规则如下:
| UI 区域 | `hasact`(已参加活动) | `personal`(已设置个人目标) | `none`(未参加未设置未组织) |
|---------|----------------------|---------------------------|----------------------|
| 排名副标题栏 | ✅ 显示 | ❌ 隐藏 | ❌ 隐藏 |
| 减重进度卡片 | ✅ 显示 | ✅ 显示 | ❌ 隐藏 |
| 排行榜-正常卡片(含"我的排名") | ✅ 显示(含"我的排名"行) | ✅ 显示(不含"我的排名"行) | ❌ 隐藏 |
| 排行榜-缺省占位 | ❌ 隐藏 | ❌ 隐藏 | ✅ 显示"排行榜示意图" |
**主要UI元素:**
- **排名副标题栏:** 显示当前活动标题+排名信息,如"公司机关020411体重管理计划 第8名(共8626人)"
- **体重卡片(核心区):** 左右人物形象插画、中间球形仪显示当前体重/BMI/体脂率、下方三列展示初始体重/BMI、体重变化(含还需减重数值)、目标体重/BMI;右侧"目标承诺"角标
- **每日打卡卡片:** 三色卡片行——吃(今日摄入热量(未打卡显示“今日未行动”)+推荐值+累计天数)、动(今日步数(未运动打卡显示“今日未行动”)+推荐值+累计天数)、体重(今日体重(未体重打卡显示“今日未行动”)+推荐范围+累计天数)
- **减重进度卡片:** 第N天减重(共N天)、当前体重、进度条(渐变填充+跑步小人图标)、初始体重和目标体重标尺
- **吃动平衡卡片(双Tab):** 今日吃动Tab(周视图日历+同心圆环+热量差+摄入/消耗环形图+宏量营养素图例+餐次热量分布)、本月吃动Tab(月历导航+日期圆环+柱状趋势图)
- **体重记录卡片(三Tab):** 体重变化/体脂变化/腰围变化,各含统计行(均值/最高/最低/变化)+折线趋势图含数据点悬浮提示
- **排行榜卡片(左列):**
- hasact 模式:显示活动名称标题、"我的排名"行(第8名/86.6分)、前三名金银铜牌排行榜
- personal 模式:排行榜卡片正常显示但不展示"我的排名"行
- none 模式:排行榜卡片替换为缺省占位样式("排行榜示意图"图标+文字)
- **建议入口卡片(右列):** 膳食建议(跳转膳食建议页)、运动建议(跳转运动建议页)、体重报告(跳转体重报告页)
- **推荐视频区:** 展示健康视频列表(暂时先不做,等后续“知识普及”模块)
**交互说明:**
- 点击"目标承诺"角标跳转到目标承诺页面
- 点击吃/动/体重打卡卡片分别跳转到吃打卡/动打卡/体重打卡页面
- 点击吃动平衡"查看历史"跳转到吃动平衡历史页面
- 点击体重记录"查看历史"跳转到体重记录页面
- 点击"膳食建议"卡片跳转到膳食建议页面
- 点击"运动建议"卡片跳转到运动建议页面
- 点击"体重报告"卡片跳转到体重报告页面
- 点击排行榜卡片跳转到排行榜页面
- 点击返回按钮跳转到应用主页
- 吃动平衡卡片内点击今日吃动/本月吃动Tab切换内容
- 体重记录卡片内点击体重变化/体脂变化/腰围变化Tab切换图表
---
## 三、每日打卡
### 10. 吃打卡(eat-checkin.html
![页面截图](screenshots/eat-checkin.jpg)
**功能说明:** 员工记录每日饮食摄入,支持智能食堂自动记录(智记)和手动录入(自记)两种方式,按早餐/午餐/晚餐/加餐分类记录。
**主要UI元素:**
- 导航栏标题:当前日期
- 今日摘要栏:显示已记录餐数和总摄入热量,右侧"历史记录"链接跳转到摄入记录页面
- 四餐卡片(早餐/午餐/晚餐/加餐):各显示已摄入热量、推荐热量参考、来源徽章(智记/自记/未记录)
- 类型显示当日总记录卡片(智记/自记)
- 自记区域:搜索框、左侧分类列表、右侧菜品列表(含+/-按钮)
- 底部固定栏:购物袋图标(显示已选菜品数量)、总热量、提交按钮
**交互说明:**
- 点击餐卡片选中当前餐次,如果已记录,则列表显示记录详情,如果未记录,则可对当前餐次进行自记提交。
- 点击智记餐品类型卡片,如果尚未记录,点击后提示“未配备或未使用智能营养餐线”文字弹窗;点击自记餐品类型卡片,如未记录,则提示“请选择餐次进行自记”文字弹窗
- 点击菜品右侧"+"按钮打开份量录入弹窗
- 点击购物袋图标打开已选菜品汇总面板(支持移除菜品)
- 点击"提交"按钮提交当日饮食记录,弹出确认弹窗
- 点击"历史记录"链接跳转到摄入记录页面
- 点击返回按钮返回体重管理主页
**业务规则:**
- 智记(智能食堂自动记录)的数据不可手动修改
- 自记需选择菜品并填写份量(克)
- 每餐可添加多个菜品
---
### 11. 动打卡(exercise-checkin.html
![页面截图](screenshots/exercise-checkin.jpg)
**功能说明:** 员工记录每日运动消耗,支持穿戴设备、器械运动、微信运动、自记四种数据来源,按一周日期展示。
**主要UI元素:**
- 导航栏标题:当前周日期范围(如"08-10 ~ 08-16"
- 今日摘要栏:显示当天日期、星期、今日消耗热量,"历史记录"链接跳转到消耗记录页面
- 一周日期卡片(2行,4+3布局):周一至周日,已记录天显示步数/消耗热量+来源徽章(微信/穿戴/器械/自记)、推荐步数,当天高亮
- 来源类型卡片:穿戴设备(步数)、器械运动(已记录时长)、微信运动(步数+同步按钮)、自记运动(已记录时长)
- 自记区域:搜索框、运动分类、运动项目列表(含+/-按钮)
- 底部固定栏:购物袋图标(显示已选运动数)、总消耗热量、提交按钮
**交互说明:**
- 选中日期卡片,如果已记录,则列表显示记录详情,如果未记录,则可对当天进行自记提交。
- 点击运动项目"+"按钮打开时长录入弹窗
- 点击微信运动"同步"按钮跳转到微信小程序进行步数同步操作
- 点击购物袋图标打开已选运动汇总面板
- 点击"提交"按钮提交当日运动记录,弹出确认弹窗
- 点击"历史记录"链接跳转到消耗记录页面
**业务规则:**
- 仅当天可添加运动记录,历史日期只读
- 穿戴设备未记录时,点击提示“您未配备或未连接穿戴设备”弹窗
- 日期卡片中的已记录数据,如果穿戴、自记、微信都有步数,取最大值。如果没有步数,则显示消耗千卡
- 微信运动需手动触发同步
---
### 12. 体重打卡(weight-checkin.html
![页面截图](screenshots/weight-checkin.jpg)
**功能说明:** 员工记录每日体重、体脂、腰围测量数据,支持身高秤、蓝牙秤、体脂仪、自记四种设备来源。
**主要UI元素:**
- 今日摘要栏:显示当天已记录的体重数据,"历史记录"链接跳转到体重记录页面
- 一周日期卡片:已记录天显示体重数据+体脂率+来源徽章(身高秤/蓝牙秤/体脂仪/自记)、推荐范围;未记录天显示"未记录"和推荐范围
- 来源类型卡片:身高秤、蓝牙秤、体脂仪、自记,各显示最近记录值或"暂未记录"
- 自记测量区:体重(含上次记录值和推荐范围)、体脂率(含上次记录值)、腰围(含上次记录值),每项右侧有"+"录入按钮
- 底部固定栏:已选测量项数量徽章、提交按钮
- 数值录入弹窗(底部弹出):含指标图标、参考范围提示、+/-步进按钮、数值输入框、确定按钮
**交互说明:**
- 选中日期卡片,如果已记录,则列表显示记录详情,如果未记录,则可对当天进行自记提交。
- 点击自记测量区各项的"+"按钮打开对应数值录入弹窗
- 在数值录入弹窗中通过+/-按钮或直接输入调整数值,点击"确定"添加
- 点击底部购物袋图标打开已选测量项汇总面板
- 点击"提交"按钮提交当日测量数据
- 点击"历史记录"链接跳转到体重记录页面
- 点击返回按钮返回体重管理主页
**业务规则:**
- 身高秤、蓝牙秤、体脂仪数据由设备自动同步,自记数据手动录入
- 同一日期可记录多个来源的数据,自记数据可覆盖
---
## 四、历史记录
### 13. 体重记录(weight-history.html
![页面截图](screenshots/weight-history.jpg)
**功能说明:** 展示体重/体脂/腰围的历史记录列表,支持周/月/季/活动期间/全部多时间维度筛选,日历视图与列表视图结合。非自记来源的记录可点击跳转到详细报告页。
**主要UI元素:**
- 月历导航(上一月/下一月箭头、月份标签、"展开月历"按钮)
- 周日历(默认视图,7天一行):用不同彩色圆环表示数据来源(体重秤、蓝牙秤、自记、体脂仪),显示当日体重和体脂
- 月历(展开后全月网格)
- 数据来源图例(体重秤/蓝牙秤/自记/体脂仪)
- 时间Tab栏:周/月/季/活动期间/全部
- 记录卡片列表:每条含日期、时间、体重/体脂/腰围、来源徽章;非自记来源的记录右侧显示箭头 `>`,可点击跳转到详细报告页
**交互说明:**
- 点击月历日期显示对应日期的记录列表
- 点击"展开月历"/"收起月历"切换日历视图
- 点击时间Tab切换时间范围,列表同步更新
- 点击非自记来源(体重秤/蓝牙秤/体脂仪)的记录行跳转到详细报告页面
- 自记来源的记录不可点击,无箭头
- 点击返回按钮返回体重管理主页
**业务规则:**
- 空数据时显示"该时间段暂无记录"
---
### 14. 详细报告(report-detail.html
![页面截图](screenshots/report-detail.jpg)
**功能说明:** 展示单次体重测量的完整报告,根据设备来源类型(体重秤/蓝牙秤/体脂仪)动态展示不同的身体成分指标分类和节段数据。
**通用UI元素(三种设备共有):**
- 报告头部(紫色渐变卡片):测量日期时间、设备标签(体重秤/蓝牙秤/体脂仪,各有不同颜色徽章)、当前体重(大字突出)、BMI 状态标签(正常/偏高)、较上次体重变化
- BMI 指示条:浅蓝→绿→橙→红渐变色轨道(BMI<18.5 消瘦 / 18.5≤BMI<24 正常 / 24≤BMI<28 超重 / BMI≥28 肥胖),紫色指针标出当前值位置。等级划分与首页 BMI 说明一致
**各设备类型展示内容:**
#### 体重秤(device=scale
| 分类 | 展示形式 | 包含指标 |
|------|---------|---------|
| 基础指标 | 2列网格 + BMI指示条 | 身高、体重、BMI、基础代谢、体脂肪量、体脂肪率 |
| 水分与蛋白质 | 行列表 | 体水分量、体水分率、蛋白质量、蛋白质率 |
| 肌肉与骨骼 | 行列表 | 肌肉率、肌肉量、体骨骼量/骨量 |
| 脂肪分析 | 行列表 | 皮下脂肪率、内脏脂肪等级、去脂体重 |
| 其他 | 行列表 | 无机盐量/矿物质量、细胞内液、细胞外液、健康评分 |
#### 蓝牙秤(device=bluetooth
| 分类 | 展示形式 | 包含指标 |
|------|---------|---------|
| 基础指标 | 2列网格 + BMI指示条 | 身高、体重、BMI、基础代谢、体脂肪量、体脂肪率 |
| 水分与蛋白质 | 行列表 | 体水分量、体水分率、蛋白质量、蛋白质率 |
| 肌肉与骨骼 | 行列表 | 体肌肉量、体肌肉率、体骨骼量 |
| 脂肪分析 | 行列表 | 皮下脂肪量、皮下脂肪率、除脂肪量 |
| 细胞液 | 行列表 | 细胞外液量、细胞外液率、细胞内液量、细胞内液率 |
| 其他 | 行列表 | 矿物质量 |
#### 体脂仪(device=fat
| 分类 | 展示形式 | 包含指标 |
|------|---------|---------|
| 基础指标 | 2列网格 + BMI指示条 | 身高、体重、BMI、基础代谢、InBody评分 |
| 体脂分析 | 行列表 | 体脂肪量、体脂肪率、内脏脂肪等级、肥胖度、去脂体重 |
| 体成分 | 行列表 | 体水分、蛋白质、无机盐、骨骼肌量、骨骼肌指数 |
| 体重管理 | 行列表 | 目标体重、体重控制、脂肪控制、肌肉控制、腰臀比 |
| 节段脂肪 | 表格(部位×测量值) | 左上肢、右上肢、左下肢、右下肢、躯干 |
| 节段肌肉 | 表格(部位×测量值) | 左上肢、右上肢、左下肢、右下肢、躯干 |
**交互说明:**
- 点击返回按钮返回体重记录页面
- 页面为只读展示,无编辑操作
**业务规则:**
---
### 15. 摄入记录(eat-history.html
![页面截图](screenshots/eat-history.jpg)
**功能说明:** 展示饮食摄入历史记录,支持周/月/季/活动期间/全部多时间维度筛选,日历视图展示每日摄入热量。
**主要UI元素:**
- 月历导航(含"展开月历"按钮)
- 周日历/月历:每天用彩色圆环表示数据来源(智记餐、自记餐、智记+自记餐),显示当日热量
- 数据来源图例(智记餐/自记餐/智记+自记餐)
- 时间Tab栏:周/月/季/活动期间/全部
- 记录卡片列表:每条含餐次名称(早餐/午餐/晚餐/加餐)、日期时间、热量、来源徽章
**交互说明:**
- 点击月历日期显示对应日期的记录
- 点击时间Tab切换时间范围
- 点击返回按钮返回吃打卡页面
**业务规则:**
- 空数据时显示"该时间段暂无记录"
---
### 16. 消耗记录(exercise-history.html
![页面截图](screenshots/exercise-history.jpg)
**功能说明:** 展示运动消耗历史记录,支持周/月/季/活动期间/全部多时间维度筛选。
**主要UI元素:**
- 月历导航(含"展开月历"按钮)
- 周日历/月历:每天用彩色圆环表示数据来源(穿戴、器械、微信、自记、多途径),显示当日消耗热量
- 数据来源图例(穿戴/器械/微信/自记/多途径)
- 时间Tab栏:周/月/季/活动期间/全部
- 记录列表:按日期查看时拆分"当日步数"区域和"运动记录"区域(含运动名称/热量/时长/来源徽章);Tab模式下为平铺列表
**交互说明:**
- 点击月历日期显示对应日期的记录
- 点击时间Tab切换时间范围
- 点击返回按钮返回动打卡页面
**业务规则:**
- 空数据时显示"该时间段暂无记录"
---
### 17. 吃动平衡历史(balance-history.html
![页面截图](screenshots/balance-history.jpg)
**功能说明:** 展示吃动平衡历史数据,通过月历和柱状图呈现每日摄入与消耗的对比趋势。
**主要UI元素:**
- 月历导航(上一月/下一月箭头、月份标签)
- 月历网格(7列):有数据的日期显示双同心圆环图(蓝色外环=摄入、黄色内环=消耗)和净热量差文字
- 柱状趋势图区域:"本月吃动趋势"标题,双色堆叠柱状图(蓝色=摄入、黄色=消耗),每天一组柱子
- 柱状图悬浮提示弹窗:显示日期、摄入详情(热量/碳水/脂肪/蛋白质)、消耗详情(基础代谢/运动消耗/步行消耗)
**交互说明:**
- 点击月历日期自动滚动到对应日期的柱状图并显示提示弹窗
- 点击/悬浮柱状图显示详细营养素和消耗来源提示弹窗
- 点击上一月/下一月切换月份
- 点击返回按钮返回体重管理主页
---
## 五、建议与报告
### 18. 膳食建议(diet-advice.html
![页面截图](screenshots/diet-advice.jpg)
**功能说明:** 为员工提供个性化膳食指导方案,首次进入需完成偏好问卷,之后展示每日热量目标、膳食注意事项、三餐推荐食谱和本周食谱计划。
**页面状态:**
- **引导态(首次进入):** 覆盖全屏的紫色引导问卷
- **内容态(完成引导后):** 完整膳食建议内容
**引导态主要UI元素:**
- Q1:所在地区(必填,省市联动选择器弹窗)
- Q2:口味偏好(必填,单选:清淡/辛辣/咸鲜/酸甜)
- Q3:上班时间(必填:正常白班/早班/晚班/倒班)
- Q4:规避食材(可选,多选:羊肉/海鲜/动物内脏/花生/鸡蛋/牛奶/香菜/辣椒/大蒜/菌菇/其他)
- 提交按钮(必填项未完成时禁用,显示"请先填写必选项";全部完成显示"开始推荐")
**内容态主要UI元素:**
- 导航栏右侧"修改偏好"按钮
- 每日热量目标卡片(绿色渐变):目标总热量、营养素进度条(蛋白质/碳水/脂肪)
- 膳食注意事项卡片(可折叠):低钠、低嘌呤、少食多餐、多饮水等提示
- 今日三餐推荐(4个Tab:早餐/午餐/晚餐/加餐):每餐菜品卡片(图标、名称、份量、热量、标签如"低钠/低嘌呤/当地"、烹饪提示)
- 本周食谱计划(7天Tab切换):每天展示3道推荐菜品
- "保存至本地"按钮
**交互说明:**
- 引导态:完成所有必填项后点击"开始推荐"关闭引导、展示内容
- 点击"修改偏好"重新打开引导问卷,回填已有选择
- 点击菜品卡片打开详情弹窗(含"不喜欢"按钮)
- 点击"不喜欢"弹出二级确认弹窗(含原因快捷选择)
- 点击"膳食注意事项"标题折叠/展开
- 点击三餐Tab切换餐次
- 点击周几Tab切换食谱计划日
- 点击"保存至本地"保存当前膳食方案
- 点击返回按钮返回体重管理主页
**业务规则:**
- 首次进入必须完成偏好问卷才能查看膳食建议
- 偏好数据保存在 localStorage
- 修改偏好后膳食建议方案重新生成
---
### 19. 膳食偏好设置(diet-questionnaire.html
![页面截图](screenshots/diet-questionnaire.jpg)
**功能说明:** 独立的膳食偏好设置页面,供修改偏好时使用(也可从膳食建议页的"修改偏好"入口进入)。
**主要UI元素:**
- 顶部引导语("定制你的专属膳食方案")
- Q1:所在地区(必填,省-市联动选择器弹窗,预置陕西/甘肃/宁夏/内蒙古等长庆油田覆盖区域)
- Q2:口味偏好(必填,多选标签:清淡·原味/鲜香/咸鲜/微辣/麻辣/酸辣/酸甜/酱香·红烧/蒜香·葱香)
- Q3:饮食禁忌(可选):预设标签行(猪肉/牛肉/羊肉等)+ "自定义添加"按钮,打开底部弹窗含快捷选择标签和自由文本输入
- 提交按钮("请先填写必选项"禁用态→"开始推荐"可用态)
**交互说明:**
- 点击地区行打开省市联动底部弹窗
- 点击口味偏好标签切换选中/取消
- 点击"自定义添加"打开饮食禁忌添加弹窗
- 点击提交按钮保存到 localStorage 并跳转到膳食建议页面
- 点击返回按钮返回上一页
- 页面初始化时从 localStorage 读取已有偏好回填
---
### 20. 运动建议(exercise-advice.html
![页面截图](screenshots/exercise-advice.jpg)
**功能说明:** 为员工提供个性化运动指导方案,展示每日消耗目标、运动注意事项、四类运动推荐和每周运动计划。
**主要UI元素:**
- 每日消耗目标卡片(橙色渐变):目标总消耗热量,分解条(基础消耗/运动消耗/步行消耗各占比进度)
- 运动注意事项卡片(可折叠):高血压、高尿酸、热身、补水、循序渐进等8条提示
- 四类运动推荐卡片:有氧运动(30-45分钟/天)、力量训练(20-30分钟/次)、柔韧拉伸(10-15分钟/天)、日常活动(全天累计)
- 每周运动计划(7天Tab栏切换):每天展示3项计划运动(含图标、描述、时长、消耗热量)
- "保存至本地"按钮
**交互说明:**
- 点击"运动注意事项"标题折叠/展开
- 点击周几Tab切换运动计划
- 点击"保存至本地"保存当前运动方案
- 点击返回按钮返回体重管理主页
---
### 21. 体重报告(weight-report.html
![页面截图](screenshots/weight-report.jpg)
**功能说明:** 展示员工的体重管理综合报告,包括减重概览和按时间维度的详细记录。通过右上角三点菜单切换三种业务模式。
**页面状态:** 通过 URL 参数 `?type=hasact|personal|none` 控制。概览卡片在所有模式下均显示,差异在于导航栏标题和内容行:
| UI 元素 | `hasact`(已参加活动) | `personal`(已设置个人目标) | `none`(未参加未设置未组织) |
|---------|----------------------|---------------------------|----------------------|
| 导航栏标题 | "2026年二季度体重管理活动" | "个人减重目标标题" | 空 |
| 概览卡片-进度行(截止日期/累计天数) | ✅ 显示 | ✅ 显示 | ❌ 隐藏 |
| "活动期间"Tab | ✅ 显示 | ✅ 显示 | ❌ 隐藏 |
**主要UI元素:**
- 导航栏标题(随模式变化):"2026年二季度体重管理活动" / "个人减重目标标题" / 空
- 右上角三点菜单(已参加活动/已设置个人目标/未参加未设置未组织)
- 概览卡片(青绿色渐变,所有模式均显示):报告日期、累计天数(N/总天数,none模式下隐藏)、已减体重、减重率百分比
- 时间Tab栏:周/月/季/全部/活动期间(none模式下隐藏"活动期间"Tab
- 记录卡片网格:每条含日期、体重/BMI/体脂/腰围、摄入热量、消耗热量
- 时间Tab栏:周/月/季/全部/活动期间(none模式下隐藏"活动期间"
- 记录卡片网格:每条含日期、体重/BMI/体脂/腰围、摄入热量、消耗热量
**交互说明:**
- 点击三点菜单切换模式
- 点击时间Tab切换时间范围
- 点击返回按钮返回体重管理主页(携带当前 type 参数)
---
### 22. 排行榜(weight-ranking.html
![页面截图](screenshots/weight-ranking.jpg)
**功能说明:** 展示体重管理活动的排行榜,支持按减重量/减重率/综合分/行动积分四个维度排名。
**页面状态:** 通过 URL 参数 `?type=hasact|personal|none` 控制。
**主要UI元素:**
- 活动头部(青绿色渐变横幅):活动名称、参与人数
- 排名Tab:减重量/减重率/综合分/行动积分
- 我的排名卡片(hasact模式):个人排名编号、姓名、部门、积分值
- 排名列表表头:排名/用户/当前Tab对应列名
- 排名列表(15行):前三名金银铜奖牌、当前用户行高亮
- 缺省提示(none模式):"单位暂未组织活动"
**交互说明:**
- 点击排名Tab切换排名维度
- 点击返回按钮返回体重管理主页
- 个人模式下隐藏"我的排名卡片"
**业务规则:**
- 综合分 = 减重量分×35% + 减重率分×20% + ...(详见活动详情页规则)
- 未参加活动(none模式)显示"单位暂未组织活动",列表为空
---
## 表单字段表
### 膳食偏好设置
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 所在地区 | 省-市联动选择 | 是 | 必选省份和城市 |
| 口味偏好 | 多选标签 | 是 | 至少选1项 |
| 上班时间 | 单选 | 是 | 必选1项 |
| 规避食材 | 多选+自定义输入 | 否 | 可多选或自定义 |
### 基本信息编辑字段
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 身高 | 标尺选择(100-250cm | 是 | 步进0.1cm |
| 体力活动 | 单选 | 是 | 五选一 |
### 指标慢病 - 测量录入
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 体重 | 标尺选择(30-200kg | 否 | 步进0.1kg,范围30-200 |
| 收缩压 | 标尺选择(60-260mmHg | 否 | 步进1mmHg |
| 舒张压 | 标尺选择(30-160mmHg | 否 | 步进1mmHg |
| 空腹血糖 | 标尺选择(1.0-33.3mmol/L | 否 | 步进0.01mmol/L |
| 尿酸 | 标尺选择(50-1200mmol/L | 否 | 步进1mmol/L |
| 总胆固醇 | 标尺选择 | 否 | - |
| 甘油三脂 | 标尺选择 | 否 | - |
| 高密度脂蛋白 | 标尺选择 | 否 | - |
| 低密度脂蛋白 | 标尺选择 | 否 | - |
| 慢性疾病确诊信息 | 单选+文本+日期 | 否 | 已确诊时医院名称和年月必填 |
### 目标管理 - 目标设定
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 活动选择 | 底部弹窗单选 | 否(个人模式) | 单位活动模式下必选 |
| 计划名称 | 文本输入 | 否(个人模式) | 可自动生成 |
| 目标体重 | 标尺选择 | 是 | 步进0.1kg |
| 目标体脂率 | 标尺选择 | 是 | 步进0.1% |
| 目标腰围 | 标尺选择 | 是 | 步进0.1cm |
### 体重打卡 - 自记测量
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 体重 | 数值输入+步进 | 否 | 一位小数,范围20-300kg |
| 体脂率 | 数值输入+步进 | 否 | 范围0-60% |
| 腰围 | 数值输入+步进 | 否 | 范围30-200cm |
### 吃打卡 - 菜品录入
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 餐次 | 选择 | 是 | 早餐/午餐/晚餐/加餐 |
| 菜品 | 搜索+分类选择 | 是 | - |
| 份量 | 数值输入 | 是 | 单位:克 |
### 动打卡 - 运动录入
| 字段名 | 类型 | 必填 | 校验规则 |
|--------|------|------|----------|
| 运动项目 | 搜索+分类选择 | 是 | - |
| 时长 | 数值输入 | 是 | 单位:分钟 |
@@ -0,0 +1,185 @@
/**
* 体重管理模块页面截图脚本
* 使用 Puppeteer 批量截取 HTML 原型页面,输出 JPEG(质量 85%)到 doc/screenshots/
*
* 用法: node doc/screenshot.js
*/
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');
const MODULE_DIR = __dirname.replace(/\\doc$/, ''); // weight-management/
const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots');
// 所有需要截图的页面(相对于模块目录)
const PAGES = [
'weight-main.html',
// 每日打卡
'eat-checkin.html',
'exercise-checkin.html',
'weight-checkin.html',
// 历史记录
'report-detail.html',
'weight-history.html',
'eat-history.html',
'exercise-history.html',
'balance-history.html',
// 建议与报告
'diet-advice.html',
'diet-questionnaire.html',
'exercise-advice.html',
'weight-report.html',
'weight-ranking.html',
// 目标承诺
'goal-commitment.html',
'goal-management.html',
'basic-info.html',
'indicators-diseases.html',
'contraindications.html',
'promise-signing.html',
'my-promise.html',
'activity-detail.html',
];
// 特殊页面配置
const PAGE_CONFIGS = {
'promise-signing.html': { viewportWidth: 844, viewportHeight: 390 },
// 膳食建议页需要注入 localStorage 跳过引导遮罩
'diet-advice.html': { injectPrefs: true },
};
async function screenshotPage(browser, htmlFile) {
const page = await browser.newPage();
const filePath = path.join(MODULE_DIR, htmlFile);
const fileUrl = `file:///${filePath.replace(/\\/g, '/')}`;
const config = PAGE_CONFIGS[htmlFile] || {};
const viewportWidth = config.viewportWidth || 390;
const viewportHeight = config.viewportHeight || 844;
await page.setViewport({ width: viewportWidth, height: viewportHeight, deviceScaleFactor: 1 });
try {
// 特殊处理:在页面加载前注入 localStorage
if (config.injectPrefs) {
await page.evaluateOnNewDocument(() => {
localStorage.setItem('dietPreferences', JSON.stringify({
region: '陕西省-西安市',
taste: 'spicy',
workSchedule: 'normal',
avoidFoods: ['羊肉', '海鲜']
}));
});
}
await page.goto(fileUrl, { waitUntil: 'networkidle0', timeout: 15000 });
// 等待页面渲染完成
await new Promise(r => setTimeout(r, 500));
// 注入 CSS:移除 phone-shell 限制,展开全部内容
await page.evaluate(() => {
const style = document.createElement('style');
style.id = 'screenshot-unwrap';
style.textContent = `
/* 移除手机壳固定高度和装饰 */
.phone-shell {
height: auto !important;
min-height: auto !important;
max-height: none !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow: visible !important;
}
/* 滚动区域全部展开 */
.scroll-content,
[class*="scroll"] {
overflow: visible !important;
max-height: none !important;
height: auto !important;
}
/* 页面外层背景改为白色 */
body {
background: #fff !important;
padding: 0 !important;
margin: 0 !important;
}
/* 隐藏 type-menu、type-mask 等弹窗遮挡元素 */
.type-menu, .type-mask, .nav-menu,
.reason-mask, .value-mask, .summary-mask,
.act-mask, .act-sheet, .target-mask, .target-sheet,
.confirm-mask, .reject-mask, .confirm-card, .reject-card,
.mask, .sheet, .height-mask, .height-sheet,
.overlay, .picker-mask, .picker-panel {
display: none !important;
}
/* 底部安全区缩到最小 */
.bottom-safe {
height: 8px !important;
}
`;
document.head.appendChild(style);
});
// 等待样式生效
await new Promise(r => setTimeout(r, 300));
// 获取实际内容高度
const bodyHandle = await page.$('.phone-shell');
const contentHeight = await page.evaluate((el) => {
if (el) return el.scrollHeight;
return document.body.scrollHeight;
}, bodyHandle);
const finalHeight = Math.max(contentHeight || viewportHeight, 400);
await page.setViewport({ width: viewportWidth, height: Math.min(finalHeight + 40, 12000), deviceScaleFactor: 1 });
const outName = htmlFile.replace('.html', '.jpg');
const outPath = path.join(SCREENSHOTS_DIR, outName);
await page.screenshot({
path: outPath,
type: 'jpeg',
quality: 85,
fullPage: true,
});
console.log(`${htmlFile}${outName} (${viewportWidth}×${finalHeight})`);
} catch (err) {
console.error(`${htmlFile}: ${err.message}`);
} finally {
await page.close();
}
}
(async () => {
console.log('体重管理模块 - 页面截图');
console.log(`模块目录: ${MODULE_DIR}`);
console.log(`输出目录: ${SCREENSHOTS_DIR}`);
console.log(`${PAGES.length} 个页面\n`);
// 确保输出目录存在
if (!fs.existsSync(SCREENSHOTS_DIR)) {
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
}
const browser = await puppeteer.launch({
headless: true,
executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
for (let i = 0; i < PAGES.length; i++) {
const pageName = PAGES[i];
console.log(`[${i + 1}/${PAGES.length}] ${pageName}`);
await screenshotPage(browser, pageName);
}
console.log(`\n✅ 全部完成!共 ${PAGES.length} 张截图,保存在 ${SCREENSHOTS_DIR}`);
} catch (err) {
console.error('执行出错:', err);
process.exit(1);
} finally {
await browser.close();
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -694,7 +694,7 @@ function renderExerciseList() {
html += ' <span class="exercise-item__type exercise-item__type--' + r.type + '">' + typeLabel + '</span>';
html += ' </div>';
html += ' </div>';
if (r.type === 'manual') {
if (r.type === 'manual' && selectedDay === TODAY) {
html += ' <button class="exercise-item__delete" onclick="deleteDayRecord(\'' + selectedDay + '\', ' + idx + ')" title="删除">✕</button>';
}
html += '</div>';
@@ -531,7 +531,7 @@ var STATE_CONFIG = {
'unset-hasact':{ mode:'activity', hideDetail:false, hideSelect:false, readOnly:false, selfLock:false, btnMode:'normal', status:'' },
'personal': { mode:'self', hideDetail:true, hideSelect:false, hideActCard:true, showInfoCard:false, readOnly:false, selfLock:true, btnMode:'modify', status:'' },
'wait': { mode:'activity', hideDetail:false, hideSelect:false, hideSelfCard:true, showInfoCard:false, readOnly:true, selfLock:false, btnMode:'none', status:'<span class="gm-status-bar--wait">管理员审核中,请耐心等待...</span>' },
'pass': { mode:'activity', hideDetail:false, hideSelect:false, hideSelfCard:true, showInfoCard:false, readOnly:true, selfLock:false, btnMode:'none', status:'<span class="gm-status-bar--pass">管理员已审核通过,请尽快签订承诺书参加活动</span>' },
'pass': { mode:'activity', hideDetail:false, hideSelect:false, hideSelfCard:true, showInfoCard:false, readOnly:false, selfLock:false, btnMode:'resubmit', status:'<span class="gm-status-bar--pass">管理员已审核通过,请尽快签订承诺书参加活动</span>' },
'reject': { mode:'activity', hideDetail:false, hideSelect:false, showInfoCard:false, hideSelfCard:true, readOnly:false, selfLock:false, btnMode:'resubmit', status:'<span class="gm-status-bar--reject">管理员审核驳回,<a class="gm-status-bar__link" onclick="openRejectModal()">查看原因</a>,请重新调整目标</span>' },
'signed': { mode:'activity', hideDetail:false, hideSelect:false, showInfoCard:false, hideSelfCard:true, readOnly:true, selfLock:false, btnMode:'none', status:'<span class="gm-status-bar--signed">已签署承诺书并参与活动</span>' }
};
-360
View File
@@ -1,360 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>历史数据 - 员工端 | 健康CQ原型</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f0f2f5; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; }
.phone-shell { width: 390px; height: 844px; background: #f5f7fa; border-radius: 44px; box-shadow: 0 20px 60px rgba(0,0,0,0.25), inset 0 0 0 2px #e0e0e0; overflow: hidden; position: relative; display: flex; flex-direction: column; }
.status-bar { height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; background: #fff; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #333; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; }
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; margin-right: 28px; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f7fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* Tab 栏 */
.tab-bar { display: flex; background: #fff; padding: 0 16px; gap: 0; border-bottom: 1px solid #f0f0f0; }
.tab-item { flex: 1; height: 44px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #999; cursor: pointer; position: relative; transition: color 0.2s; }
.tab-item.active { color: #667eea; font-weight: 600; }
.tab-item.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 24px; height: 3px; background: #667eea; border-radius: 2px; }
/* 切换按钮 */
.view-switch { display: flex; align-items: center; justify-content: flex-end; padding: 12px 16px 0; gap: 8px; }
.switch-btn { width: 36px; height: 36px; border: 1px solid #e8e8e8; background: #fff; border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.15s; }
.switch-btn.active { background: #667eea; border-color: #667eea; }
.switch-btn.active svg path, .switch-btn.active svg rect, .switch-btn.active svg line { stroke: #fff; }
/* 统计卡片 */
.stats-row { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 8px; padding: 12px 16px; }
.stats-item { background: #fff; border-radius: 10px; padding: 12px 8px; text-align: center; }
.stats-item__label { font-size: 11px; color: #999; margin-bottom: 4px; }
.stats-item__value { font-size: 16px; font-weight: 700; color: #1a1a1a; }
.stats-item__unit { font-size: 10px; color: #999; margin-left: 1px; }
.stats-item--change .stats-item__value { color: #52c41a; }
/* 趋势图区域 */
.chart-area { margin: 0 16px 12px; background: #fff; border-radius: 16px; padding: 16px; }
.chart-title { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px; }
.chart-wrapper { position: relative; height: 200px; }
.chart-y-axis { position: absolute; left: 0; top: 0; bottom: 20px; width: 36px; display: flex; flex-direction: column; justify-content: space-between; }
.chart-y-label { font-size: 10px; color: #999; text-align: right; }
.chart-grid { position: absolute; left: 40px; right: 0; top: 0; bottom: 20px; }
.chart-grid-line { position: absolute; left: 0; right: 0; height: 1px; background: #f0f0f0; }
.chart-line { position: absolute; left: 40px; right: 0; top: 0; bottom: 20px; }
.chart-svg { width: 100%; height: 100%; }
.chart-x-axis { position: absolute; left: 40px; right: 0; bottom: 0; height: 20px; display: flex; justify-content: space-between; }
.chart-x-label { font-size: 10px; color: #999; text-align: center; }
/* 趋势图节点 */
.chart-dot { cursor: pointer; transition: r 0.15s; }
.chart-dot:hover { r: 6; }
/* Tip 弹窗 */
.chart-tip { position: absolute; background: #fff; border-radius: 10px; padding: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); border: 1px solid #f0f0f0; z-index: 10; min-width: 150px; display: none; }
.chart-tip.show { display: block; }
.chart-tip__title { font-size: 12px; color: #999; margin-bottom: 8px; }
.chart-tip__row { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; }
.chart-tip__label { font-size: 12px; color: #666; }
.chart-tip__value { font-size: 13px; font-weight: 600; color: #1a1a1a; }
.chart-tip__close { position: absolute; top: 6px; right: 8px; font-size: 14px; color: #ccc; cursor: pointer; line-height: 1; }
/* 列表模式 */
.list-area { margin: 0 16px 12px; }
.list-item { background: #fff; border-radius: 12px; padding: 14px 16px; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; transition: transform 0.15s; text-decoration: none; }
.list-item:active { transform: scale(0.98); }
.list-item__left { flex: 1; min-width: 0; }
.list-item__time { font-size: 14px; font-weight: 500; color: #1a1a1a; margin-bottom: 4px; }
.list-item__metrics { display: flex; gap: 12px; }
.list-item__metric { font-size: 12px; color: #999; }
.list-item__metric strong { color: #1a1a1a; font-weight: 600; }
.list-item__top { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.list-item__time { margin-bottom: 0; }
.device-tag { display: inline-flex; align-items: center; font-size: 10px; padding: 1px 6px; border-radius: 4px; font-weight: 500; flex-shrink: 0; }
.device-tag--scale { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
.device-tag--bluetooth { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.device-tag--fat { background: #fff7e6; color: #fa8c16; border: 1px solid #ffd591; }
.list-item__arrow { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
/* 底部安全区 */
.bottom-safe { height: 20px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<span class="icons">
<svg width="16" height="16" fill="none"><rect x="1" y="4" width="3" height="8" rx="1" fill="#333"/><rect x="5" y="3" width="3" height="9" rx="1" fill="#333"/><rect x="9" y="1" width="3" height="11" rx="1" fill="#333"/><rect x="13" y="0" width="3" height="12" rx="1" fill="#333" opacity=".3"/></svg>
<svg width="16" height="16" fill="none"><path d="M8 3C5.5 3 3.2 4 1.5 5.5l1.4 1.4C4.3 5.5 6.1 5 8 5s3.7.5 5.1 1.9l1.4-1.4C12.8 4 10.5 3 8 3zm0 4c-1.4 0-2.6.5-3.5 1.3L6 9.8c.6-.5 1.3-.8 2-.8s1.4.3 2 .8l1.5-1.5C10.6 7.5 9.4 7 8 7zm0 4c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1z" fill="#333"/></svg>
<svg width="22" height="12" fill="none"><rect x=".5" y=".5" width="19" height="11" rx="2" stroke="#333"/><rect x="20" y="3.5" width="1.5" height="5" rx=".75" fill="#333"/><rect x="2" y="2" width="14" height="8" rx="1" fill="#333"/></svg>
</span>
</div>
<div class="nav-bar">
<div class="nav-back" onclick="location.href='index.html'">
<svg width="12" height="20" fill="none"><path d="M10 2L2 10l8 8" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="nav-title">历史数据</div>
</div>
<!-- Tab -->
<div class="tab-bar">
<div class="tab-item active" onclick="switchTab(this)">近7天</div>
<div class="tab-item" onclick="switchTab(this)">近30天</div>
<div class="tab-item" onclick="switchTab(this)">近3个月</div>
</div>
<div class="scroll-content">
<!-- 切换按钮 -->
<div class="view-switch">
<div class="switch-btn active" id="btnChart" onclick="switchView('chart')">
<svg width="18" height="18" fill="none"><path d="M2 14l4-5 3 3 5-7" stroke="#666" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="switch-btn" id="btnList" onclick="switchView('list')">
<svg width="18" height="18" fill="none"><line x1="3" y1="5" x2="15" y2="5" stroke="#666" stroke-width="1.8" stroke-linecap="round"/><line x1="3" y1="9" x2="15" y2="9" stroke="#666" stroke-width="1.8" stroke-linecap="round"/><line x1="3" y1="13" x2="15" y2="13" stroke="#666" stroke-width="1.8" stroke-linecap="round"/></svg>
</div>
</div>
<!-- 统计 -->
<div class="stats-row">
<div class="stats-item">
<div class="stats-item__label">最高</div>
<div class="stats-item__value">73.2<span class="stats-item__unit">kg</span></div>
</div>
<div class="stats-item">
<div class="stats-item__label">最低</div>
<div class="stats-item__value">71.8<span class="stats-item__unit">kg</span></div>
</div>
<div class="stats-item">
<div class="stats-item__label">平均</div>
<div class="stats-item__value">72.4<span class="stats-item__unit">kg</span></div>
</div>
<div class="stats-item stats-item--change">
<div class="stats-item__label">变化</div>
<div class="stats-item__value">-0.7<span class="stats-item__unit">kg</span></div>
</div>
</div>
<!-- 趋势图 -->
<div class="chart-area" id="chartArea">
<div class="chart-title">体重趋势</div>
<div class="chart-wrapper" id="chartWrapper">
<div class="chart-y-axis">
<span class="chart-y-label">74</span>
<span class="chart-y-label">73</span>
<span class="chart-y-label">72</span>
<span class="chart-y-label">71</span>
</div>
<div class="chart-grid">
<div class="chart-grid-line" style="top:0%"></div>
<div class="chart-grid-line" style="top:33.3%"></div>
<div class="chart-grid-line" style="top:66.6%"></div>
<div class="chart-grid-line" style="top:100%"></div>
</div>
<div class="chart-line">
<svg class="chart-svg" viewBox="0 0 300 160" preserveAspectRatio="none">
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#667eea" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#667eea" stop-opacity="0.02"/>
</linearGradient>
</defs>
<!-- 面积 -->
<polygon points="0,80 50,53 100,107 150,40 200,67 250,27 300,53 300,160 0,160" fill="url(#areaGrad)"/>
<!-- 线条 -->
<polyline points="0,80 50,53 100,107 150,40 200,67 250,27 300,53" fill="none" stroke="#667eea" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<!-- 节点 -->
<circle class="chart-dot" cx="0" cy="80" r="4" fill="#667eea" stroke="#fff" stroke-width="2" onclick="showTip(event,0)"/>
<circle class="chart-dot" cx="50" cy="53" r="4" fill="#667eea" stroke="#fff" stroke-width="2" onclick="showTip(event,1)"/>
<circle class="chart-dot" cx="100" cy="107" r="4" fill="#667eea" stroke="#fff" stroke-width="2" onclick="showTip(event,2)"/>
<circle class="chart-dot" cx="150" cy="40" r="4" fill="#667eea" stroke="#fff" stroke-width="2" onclick="showTip(event,3)"/>
<circle class="chart-dot" cx="200" cy="67" r="4" fill="#667eea" stroke="#fff" stroke-width="2" onclick="showTip(event,4)"/>
<circle class="chart-dot" cx="250" cy="27" r="4" fill="#667eea" stroke="#fff" stroke-width="2" onclick="showTip(event,5)"/>
<circle class="chart-dot" cx="300" cy="53" r="4" fill="#667eea" stroke="#fff" stroke-width="2" onclick="showTip(event,6)"/>
</svg>
</div>
<div class="chart-x-axis">
<span class="chart-x-label">5/8</span>
<span class="chart-x-label">5/9</span>
<span class="chart-x-label">5/10</span>
<span class="chart-x-label">5/11</span>
<span class="chart-x-label">5/12</span>
<span class="chart-x-label">5/13</span>
<span class="chart-x-label">5/14</span>
</div>
<!-- Tip -->
<div class="chart-tip" id="chartTip">
<span class="chart-tip__close" onclick="hideTip()">&times;</span>
<div class="chart-tip__title" id="tipDate"></div>
<div class="chart-tip__row"><span class="chart-tip__label">设备来源</span><span class="chart-tip__value" id="tipDevice"></span></div>
<div class="chart-tip__row"><span class="chart-tip__label">体重</span><span class="chart-tip__value" id="tipWeight"></span></div>
<div class="chart-tip__row"><span class="chart-tip__label">BMI</span><span class="chart-tip__value" id="tipBmi"></span></div>
<div class="chart-tip__row"><span class="chart-tip__label">体脂率</span><span class="chart-tip__value" id="tipFat"></span></div>
</div>
</div>
</div>
<!-- 列表模式 -->
<div class="list-area" id="listArea" style="display:none">
<a class="list-item" href="report-detail.html?device=fat">
<div class="list-item__left">
<div class="list-item__top">
<div class="list-item__time">2026-05-14 08:30</div>
<span class="device-tag device-tag--fat">体脂仪</span>
</div>
<div class="list-item__metrics">
<span class="list-item__metric">体重 <strong>72.5kg</strong></span>
<span class="list-item__metric">BMI <strong>23.7</strong></span>
<span class="list-item__metric">体脂率 <strong>18.5%</strong></span>
</div>
</div>
<div class="list-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
</a>
<a class="list-item" href="report-detail.html?device=bluetooth">
<div class="list-item__left">
<div class="list-item__top">
<div class="list-item__time">2026-05-13 07:55</div>
<span class="device-tag device-tag--bluetooth">蓝牙秤</span>
</div>
<div class="list-item__metrics">
<span class="list-item__metric">体重 <strong>72.8kg</strong></span>
<span class="list-item__metric">BMI <strong>23.8</strong></span>
<span class="list-item__metric">体脂率 <strong>18.8%</strong></span>
</div>
</div>
<div class="list-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
</a>
<a class="list-item" href="report-detail.html?device=scale">
<div class="list-item__left">
<div class="list-item__top">
<div class="list-item__time">2026-05-12 08:10</div>
<span class="device-tag device-tag--scale">体重秤</span>
</div>
<div class="list-item__metrics">
<span class="list-item__metric">体重 <strong>71.8kg</strong></span>
<span class="list-item__metric">BMI <strong>23.4</strong></span>
<span class="list-item__metric">体脂率 <strong>17.9%</strong></span>
</div>
</div>
<div class="list-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
</a>
<a class="list-item" href="report-detail.html?device=fat">
<div class="list-item__left">
<div class="list-item__top">
<div class="list-item__time">2026-05-11 09:20</div>
<span class="device-tag device-tag--fat">体脂仪</span>
</div>
<div class="list-item__metrics">
<span class="list-item__metric">体重 <strong>73.2kg</strong></span>
<span class="list-item__metric">BMI <strong>23.9</strong></span>
<span class="list-item__metric">体脂率 <strong>19.1%</strong></span>
</div>
</div>
<div class="list-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
</a>
<a class="list-item" href="report-detail.html?device=bluetooth">
<div class="list-item__left">
<div class="list-item__top">
<div class="list-item__time">2026-05-10 08:40</div>
<span class="device-tag device-tag--bluetooth">蓝牙秤</span>
</div>
<div class="list-item__metrics">
<span class="list-item__metric">体重 <strong>72.3kg</strong></span>
<span class="list-item__metric">BMI <strong>23.6</strong></span>
<span class="list-item__metric">体脂率 <strong>18.3%</strong></span>
</div>
</div>
<div class="list-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
</a>
<a class="list-item" href="report-detail.html?device=scale">
<div class="list-item__left">
<div class="list-item__top">
<div class="list-item__time">2026-05-09 07:30</div>
<span class="device-tag device-tag--scale">体重秤</span>
</div>
<div class="list-item__metrics">
<span class="list-item__metric">体重 <strong>72.9kg</strong></span>
<span class="list-item__metric">BMI <strong>23.8</strong></span>
<span class="list-item__metric">体脂率 <strong>18.6%</strong></span>
</div>
</div>
<div class="list-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
</a>
<a class="list-item" href="report-detail.html?device=fat">
<div class="list-item__left">
<div class="list-item__top">
<div class="list-item__time">2026-05-08 08:15</div>
<span class="device-tag device-tag--fat">体脂仪</span>
</div>
<div class="list-item__metrics">
<span class="list-item__metric">体重 <strong>72.5kg</strong></span>
<span class="list-item__metric">BMI <strong>23.7</strong></span>
<span class="list-item__metric">体脂率 <strong>18.5%</strong></span>
</div>
</div>
<div class="list-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
</a>
</div>
<div class="bottom-safe"></div>
</div>
</div>
<script>
/* Tab 切换 */
function switchTab(el) {
var tabs = el.parentElement.querySelectorAll('.tab-item');
tabs.forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
}
/* 图表/列表切换 */
function switchView(mode) {
var chartArea = document.getElementById('chartArea');
var listArea = document.getElementById('listArea');
var btnChart = document.getElementById('btnChart');
var btnList = document.getElementById('btnList');
if (mode === 'chart') {
chartArea.style.display = 'block';
listArea.style.display = 'none';
btnChart.classList.add('active');
btnList.classList.remove('active');
} else {
chartArea.style.display = 'none';
listArea.style.display = 'block';
btnChart.classList.remove('active');
btnList.classList.add('active');
}
hideTip();
}
/* Tip 数据 */
var tipData = [
{ date:'2026-05-08', device:'体脂仪', weight:'72.5kg', bmi:'23.7', fat:'18.5%' },
{ date:'2026-05-09', device:'体重秤', weight:'72.9kg', bmi:'23.8', fat:'18.6%' },
{ date:'2026-05-10', device:'蓝牙秤', weight:'71.8kg', bmi:'23.4', fat:'17.9%' },
{ date:'2026-05-11', device:'体脂仪', weight:'73.2kg', bmi:'23.9', fat:'19.1%' },
{ date:'2026-05-12', device:'体重秤', weight:'72.3kg', bmi:'23.6', fat:'18.3%' },
{ date:'2026-05-13', device:'蓝牙秤', weight:'72.8kg', bmi:'23.8', fat:'18.8%' },
{ date:'2026-05-14', device:'体脂仪', weight:'72.5kg', bmi:'23.7', fat:'18.5%' }
];
function showTip(e, idx) {
e.stopPropagation();
var tip = document.getElementById('chartTip');
var d = tipData[idx];
document.getElementById('tipDate').textContent = d.date;
document.getElementById('tipDevice').textContent = d.device;
document.getElementById('tipWeight').textContent = d.weight;
document.getElementById('tipBmi').textContent = d.bmi;
document.getElementById('tipFat').textContent = d.fat;
/* 定位 */
var wrapper = document.getElementById('chartWrapper');
var rect = wrapper.getBoundingClientRect();
var dotRect = e.target.getBoundingClientRect();
var left = dotRect.left - rect.left - 60;
var top = dotRect.top - rect.top + 16;
if (left < 0) left = 4;
if (left > rect.width - 160) left = rect.width - 164;
tip.style.left = left + 'px';
tip.style.top = top + 'px';
tip.classList.add('show');
}
function hideTip() {
document.getElementById('chartTip').classList.remove('show');
}
/* 点击空白关闭 */
document.addEventListener('click', function(e) {
if (!e.target.closest('.chart-tip') && !e.target.closest('.chart-dot')) {
hideTip();
}
});
</script>
</body>
</html>
-192
View File
@@ -1,192 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>体重监测 - 员工端 | 健康CQ原型</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f0f2f5; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; }
.phone-shell { width: 390px; height: 844px; background: #f5f7fa; border-radius: 44px; box-shadow: 0 20px 60px rgba(0,0,0,0.25), inset 0 0 0 2px #e0e0e0; overflow: hidden; position: relative; display: flex; flex-direction: column; }
.status-bar { height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; background: #fff; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #333; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; }
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; margin-right: 28px; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f7fa; padding: 12px; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 最近测量卡片 */
.measure-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; padding: 20px; color: #fff; position: relative; overflow: hidden; margin-bottom: 12px; }
.measure-card::after { content: ''; position: absolute; top: -40px; right: -40px; width: 140px; height: 140px; background: rgba(255,255,255,0.08); border-radius: 50%; }
.measure-card__label { font-size: 12px; opacity: 0.8; margin-bottom: 4px; }
.measure-card__time { font-size: 13px; opacity: 0.9; margin-bottom: 16px; display: flex; align-items: center; gap: 6px; }
.measure-card__time svg { opacity: 0.7; }
.measure-card__main { display: flex; align-items: flex-end; gap: 16px; margin-bottom: 16px; }
.measure-card__weight { display: flex; align-items: baseline; gap: 4px; }
.measure-card__weight-num { font-size: 48px; font-weight: 800; line-height: 1; }
.measure-card__weight-unit { font-size: 16px; opacity: 0.8; }
.measure-card__change { display: flex; align-items: center; gap: 4px; font-size: 13px; padding: 4px 10px; border-radius: 20px; background: rgba(255,255,255,0.15); }
.measure-card__change--down { color: #a5f3c4; }
.measure-card__change--up { color: #fca5a5; }
.measure-card__stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.stat-item { background: rgba(255,255,255,0.12); border-radius: 10px; padding: 12px; }
.stat-item__label { font-size: 11px; opacity: 0.7; margin-bottom: 4px; }
.stat-item__value { font-size: 18px; font-weight: 700; }
.stat-item__unit { font-size: 11px; opacity: 0.7; margin-left: 2px; }
/* 详细信息 */
.detail-section { background: #fff; border-radius: 16px; padding: 16px; margin-bottom: 12px; }
.detail-section__title { font-size: 15px; font-weight: 700; color: #1a1a1a; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
.detail-section__title::before { content: ''; width: 3px; height: 16px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 2px; }
.detail-grid { display: flex; flex-direction: column; gap: 0; }
.detail-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f5f5f5; }
.detail-row:last-child { border-bottom: none; }
.detail-row__left { display: flex; align-items: center; gap: 10px; }
.detail-row__icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.detail-row__icon--fat { background: #fff3e0; }
.detail-row__icon--fat-mass { background: #fce4ec; }
.detail-row__icon--bmr { background: #e8f5e9; }
.detail-row__info { display: flex; flex-direction: column; }
.detail-row__label { font-size: 14px; color: #1a1a1a; font-weight: 500; }
.detail-row__sub { font-size: 11px; color: #999; margin-top: 2px; }
.detail-row__right { display: flex; align-items: baseline; gap: 2px; }
.detail-row__value { font-size: 18px; font-weight: 700; color: #1a1a1a; }
.detail-row__unit { font-size: 12px; color: #999; }
/* 查看历史按钮 */
.btn-history { width: 100%; height: 48px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 24px; font-size: 16px; font-weight: 600; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: transform 0.15s; margin-bottom: 12px; }
.btn-history:active { transform: scale(0.97); }
/* 健康建议 */
.advice-card { background: #fff; border-radius: 16px; padding: 16px; margin-bottom: 12px; }
.advice-card__title { font-size: 15px; font-weight: 700; color: #1a1a1a; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.advice-card__title::before { content: ''; width: 3px; height: 16px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 2px; }
.advice-item { display: flex; gap: 10px; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }
.advice-item:last-child { border-bottom: none; }
.advice-item__icon { font-size: 20px; flex-shrink: 0; line-height: 1.4; }
.advice-item__text { font-size: 13px; color: #666; line-height: 1.6; }
.advice-item__text strong { color: #1a1a1a; font-weight: 600; }
/* 底部安全区 */
.bottom-safe { height: 20px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<span class="icons">
<svg width="16" height="16" fill="none"><rect x="1" y="4" width="3" height="8" rx="1" fill="#333"/><rect x="5" y="3" width="3" height="9" rx="1" fill="#333"/><rect x="9" y="1" width="3" height="11" rx="1" fill="#333"/><rect x="13" y="0" width="3" height="12" rx="1" fill="#333" opacity=".3"/></svg>
<svg width="16" height="16" fill="none"><path d="M8 3C5.5 3 3.2 4 1.5 5.5l1.4 1.4C4.3 5.5 6.1 5 8 5s3.7.5 5.1 1.9l1.4-1.4C12.8 4 10.5 3 8 3zm0 4c-1.4 0-2.6.5-3.5 1.3L6 9.8c.6-.5 1.3-.8 2-.8s1.4.3 2 .8l1.5-1.5C10.6 7.5 9.4 7 8 7zm0 4c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1z" fill="#333"/></svg>
<svg width="22" height="12" fill="none"><rect x=".5" y=".5" width="19" height="11" rx="2" stroke="#333"/><rect x="20" y="3.5" width="1.5" height="5" rx=".75" fill="#333"/><rect x="2" y="2" width="14" height="8" rx="1" fill="#333"/></svg>
</span>
</div>
<div class="nav-bar">
<div class="nav-back" onclick="location.href='../home/index.html'">
<svg width="12" height="20" fill="none"><path d="M10 2L2 10l8 8" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="nav-title">体重监测</div>
</div>
<div class="scroll-content">
<!-- 最近测量 -->
<div class="measure-card">
<div class="measure-card__label">最近一次测量</div>
<div class="measure-card__time">
<svg width="14" height="14" fill="none"><circle cx="7" cy="7" r="6" stroke="#fff" stroke-width="1.5"/><path d="M7 4v3.5l2.5 1.5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
2026-05-14 08:30
</div>
<div class="measure-card__main">
<div class="measure-card__weight">
<span class="measure-card__weight-num">72.5</span>
<span class="measure-card__weight-unit">kg</span>
</div>
<div class="measure-card__change measure-card__change--down">
<svg width="12" height="12" fill="none"><path d="M6 2v8M3 7l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
较上次 -0.3kg
</div>
</div>
<div class="measure-card__stats">
<div class="stat-item">
<div class="stat-item__label">身高</div>
<div class="stat-item__value">175<span class="stat-item__unit">cm</span></div>
</div>
<div class="stat-item">
<div class="stat-item__label">BMI</div>
<div class="stat-item__value">23.7<span class="stat-item__unit">正常</span></div>
</div>
</div>
</div>
<!-- 详细信息 -->
<div class="detail-section">
<!-- <div class="detail-section__title">详细信息</div>-->
<div class="detail-grid">
<div class="detail-row">
<div class="detail-row__left">
<div class="detail-row__icon detail-row__icon--fat">
<svg width="20" height="20" fill="none"><circle cx="10" cy="10" r="8" stroke="#f57c00" stroke-width="2"/><path d="M10 6v4M10 12.5v.5" stroke="#f57c00" stroke-width="2" stroke-linecap="round"/></svg>
</div>
<div class="detail-row__info">
<div class="detail-row__label">体脂率</div>
<div class="detail-row__sub">正常范围 10-20%</div>
</div>
</div>
<div class="detail-row__right">
<span class="detail-row__value">18.5</span>
<span class="detail-row__unit">%</span>
</div>
</div>
<div class="detail-row">
<div class="detail-row__left">
<div class="detail-row__icon detail-row__icon--fat-mass">
<svg width="20" height="20" fill="none"><rect x="3" y="6" width="14" height="10" rx="2" stroke="#e91e63" stroke-width="1.8"/><path d="M7 6V4a3 3 0 0 1 6 0v2" stroke="#e91e63" stroke-width="1.8" stroke-linecap="round"/></svg>
</div>
<div class="detail-row__info">
<div class="detail-row__label">体脂肪量</div>
<div class="detail-row__sub">正常范围 8-15kg</div>
</div>
</div>
<div class="detail-row__right">
<span class="detail-row__value">13.4</span>
<span class="detail-row__unit">kg</span>
</div>
</div>
<div class="detail-row">
<div class="detail-row__left">
<div class="detail-row__icon detail-row__icon--bmr">
<svg width="20" height="20" fill="none"><path d="M4 14l3-4 3 2 3-5 3 3" stroke="#4caf50" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="detail-row__info">
<div class="detail-row__label">基础代谢</div>
<div class="detail-row__sub">每日静息消耗</div>
</div>
</div>
<div class="detail-row__right">
<span class="detail-row__value">1,685</span>
<span class="detail-row__unit">kcal</span>
</div>
</div>
</div>
</div>
<!-- 查看历史 -->
<button class="btn-history" onclick="location.href='history.html'">
<svg width="18" height="18" fill="none"><path d="M9 1a8 8 0 1 0 0 16A8 8 0 0 0 9 1z" stroke="#fff" stroke-width="1.8"/><path d="M9 4.5V9l3 2" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
查看历史数据
</button>
<!-- 健康建议 -->
<div class="advice-card">
<div class="advice-card__title">健康建议</div>
<div class="advice-item">
<span class="advice-item__icon">💪</span>
<span class="advice-item__text"><strong>BMI 正常</strong>,您的体重在健康范围内,继续保持规律运动和均衡饮食。</span>
</div>
<div class="advice-item">
<span class="advice-item__icon">🥗</span>
<span class="advice-item__text"><strong>体脂率达标</strong>,建议每周进行3-5次有氧运动,每次30分钟以上。</span>
</div>
<div class="advice-item">
<span class="advice-item__icon">💧</span>
<span class="advice-item__text">每日饮水量建议 <strong>2000-2500ml</strong>,有助于维持基础代谢水平。</span>
</div>
</div>
<div class="bottom-safe"></div>
</div>
</div>
</body>
</html>
@@ -55,7 +55,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.data-row__unit { font-size: 12px; color: #999; }
/* BMI 指示条 */
.bmi-bar { margin-top: 12px; }
.bmi-bar__track { height: 8px; border-radius: 4px; background: linear-gradient(90deg, #52c41a 0%, #52c41a 35%, #fadb14 35%, #fadb14 55%, #fa8c16 55%, #fa8c16 75%, #ff4d4f 75%, #ff4d4f 100%); position: relative; }
.bmi-bar__track { height: 8px; border-radius: 4px; background: linear-gradient(90deg, #69c0ff 0%, #69c0ff 17.5%, #52c41a 17.5%, #52c41a 45%, #faad14 45%, #faad14 65%, #ff4d4f 65%, #ff4d4f 100%); position: relative; }
.bmi-bar__pointer { position: absolute; top: -4px; width: 16px; height: 16px; background: #fff; border: 3px solid #667eea; border-radius: 50%; transform: translateX(-50%); box-shadow: 0 2px 6px rgba(0,0,0,0.15); }
.bmi-bar__labels { display: flex; justify-content: space-between; margin-top: 6px; font-size: 10px; color: #999; }
/* 节段数据表格 */
@@ -78,7 +78,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</span>
</div>
<div class="nav-bar">
<div class="nav-back" onclick="location.href='history.html'">
<div class="nav-back" onclick="goBack()">
<svg width="12" height="20" fill="none"><path d="M10 2L2 10l8 8" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="nav-title">详细报告</div>
@@ -112,6 +112,11 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<script>
var device = new URLSearchParams(location.search).get('device') || 'fat';
/* 返回来源页面 */
function goBack() {
location.href = 'weight-history.html';
}
/* 头部设备标签 */
var tagEl = document.getElementById('headerDeviceTag');
var tagMap = { scale:['体重秤','device-tag--scale'], bluetooth:['蓝牙秤','device-tag--bluetooth'], fat:['体脂仪','device-tag--fat'] };
@@ -254,8 +259,8 @@ sections.forEach(function(sec) {
});
html += '</div>';
if (sec.bmi) {
html += '<div class="bmi-bar"><div class="bmi-bar__track"><div class="bmi-bar__pointer" style="left:48%"></div></div>';
html += '<div class="bmi-bar__labels"><span>偏瘦 &lt;18.5</span><span>正常 18.5-24</span><span>偏胖 24-28</span><span>肥胖 &ge;28</span></div></div>';
html += '<div class="bmi-bar"><div class="bmi-bar__track"><div class="bmi-bar__pointer" style="left:43.5%"></div></div>';
html += '<div class="bmi-bar__labels"><span>BMI&lt;18.5 消瘦</span><span>18.5≤BMI&lt;24 正常</span><span>24≤BMI&lt;28 超重</span><span>BMI≥28 肥胖</span></div></div>';
}
} else if (sec.type === 'rows') {
sec.fields.forEach(function(f, i) {
@@ -73,6 +73,29 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.type-card__val em { font-style: normal; font-weight: 700; color: #1a1a1a; }
.type-card__empty { font-size: 10px; color: #bbb; }
/* ===== 记录详情只读展示(历史日期) ===== */
.record-detail { margin: 0 12px 12px; display: none; }
.record-detail.show { display: block; }
.record-detail__title { font-size: 15px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; padding-left: 2px; }
.record-detail__list { background: #fff; border-radius: 12px; overflow: hidden; }
.record-detail__item { display: flex; align-items: center; padding: 14px 12px; border-bottom: 1px solid #f5f5f5; gap: 10px; }
.record-detail__item:last-child { border-bottom: none; }
.record-detail__icon { width: 44px; height: 44px; border-radius: 10px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 22px; }
.record-detail__icon--weight { background: #f0e6ff; }
.record-detail__icon--fat { background: #fff0f6; }
.record-detail__icon--waist { background: #e6f7ff; }
.record-detail__info { flex: 1; min-width: 0; }
.record-detail__name { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 4px; }
.record-detail__val { font-size: 18px; font-weight: 800; color: #8b6cff; }
.record-detail__val-unit { font-size: 12px; color: #999; margin-left: 2px; }
.record-detail__source { margin-left: auto; }
.record-detail__source-badge { display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
.record-detail__source-badge--height { background: #e6f7ff; color: #1890ff; }
.record-detail__source-badge--bluetooth { background: #f6ffed; color: #52c41a; }
.record-detail__source-badge--fat { background: #fff0f6; color: #eb2f96; }
.record-detail__source-badge--manual { background: #fff7e6; color: #fa8c16; }
.record-detail__empty { padding: 24px; text-align: center; color: #bbb; font-size: 13px; }
/* ===== 自记测量选择区 ===== */
.self-record { margin: 0 12px 12px; }
.self-record__title { font-size: 15px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; padding-left: 2px; }
@@ -369,6 +392,12 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
</div>
<!-- 记录详情只读展示(历史日期已记录时显示) -->
<div class="record-detail" id="recordDetail">
<div class="record-detail__title">记录详情</div>
<div class="record-detail__list" id="recordDetailList"></div>
</div>
<!-- 自记测量选择区 -->
<div class="self-record" id="selfRecordSection">
<div class="self-record__title">选择指标进行自记</div>
@@ -523,7 +552,7 @@ var TODAY = 'wed';
/* 各天已记录数据 */
var dayRecords = {
mon: { weight: { val: 62.8, type: 'bluetooth' } },
mon: { weight: { val: 62.8, type: 'bluetooth' }, fatVal: 20.3, waistVal: 77.5 },
tue: { weight: { val: 62.7, type: 'fat', fatVal: 20.2, waistVal: 77.0 } },
wed: { weight: { val: 62.6, type: 'height', fatVal: 20.0, waistVal: 76.5 } },
thu: {},
@@ -549,10 +578,12 @@ var CURRENT_STEP = 0.1;
/* ===== 日期选择 ===== */
/* 选中/取消选中日期 */
/* [规则] 只有当天可以操作(选中+自记),非当天只能查看,不可点击切换 */
/* [规则] 当天可选并可编辑;有记录的历史日期可选但仅查看;无记录的历史日期不可选 */
function selectDay(day) {
// 仅当天可交互,其他日期无任何响应
if (day !== TODAY) return;
var hasRecords = dayRecords[day] && dayRecords[day].weight;
// 无记录的历史日期:不可选
if (day !== TODAY && !hasRecords) return;
if (selectedDay === day) {
selectedDay = null;
@@ -560,22 +591,38 @@ function selectDay(day) {
selectedDay = day;
}
// 更新卡片选中态(仅当天卡片可选中)
// 更新卡片选中态
document.querySelectorAll('.day-card').forEach(function(card) {
card.classList.toggle('day-card--selected', card.dataset.day === selectedDay);
});
// 根据是否选中切换操作区显隐
toggleSelfRecordBody();
// 根据是否当日和有记录切换操作区显隐
toggleViewMode();
}
/* 切换底部栏和操作区:仅当日可自记 */
function toggleSelfRecordBody() {
/* 切换视图模式:当天=编辑模式,历史日期=只读模式 */
function toggleViewMode() {
var isToday = selectedDay === TODAY;
var selfRecord = document.getElementById('selfRecordSection');
var recordDetail = document.getElementById('recordDetail');
var bottomBar = document.getElementById('bottomBar');
if (isToday) {
// 当天:显示编辑区和底部栏
selfRecord.style.display = '';
recordDetail.classList.remove('show');
bottomBar.style.display = '';
updateBottomBar();
} else if (selectedDay && dayRecords[selectedDay] && dayRecords[selectedDay].weight) {
// 有记录的历史日期:显示只读详情
selfRecord.style.display = 'none';
renderRecordDetail();
recordDetail.classList.add('show');
bottomBar.style.display = 'none';
} else {
// 未选中:全部隐藏
selfRecord.style.display = 'none';
recordDetail.classList.remove('show');
bottomBar.style.display = 'none';
}
}
@@ -661,6 +708,54 @@ function confirmAddValue() {
/* ===== 底部栏操作 ===== */
/* ===== 记录详情只读渲染 ===== */
function renderRecordDetail() {
var rec = dayRecords[selectedDay] || {};
var weightData = rec.weight;
if (!weightData) {
document.getElementById('recordDetailList').innerHTML = '<div class="record-detail__empty">暂无记录</div>';
return;
}
var sourceMap = { height: ['身高秤','record-detail__source-badge--height'], bluetooth: ['蓝牙','record-detail__source-badge--bluetooth'], fat: ['体脂仪','record-detail__source-badge--fat'], manual: ['自记','record-detail__source-badge--manual'] };
var source = sourceMap[weightData.type] || ['自记','record-detail__source-badge--manual'];
var html = '';
// 体重
html += '<div class="record-detail__item">';
html += ' <div class="record-detail__icon record-detail__icon--weight">⚖️</div>';
html += ' <div class="record-detail__info">';
html += ' <div class="record-detail__name">体重</div>';
html += ' <span class="record-detail__val">' + weightData.val + '<span class="record-detail__val-unit">kg</span></span>';
html += ' </div>';
html += ' <div class="record-detail__source"><span class="record-detail__source-badge ' + source[1] + '">' + source[0] + '</span></div>';
html += '</div>';
// 体脂
if (rec.fatVal !== undefined) {
html += '<div class="record-detail__item">';
html += ' <div class="record-detail__icon record-detail__icon--fat">🧬</div>';
html += ' <div class="record-detail__info">';
html += ' <div class="record-detail__name">体脂</div>';
html += ' <span class="record-detail__val">' + rec.fatVal.toFixed(1) + '<span class="record-detail__val-unit">%</span></span>';
html += ' </div>';
html += ' <div class="record-detail__source"><span class="record-detail__source-badge ' + source[1] + '">' + source[0] + '</span></div>';
html += '</div>';
}
// 腰围
if (rec.waistVal !== undefined) {
html += '<div class="record-detail__item">';
html += ' <div class="record-detail__icon record-detail__icon--waist">📏</div>';
html += ' <div class="record-detail__info">';
html += ' <div class="record-detail__name">腰围</div>';
html += ' <span class="record-detail__val">' + rec.waistVal.toFixed(1) + '<span class="record-detail__val-unit">cm</span></span>';
html += ' </div>';
html += ' <div class="record-detail__source"><span class="record-detail__source-badge ' + source[1] + '">' + source[0] + '</span></div>';
html += '</div>';
}
document.getElementById('recordDetailList').innerHTML = html;
}
/* 更新底部栏 */
function updateBottomBar() {
var totalItems = Object.keys(cart).length;
@@ -937,6 +1032,12 @@ function scrollToSelfRecord() {
}
/* 初始化 */
// 更新所有已记录日期的卡片显示
Object.keys(dayRecords).forEach(function(day) {
if (dayRecords[day] && dayRecords[day].weight) {
updateDayCard(day);
}
});
selectDay(TODAY);
updateBottomBar();
</script>
@@ -91,6 +91,8 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
/* ===== 列表区域 ===== */
.record-list { padding: 8px 12px 16px; display: flex; flex-direction: column; gap: 8px; }
.record-item { background: #fff; border-radius: 14px; padding: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.03); }
.record-item.clickable { cursor: pointer; transition: transform 0.15s; text-decoration: none; display: block; }
.record-item.clickable:active { transform: scale(0.98); }
.record-item__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.record-item__title { font-size: 14px; font-weight: 700; color: #1a1a1a; }
.record-item__time { font-size: 11px; color: #bbb; }
@@ -99,7 +101,8 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.record-item__metric-label { font-size: 10px; color: #999; margin-bottom: 2px; }
.record-item__metric-val { font-size: 16px; font-weight: 700; color: #1a1a1a; }
.record-item__metric-unit { font-size: 10px; color: #999; font-weight: 400; margin-left: 1px; }
.record-item__source { margin-left: auto; }
.record-item__source { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.record-item__arrow { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.source-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }
.source-badge--scale { background: #e8effa; color: #5470c6; }
.source-badge--bt { background: #f3efff; color: #8b5cf6; }
@@ -149,12 +152,12 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="month-cal" id="monthCal" style="display:none;"></div>
<!-- 图例 -->
<div class="legend">
<div class="legend__item"><span class="legend__dot legend__dot--scale"></span>身高</div>
<div class="legend__item"><span class="legend__dot legend__dot--bt"></span>蓝牙秤</div>
<div class="legend__item"><span class="legend__dot legend__dot--self"></span>自记</div>
<div class="legend__item"><span class="legend__dot legend__dot--fat"></span>体脂仪</div>
</div>
<div class="legend">
<div class="legend__item"><span class="legend__dot legend__dot--scale"></span>体重</div>
<div class="legend__item"><span class="legend__dot legend__dot--bt"></span>蓝牙秤</div>
<div class="legend__item"><span class="legend__dot legend__dot--self"></span>自记</div>
<div class="legend__item"><span class="legend__dot legend__dot--fat"></span>体脂仪</div>
</div>
<!-- Tab 栏 -->
<div class="record-tabs" id="recordTabs">
@@ -176,7 +179,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
/* ===== 数据 ===== */
// 来源类型及其颜色映射
var SOURCE_TYPES = {
'scale': { label:'身高秤', css:'scale', ring:'week-cal__ring--scale', mring:'month-cal__ring--scale', dot:'month-cal__dot--scale', badge:'source-badge--scale' },
'scale': { label:'体重秤', css:'scale', ring:'week-cal__ring--scale', mring:'month-cal__ring--scale', dot:'month-cal__dot--scale', badge:'source-badge--scale' },
'bt': { label:'蓝牙秤', css:'bt', ring:'week-cal__ring--bt', mring:'month-cal__ring--bt', dot:'month-cal__dot--bt', badge:'source-badge--bt' },
'self': { label:'自记', css:'self', ring:'week-cal__ring--self', mring:'month-cal__ring--self', dot:'month-cal__dot--self', badge:'source-badge--self' },
'fat': { label:'体脂仪', css:'fat', ring:'week-cal__ring--fat', mring:'month-cal__ring--fat', dot:'month-cal__dot--fat', badge:'source-badge--fat' }
@@ -466,27 +469,46 @@ function renderRecordList() {
return;
}
var html = '';
for (var i = 0; i < filtered.length; i++) {
var r = filtered[i];
var src = SOURCE_TYPES[r.source];
html += '<div class="record-item">';
html += '<div class="record-item__header">';
html += '<span class="record-item__title">体重记录</span>';
html += '<span class="record-item__time">' + r.date + ' ' + r.time + '</span>';
html += '</div>';
html += '<div class="record-item__body">';
html += '<div class="record-item__metric"><div class="record-item__metric-label">体重</div><div class="record-item__metric-val">' + r.weight.toFixed(1) + '<span class="record-item__metric-unit">kg</span></div></div>';
if (r.bodyFat !== null) {
html += '<div class="record-item__metric"><div class="record-item__metric-label">体脂率</div><div class="record-item__metric-val">' + r.bodyFat.toFixed(1) + '<span class="record-item__metric-unit">%</span></div></div>';
}
if (r.waist !== null) {
html += '<div class="record-item__metric"><div class="record-item__metric-label">腰围</div><div class="record-item__metric-val">' + r.waist.toFixed(1) + '<span class="record-item__metric-unit">cm</span></div></div>';
}
html += '<div class="record-item__source"><span class="source-badge ' + src.badge + '">' + src.label + '</span></div>';
html += '</div>';
html += '</div>';
}
var html = '';
// 设备来源到 URL device 参数的映射
var deviceParamMap = { 'scale': 'scale', 'bt': 'bluetooth', 'fat': 'fat' };
for (var i = 0; i < filtered.length; i++) {
var r = filtered[i];
var src = SOURCE_TYPES[r.source];
var isSelf = r.source === 'self';
var deviceParam = deviceParamMap[r.source];
// 非自记来源:用 <a> 标签包裹,可点击跳转
if (!isSelf && deviceParam) {
html += '<a class="record-item clickable" href="report-detail.html?device=' + deviceParam + '">';
} else {
html += '<div class="record-item">';
}
html += '<div class="record-item__header">';
html += '<span class="record-item__title">体重记录</span>';
html += '<span class="record-item__time">' + r.date + ' ' + r.time + '</span>';
html += '</div>';
html += '<div class="record-item__body">';
html += '<div class="record-item__metric"><div class="record-item__metric-label">体重</div><div class="record-item__metric-val">' + r.weight.toFixed(1) + '<span class="record-item__metric-unit">kg</span></div></div>';
if (r.bodyFat !== null) {
html += '<div class="record-item__metric"><div class="record-item__metric-label">体脂率</div><div class="record-item__metric-val">' + r.bodyFat.toFixed(1) + '<span class="record-item__metric-unit">%</span></div></div>';
}
if (r.waist !== null) {
html += '<div class="record-item__metric"><div class="record-item__metric-label">腰围</div><div class="record-item__metric-val">' + r.waist.toFixed(1) + '<span class="record-item__metric-unit">cm</span></div></div>';
}
html += '<div class="record-item__source"><span class="source-badge ' + src.badge + '">' + src.label + '</span>';
// 非自记来源:显示箭头
if (!isSelf) {
html += '<div class="record-item__arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>';
}
html += '</div>';
html += '</div>';
// 闭合标签
if (!isSelf && deviceParam) {
html += '</a>';
} else {
html += '</div>';
}
}
container.innerHTML = html;
}
+1 -1
View File
@@ -575,7 +575,7 @@ const SITE_MAP = [
{ file:'report-gastro.html', label:'胃肠镜报告详情' },
{ file:'project-detail.html', label:'体检项目详情' }
] },
{ name:'体重管理', dir:'employee-app/weight-management', entry:'weight-main.html', pages:[
{ name:'体重管理', dir:'employee-app/weight-management', entry:'weight-main.html', prd:'employee-app/weight-management/doc/PRD.md', done:true, pages:[
{ file:'weight-main.html', label:'体重管理主页' },
{ file:'weight-ranking.html', label:'排行榜' },
{ file:'eat-checkin.html', label:'吃打卡' },
+16 -223
View File
@@ -1,228 +1,21 @@
/**
* 原型站轻量登录网关
*
* 功能:
* - 所有静态文件访问前必须通过 Cookie 认证
* - 未登录自动跳转到自定义登录页
* - 密码写死在本文件中,按需修改
*
* 启动:node server.js
* 默认端口:3000(可通过环境变量 PORT 修改)
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const crypto = require('crypto');
const querystring = require('querystring');
/* ========== 配置区 ========== */
/** 访问密码(按需修改) */
const PASSWORD = 'JKCQ_2026!';
/** Cookie 签名密钥(部署时建议替换为随机字符串) */
const SECRET = 'hc-prototype-secret-key-2026';
/** Cookie 名称 */
const COOKIE_NAME = 'prototype_auth';
/** Cookie 有效期(秒),默认 7 天 */
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60;
/** 服务端口 */
const PORT = process.env.PORT || 3010;
/** 静态文件根目录(当前目录) */
const STATIC_ROOT = __dirname;
/** 不需要登录即可访问的路径 */
const PUBLIC_PATHS = ['/login', '/login.html', '/favicon.ico'];
/* ========== MIME 类型映射 ========== */
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.md': 'text/plain; charset=utf-8',
const PORT = 3000;
const ROOT = __dirname;
const MIME = {
'.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8',
'.md': 'text/plain; charset=utf-8', '.png': 'image/png', '.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
};
/* ========== 工具函数 ========== */
/** 生成签名 Cookie 值 */
function signToken() {
var expires = Date.now() + COOKIE_MAX_AGE * 1000;
var payload = 'ok|' + expires;
var sig = crypto.createHmac('sha256', SECRET).update(payload).digest('hex').slice(0, 16);
return payload + '|' + sig;
}
/** 校验签名 Cookie 值 */
function verifyToken(token) {
if (!token) return false;
var parts = token.split('|');
if (parts.length !== 3) return false;
var status = parts[0];
var expires = parseInt(parts[1], 10);
var sig = parts[2];
if (status !== 'ok' || isNaN(expires)) return false;
if (Date.now() > expires) return false;
var expected = crypto.createHmac('sha256', SECRET).update(status + '|' + expires).digest('hex').slice(0, 16);
return sig === expected;
}
/** 从请求头解析指定 Cookie */
function getCookie(req, name) {
var header = req.headers.cookie || '';
var cookies = header.split(';');
for (var i = 0; i < cookies.length; i++) {
var pair = cookies[i].trim().split('=');
if (pair[0] === name) return decodeURIComponent(pair.slice(1).join('='));
}
return null;
}
/** 读取 POST 请求体 */
function readBody(req, callback) {
var chunks = [];
req.on('data', function(chunk) { chunks.push(chunk); });
req.on('end', function() { callback(Buffer.concat(chunks).toString()); });
}
/** 安全校验回跳地址:只允许站内相对路径 */
function safeRedirect(target) {
if (!target || typeof target !== 'string') return '/';
if (target.charAt(0) !== '/') return '/';
if (target.indexOf('//') === 0) return '/';
return target;
}
/** 发送静态文件 */
function serveFile(res, filePath) {
/* 防止路径穿越攻击 */
var resolved = path.resolve(filePath);
if (resolved.indexOf(path.resolve(STATIC_ROOT)) !== 0) {
res.writeHead(403);
res.end('Forbidden');
return;
}
/* 禁止访问敏感文件 */
var basename = path.basename(resolved);
if (basename === 'server.js' || basename === '.env' || basename === '.gitignore') {
res.writeHead(403);
res.end('Forbidden');
return;
}
fs.stat(resolved, function(err, stats) {
if (err || !stats.isFile()) {
/* 目录请求尝试补 index-v2.html */
if (!err && stats.isDirectory()) {
var indexFile = path.join(resolved, 'index-v2.html');
serveFile(res, indexFile);
return;
}
res.writeHead(404);
res.end('Not Found');
return;
}
var ext = path.extname(resolved).toLowerCase();
var contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': stats.size });
fs.createReadStream(resolved).pipe(res);
http.createServer((req, res) => {
let fp = path.join(ROOT, decodeURIComponent(req.url.split('?')[0]));
if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) fp = path.join(fp, 'index.html');
const ext = path.extname(fp).toLowerCase();
fs.readFile(fp, (err, data) => {
if (err) { res.writeHead(404); res.end('404: ' + req.url); return; }
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(data);
});
}
/* ========== 请求处理 ========== */
var server = http.createServer(function(req, res) {
var parsed = url.parse(req.url, true);
var pathname = decodeURIComponent(parsed.pathname);
/* --- POST /simple-login:校验密码 --- */
if (req.method === 'POST' && pathname === '/simple-login') {
readBody(req, function(body) {
var data = querystring.parse(body);
var password = data.password || '';
var redirect = safeRedirect(data.redirect);
if (password !== PASSWORD) {
res.writeHead(302, { 'Location': '/login.html?error=1&redirect=' + encodeURIComponent(redirect) });
res.end();
return;
}
var token = signToken();
var cookie = COOKIE_NAME + '=' + encodeURIComponent(token)
+ '; Path=/; HttpOnly; SameSite=Lax; Max-Age=' + COOKIE_MAX_AGE;
res.writeHead(302, {
'Set-Cookie': cookie,
'Location': redirect
});
res.end();
});
return;
}
/* --- POST /simple-logout:退出登录 --- */
if (req.method === 'POST' && pathname === '/simple-logout') {
var clearCookie = COOKIE_NAME + '=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0';
res.writeHead(302, { 'Set-Cookie': clearCookie, 'Location': '/login.html' });
res.end();
return;
}
/* --- GET /simple-logout:也支持 GET 方式退出 --- */
if (req.method === 'GET' && pathname === '/simple-logout') {
var clearCookie2 = COOKIE_NAME + '=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0';
res.writeHead(302, { 'Set-Cookie': clearCookie2, 'Location': '/login.html' });
res.end();
return;
}
/* --- 公开路径放行 --- */
var isPublic = PUBLIC_PATHS.some(function(p) { return pathname === p; });
if (isPublic) {
var loginFile = pathname === '/login' ? '/login.html' : pathname;
var fullPath = path.join(STATIC_ROOT, loginFile);
serveFile(res, fullPath);
return;
}
/* --- 鉴权:检查 Cookie --- */
var token = getCookie(req, COOKIE_NAME);
if (!verifyToken(token)) {
res.writeHead(302, { 'Location': '/login.html?redirect=' + encodeURIComponent(pathname) });
res.end();
return;
}
/* --- 已登录:返回静态文件 --- */
if (pathname === '/') pathname = '/index-v2.html';
var filePath = path.join(STATIC_ROOT, pathname);
serveFile(res, filePath);
});
server.listen(PORT, function() {
console.log('[原型网关] 已启动 -> http://localhost:' + PORT);
console.log('[原型网关] 访问密码: ' + PASSWORD);
console.log('[原型网关] 按 Ctrl+C 停止');
});
}).listen(PORT, () => console.log('Server: http://localhost:' + PORT));
+10
View File
@@ -0,0 +1,10 @@
@echo off
echo ============================================
echo 健康CQ原型 - 本地预览服务器
echo ============================================
echo.
echo 打开浏览器访问: http://localhost:3000/
echo 按 Ctrl+C 停止服务器
echo.
node server.js
pause