feat: 体重管理原型更新(管理端方案编辑审核人分组、员工端目标/打卡调整及PRD、截图同步)

- 管理端:方案编辑审核人按医疗点/管理员人员分组且均可添加多个;体重排行、设备添加页及监测页调整
- 员工端:历史计划更名为历史目标;目标详情新增编辑态;吃/动打卡新增成功与已记录态;体重记录成功提示
- PRD 与截图同步更新
This commit is contained in:
liuyunqin
2026-08-26 19:25:51 +08:00
parent e76ca89ae6
commit 059d1c82ab
57 changed files with 1345 additions and 332 deletions
@@ -17,7 +17,6 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #1a1a1a; }
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; }
.nav-placeholder { width: 28px; height: 28px; }
.nav-action { font-size: 13px; color: #36cfc9; font-weight: 600; cursor: pointer; white-space: nowrap; flex-shrink: 0; }
/* ===== 周日历(页面顶部) ===== */
.calendar-card { background: #fff; border-radius: 16px; margin: 10px 12px 0; padding: 12px 12px 8px; box-shadow: 0 4px 14px rgba(0,0,0,0.04); position: relative; }
@@ -343,7 +342,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</div>
<div class="nav-title">今日饮食建议</div>
<span class="nav-action" onclick="location.href='diet-questionnaire.html'">修改偏好</span>
<div class="nav-placeholder"></div>
</div>
<!-- ===== 周日历(页面顶部) ===== -->
+87 -59
View File
@@ -10,7 +10,7 @@
体重管理(员工端)
├── 体重管理主页(入口)
│ ├── 目标管理 → 目标详情
│ ├── 历史计划
│ ├── 历史目标
│ ├── 活动详情
│ ├── 基本信息
│ ├── 签订承诺 / 我的承诺
@@ -45,6 +45,8 @@ stateDiagram-v2
每日打卡 --> 饮食记录: 查看饮食
每日打卡 --> 运动记录: 查看运动
每日打卡 --> 体重记录: 查看体重
体重管理主页 --> 目标详情: 查看目标卡片
体重管理主页 --> 健康科普: 查看科普食谱/推荐视频
体重管理主页 --> 吃动平衡: 查看吃动平衡
体重管理主页 --> 排行榜: 查看排名
体重管理主页 --> 体重报告: 查看报告
@@ -61,7 +63,7 @@ stateDiagram-v2
| 1 | 体重管理主页 | weight-main-v2.html | 页面 |
| 2 | 目标管理 | goal-management-v2.html | 页面 |
| 3 | 目标详情 | goal-detail-v2.html | 页面 |
| 4 | 历史计划 | goal-history-v2.html | 页面 |
| 4 | 历史目标 | goal-history-v2.html | 页面 |
| 5 | 活动详情 | activity-detail.html | 页面 |
| 6 | 基本信息 | basic-info.html | 页面 |
| 7 | 签订承诺 | promise-signing.html | 页面 |
@@ -117,18 +119,18 @@ stateDiagram-v2
**功能说明:** 体重管理模块的核心中枢页面,聚合展示体重管理目标、每日吃/动/体重打卡概览、吃动平衡、排行榜、体重报告与推荐视频。
页面存在六种状态:6 种演示状态:已设置-参加活动·已签署、已设置-参加活动·审核中、已设置-参加活动·已通过待签署、已设置-参加活动·已驳回、已设置-个人目标、未设置目标。
| UI 区域 | 已签署 | 审核中 | 已通过待签署 | 已驳回 | 个人目标 | 未设置 |
|---------|:--:|:--:|:--:|:--:|:--:|:--:|
| 目标卡片-标尺(人物+波浪球) | ✅ | ❌ | ❌ | ❌ | ✅ | |
| 目标状态横幅 | ❌ | 🟡 审核中 | 🟢 去签署 | 🔴 重新提交 | ❌ | |
| 目标空状态 | ❌ | ❌ | ❌ | ❌ | ❌ | |
| 排名徽章(第N名) | ✅ | ❌ | ❌ | ❌ | ❌ | |
| 排行榜-我的排名行 | ✅ | ❌ | ❌ | ❌ | ❌ | |
| 体重报告-目标相关行 | ✅ | ❌ | ❌ | ❌ | ✅ | |
| UI 区域 | 已签署 | 审核中 | 已通过待签署 | 已驳回 | 个人目标 | 未设置/未参加 |
|---------|:--:|:--:|:--:|:--:|:--:|:-------:|
| 目标卡片-标尺(人物+波浪球) | ✅ | ❌ | ❌ | ❌ | ✅ | |
| 目标状态横幅 | ❌ | 🟡 审核中 | 🟢 去签署 | 🔴 重新提交 | ❌ | |
| 目标空状态 | ❌ | ❌ | ❌ | ❌ | ❌ | |
| 排名徽章(第N名) | ✅ | ❌ | ❌ | ❌ | ❌ | |
| 排行榜-我的排名行 | ✅ | ❌ | ❌ | ❌ | ❌ | |
| 体重报告-目标相关行 | ✅ | ❌ | ❌ | ❌ | ✅ | |
**主要UI元素:**
- **导航栏:** 标题"体重管理",右侧"历史计划"链接(跳转到历史计划页)
- **导航栏:** 标题"体重管理",右侧"历史目标"链接(跳转到历史目标页)
- **体重管理目标卡片:**
- 标题"体重管理目标"+ 右侧"当前第N天/共N天"(点击跳转到目标详情/目标管理页)
- 活动名称 + 排名徽章(🏆 第N名)
@@ -142,26 +144,29 @@ stateDiagram-v2
- **记录体重弹窗(底部弹层):** 记录体重(必填,滑动尺子调整)+ 记录体脂率(开关)+ 记录腰围(开关)+ "保存"按钮,构成同体重记录页的记录体重弹窗(见第 18 节)
![记录体重弹窗](screenshots/weight-main-record.jpg)
- **记录成功弹窗(保存后弹出):** 居中弹窗——今日日期 + "记录成功" + 今日体重(kg,青绿高亮)+ 体脂率/腰围摘要行(对应开关开启时显示)+ "确定"按钮(仅关闭弹窗,留在当前页)
![记录成功弹窗](screenshots/weight-main-record-success.jpg)
- **排行榜卡片:** "我的排名"行(排名/积分)+ 前三名金银铜牌列表,点击跳转到排行榜页
- **体重报告卡片:** 初始体重 → 当前体重 → 目标体重,统计行(已减体重/距目标/目标进度)+ 进度条
- **推荐视频卡片:** 2列网格展示健康科普视频(缩略图 + 播放次数 + 作者 + 标题 + 机构)
**交互说明:**
- 点击"历史计划"跳转到历史计划页(goal-history-v2.html
- 点击目标卡片天数链接跳转到目标详情页(goal-detail-v2.html),未设置目标时跳转到目标管理页(goal-management-v2.html
- 点击"历史目标"跳转到历史目标页(goal-history-v2.html
- 点击目标卡片跳转到目标详情页(goal-detail-v2.html),未设置目标时跳转到目标管理页(goal-management-v2.html
- 点击状态横幅"去签署"跳转到签订承诺页(promise-signing.html);"重新提交"跳转到目标管理页(goal-management-v2.html,携带活动回填参数)
- 点击吃/动/体重卡片:已记录时跳转到对应记录页(饮食记录 eat-detail-v2.html / 运动记录 exercise-detail-v2.html / 体重记录 weight-detail-v2.html),未记录时直接跳转到自记打卡页(饮食录入 eat-checkin-v2.html / 运动录入 exercise-checkin-v2.html,体重为打开记录体重弹窗)
- 点击吃卡片"+"跳转到饮食录入页(eat-checkin-v2.html);动卡片"+"跳转到运动录入页(exercise-checkin-v2.html);体重卡片"+"打开记录体重弹窗
- 记录体重弹窗点击"保存"后关闭弹窗并弹出记录成功弹窗(展示本次记录的体重等数据),主页体重卡片同步更新为最新记录
- 点击"今日饮食建议",先判断是否设置饮食偏好,如未设置,跳转到diet-questionnaire.html,设置了则跳转到今日饮食建议页(diet-advice-v2.html);"智能运动计划"跳转到智能运动计划页(exercise-advice-v2.html);"蓝牙称"跳转到蓝牙称页(bluetooth-scale-v2.html
- 点击吃动平衡卡片跳转到吃动平衡页(eat-exercise-detail-v2.html);点击周日历中的日期切换当日吃动统计(不触发跳转)
- 点击排行榜卡片跳转到排行榜页(weight-ranking.html);体重报告卡片跳转到体重报告页(weight-report.html
- 点击返回按钮跳转到应用主页(home/index.html
**业务规则:**
- 非"已签署"状态下,排行榜隐藏"我的排名"行、体重报告隐藏目标相关行、标尺隐藏并展示状态横幅
- 个人目标模式下隐藏排名徽章与"我的排名"行,体重报告完整展示
- 如员工有参加过活动(签署过承诺书),则后续被纳入参与人员范围的活动自动参加,自动变成已签署状态
- 未参加活动时,判断当前单位是否存在活动,已存在,显示排行榜;未存在活动,显示排行榜示意图,无法点击
- 如员工有参加过活动(签署过承诺书),则后续被纳入参与人员范围的活动自动参加,自动变成已签署状态(已参加)
-
---
### 2. 目标管理(goal-management-v2.html
@@ -188,9 +193,13 @@ stateDiagram-v2
- **活动选择弹窗:** 活动列表(活动名称、起止时间、组织机构)+ 确定按钮
![活动选择弹窗](screenshots/goal-management-activity.jpg)
- **确认提交弹窗:** 活动信息 / 初始数据 / 目标数据三段式确认,取消 / 确认提交(个人目标模式下活动信息段为空占位"--"
- **确认提交弹窗:** 按目标类型展示信息段 + 初始数据 / 目标数据三段式确认,取消 / 确认提交
- 活动目标模式:信息段标题为"活动信息",含活动名称 / 起止日期 / 组织机构
![确认提交弹窗](screenshots/goal-management-confirm.jpg)
![确认提交弹窗-活动目标](screenshots/goal-management-confirm.jpg)
- 个人目标模式:信息段标题为"个人目标信息",含目标名称 / 起止日期(回填所填名称与开始~结束日期),不展示组织机构
![确认提交弹窗-个人目标](screenshots/goal-management-confirm-personal.jpg)
**交互说明:**
- 点击"活动目标/个人目标"切换类型
@@ -198,13 +207,12 @@ stateDiagram-v2
- 点击"查看全部"跳转到基本信息页(basic-info.html
- 点击底部"提交"校验(活动模式需先选活动,个人模式需填写名称与合法日期),通过后打开确认弹窗
- 点击"确认提交"跳转到目标详情页(goal-detail-v2.html
- 页面支持通过 URL 参数回填(个人目标:name/startDate/endDate;活动目标:actId/actName/actPeriod/actOrg
**业务规则:**
- 活动天数由活动起止日期自动计算,不可修改
- 个人目标天数由开始/结束日期自动计算
- 目标数据各项分别给出推荐范围与换算说明:体重按身高与健康 BMI 标准(18.5~23.9)换算;体脂率按性别健康标准;腰围按身高比例与性别中心性肥胖线取较严值(原型中为静态示例文案)
- 结束日期必须晚于开始日期
- 目标数据各项分别给出推荐范围与换算说明(推荐范围按原系统公式走,换算说明可暂时等待AI处理)
- 开始日期只能今日及以后,结束日期必须晚于开始日期
---
@@ -212,24 +220,28 @@ stateDiagram-v2
![页面截图](screenshots/goal-detail-signed.jpg)
![修改目标弹窗](screenshots/goal-detail-edit.jpg)
![修改目标审核中](screenshots/goal-detail-review.jpg)
**功能说明:** 展示单个活动目标或个人目标的完整信息,含审核状态、目标数据、变化趋势、排行榜与体重报告。
**页面状态:** 导航栏标题旁下拉菜单切换"活动目标 / 个人目标";活动目标模式下通过顶部状态标签切换 5 种状态演示:审核中、已通过待签署、已驳回、已签署、已结束。
**页面状态:** 导航栏标题"目标详情",标题旁下拉菜单切换"活动目标 / 个人目标";活动目标模式下通过顶部状态标签切换 5 种状态演示:审核中、已通过待签署、已驳回、已签署、已结束。
| 状态 | 状态横幅 | 目标信息列 | 变化趋势 | 排行榜/报告 |
|------|------|------|:--:|:--:|
| 审核中 | 🟡 目标审核中,请耐心等待(提交时间) | 初始 → 目标 | ❌ | |
| 已通过待签署 | 🟢 审核已通过,请签署目标承诺书(审核人)+ 签署按钮 | 初始 → 目标 | ❌ | |
| 已驳回 | 🔴 目标已被驳回(驳回原因 + 重新提交按钮) | 初始 → 目标(体重标红) | ❌ | |
| 已签署 | 🎉 承诺书已签署,活动进行中(签署时间)+ 承诺书按钮 | 初始 → 当前 → 目标 | ❌ | |
| 已结束 | 🏁 活动已结束,恭喜完成目标 + 承诺书按钮 | 初始 → 最终 → 目标 | ✅ | |
| 状态 | 状态横幅 | 目标信息列 | 变化趋势 | 排行榜/报告/摄入消耗 |
|------|------|------|:--:|:-----------:|
| 审核中 | 🟡 目标审核中,请耐心等待(提交时间) | 初始 → 目标 | ❌ | |
| 已通过待签署 | 🟢 审核已通过,请签署目标承诺书(审核人)+ 签署按钮 | 初始 → 目标 | ❌ | |
| 已驳回 | 🔴 目标已被驳回(驳回原因 + 重新提交按钮) | 初始 → 目标 | ❌ | |
| 已签署 | 🎉 承诺书已签署,活动进行中(签署时间)+ 承诺书按钮;目标信息卡片标题右侧"修改目标"按钮 | 初始 → 当前 → 目标 | ❌ | |
| 已结束 | 🏁 活动已结束 + 承诺书按钮 | 初始 → 最终 → 目标 | ✅ | |
**主要UI元素:**
- **导航栏:** 标题"活动目标详情/个人目标详情" + 标题旁下拉菜单(活动目标/个人目标)
- **导航栏:** 标题"目标详情" + 标题旁下拉菜单(活动目标/个人目标)
- **状态横幅:** 按状态显示不同颜色与文案,含提交/审核/签署时间、审核人、驳回原因及操作按钮
- **活动信息卡片:** 活动名称 + "活动详情"链接 + 起止时间 · 组织机构
- **目标信息卡片:** 三列数据(初始 / 当前·最终 / 目标),含体重、体脂率、腰围
- **目标信息卡片:** 三列数据(初始 / 当前·最终 / 目标),含体重、体脂率、腰围;已签署状态下卡片标题右侧显示"修改目标"按钮,提交修改后按钮隐藏、卡片底部显示黄色审核提示条("修改目标已提交,正在审核中" + 修改后的目标体重/体脂/腰围数据)
- **变化趋势卡片:** 体重变化 / 体脂变化 / 腰围变化三 Tab,各含统计行(平均/最大/最小/变化)+ 折线图(初始参考线、目标参考线、实际趋势)
- **排行榜卡片:** 我的排名 + 前三名(跳转排行榜页)
- **体重报告卡片:** 截止日期/天数 + 初始→当前→目标体重 + 已减/距目标/目标进度 + 进度条
@@ -241,33 +253,38 @@ stateDiagram-v2
- 点击状态标签切换活动目标状态演示
- 点击"活动详情"跳转到活动详情页(activity-detail.html
- 点击"签署"跳转到签订承诺页(promise-signing.html),"承诺书"跳转到我的承诺页(my-promise.html
- 点击"重新提交/修改目标"跳转到目标管理页(goal-management-v2.html,携带回填参数)
- 点击"重新提交"跳转到目标管理页(goal-management-v2.html,携带回填参数)
- 已签署状态下点击"修改目标"弹出修改目标弹窗:提示"当前已参加活动,仅限修改一次目标数据",下方为目标体重、目标体脂、目标腰围三个输入项并回填当前目标值;点击"提交"关闭弹窗、隐藏"修改目标"按钮,并在目标信息卡片下方显示"修改目标已提交,正在审核中"提示及修改后的目标数据;点击"取消"直接关闭弹窗
- 个人目标模式下点击底部"修改目标"跳转到目标管理页(goal-management-v2.html,携带回填参数)
- 点击排行榜卡片跳转到排行榜页(weight-ranking.html),体重报告卡片跳转到体重报告页(weight-report.html
**业务规则:**
- 活动目标状态流转:审核中 → 已通过待签署 → 已签署 → 已结束;审核驳回后可重新提交
- 已驳回状态下目标信息中的体重值红色高亮提
- 目标值超过推荐范围则红色警
- 个人目标模式无审核流程,提交后即为"进行中",可修改目标
- 活动目标已签署状态下修改目标仅限一次,提交后进入审核中状态,审核通过前目标数据不变
---
### 4. 历史计划goal-history-v2.html
### 4. 历史目标goal-history-v2.html
![页面截图](screenshots/goal-history.jpg)
**功能说明:** 展示员工参与过的全部活动计划与个人计划历史列表。
**功能说明:** 展示员工参与过的全部活动目标与个人目标历史列表。
**主要UI元素:**
- Tab 栏:活动计划 / 个人计划
- 计划卡片:计划名称、状态标签(进行中/已结束/已完成)、起止日期、机构(活动计划)、数据行(初始体重 → 目标体重 → 当前/最终体重)、底部(签署时间 / 剩余天数 / 减重结果)+ "详情"链接
- Tab 栏:活动目标 / 个人目标
- 目标卡片:目标名称、状态标签(进行中/已结束/已完成)、起止日期、机构(活动目标)、数据行(初始体重 → 目标体重 → 当前/最终体重)、底部(参加时间 / 剩余天数 / 减重结果)+ "详情"链接
- 底部"新建目标"按钮:固定于手机壳底部,带渐变背景过渡
**交互说明:**
- 点击 Tab 切换活动计划/个人计划列表
- 点击计划卡片或"详情"跳转到目标详情页(goal-detail-v2.html,携带类型与状态参数)
- 点击 Tab 切换活动目标/个人目标列表
- 点击目标卡片或"详情"跳转到目标详情页(goal-detail-v2.html,携带类型与状态参数)
- 点击底部"新建目标"跳转到目标管理页(goal-management-v2.html
**业务规则:**
- 已结束计划展示最终体重与减重结果
- 个人计划已达标时展示"达成目标 ✅"
- 进行中的活动目标底部展示参加时间,已结束目标展示最终体重与减重结果
- 个人目标已达标时展示"达成目标 ✅"
---
@@ -373,26 +390,32 @@ stateDiagram-v2
**功能说明:** 员工记录每日饮食摄入,按早餐/午餐/晚餐/加餐分类,支持搜索餐品、手动录入克数和拍照识别。
**主要UI元素:**
- 导航栏标题当前日期
- 导航栏标题当前日期,右上角"拍照识别"按钮(取景框图标 + 文字,跳转拍照识别页)
- 餐次选择 + 搜索行:餐次下拉(点击展开下拉浮层选择:早餐/午餐/晚餐/加餐,当前餐次高亮)+ 餐品搜索框
![餐次下拉浮层](screenshots/eat-checkin-meal.jpg)
- 双列布局:左侧餐品分类(常用/主食/肉蛋奶/蔬菜/水果/饮品/零食)、右侧菜品列表(emoji + 名称 + 千卡/参考份量 + "+"按钮)
- 底部固定栏:购物袋图标(显示已选数量徽章)、"完成打卡"按钮、拍照识别图标按钮(取景框图标,无文字)
- 双列布局:左侧餐品分类("已记录"置顶,下接常用/主食/肉蛋奶/蔬菜/水果/饮品/零食)、右侧菜品列表(emoji + 名称 + 千卡/参考份量 + "+"按钮)
- "已记录"分类列表:汇总展示各餐次已记录餐品(emoji + 名称 + 餐次标签 + 智记/自记标签 + 千卡/克数,自记条目可删除),支持搜索过滤
![已记录分类](screenshots/eat-checkin-recorded.jpg)
- 底部固定栏:购物袋图标(显示当前餐次已选数量徽章)+ 已选统计两行("已选 X 样" / "摄入 XXX 千卡",跟随当前餐次刷新)+ "提交记录"按钮(固定宽度,靠右)
- 克数录入弹窗:菜品信息 + 克数(+/- 步进)+ 预估热量 + 确认添加
![克数录入弹窗](screenshots/eat-checkin-gram.jpg)
- 已选餐品弹窗:四个餐次 Tab(含数量角标)+ 已选列表(智记/自记标签 + 克数 + 热量 + 移除按钮)+ 完成打卡
- 已选餐品弹窗(仅当前餐次):标题"XX · 已选餐品"(XX 为当前餐次名)+ 汇总行(共 X 样,XXX 千卡)+ 已选列表(智记/自记标签 + 克数 + 热量 + 移除按钮)+ 提交记录
![已选餐品弹窗](screenshots/eat-checkin-bag.jpg)
- 提交成功弹窗:今日日期 + "记录成功" + "XX摄入 XXX 千卡"(XX 为当前餐次名,数值为该餐次汇总)+ "确定"按钮
![提交成功弹窗](screenshots/eat-checkin-success.jpg)
**交互说明:**
- 点击餐次下拉展开选项浮层,选择后切换餐次并收起浮层
- 点击分类切换菜品列表,支持搜索过滤
- 点击餐次下拉展开选项浮层,选择后切换餐次(底部栏统计与购物袋徽章同步刷新)并收起浮层
- 点击分类切换菜品列表,支持搜索过滤;点击"已记录"分类查看各餐次已记录餐品,可删除自记条目
- 点击菜品"+"打开克数录入弹窗
- 点击购物袋打开已选餐品汇总面板,可切换餐次、移除菜品(智记不可移除)
- 点击"完成打卡"提交当日饮食记录
- 点击拍照识别图标按钮跳转到拍照识别页(scan-checkin.html
- 点击购物袋打开当前餐次的已选餐品面板,可移除菜品(智记不可移除),当前餐次删空后自动关闭面板
- 点击"提交记录"提交记录并弹出提交成功弹窗,点击"确定"返回上一页
- 点击右上角"拍照识别"跳转到拍照识别页(scan-checkin.html
- 点击返回按钮返回上一页
**业务规则:**
@@ -464,20 +487,26 @@ stateDiagram-v2
**主要UI元素:**
- 导航栏标题:"添加YYYY/MM/DD运动"
- 搜索行:运动搜索框
- 双列布局:左侧运动分类(常用/有氧运动/力量训练/球类运动/日常活动/柔韧平衡)、右侧运动列表(emoji + 名称 + 千卡/30分钟 + "+"按钮)
- 底部固定栏:购物袋图标(显示已选数量徽章)、"完成记录"按钮
- 双列布局:左侧运动分类("已记录"置顶,下接常用/有氧运动/力量训练/球类运动/日常活动/柔韧平衡)、右侧运动列表(emoji + 名称 + 千卡/30分钟 + "+"按钮)
- "已记录"分类列表:展示已记录运动(emoji + 名称 + 来源标签(穿戴/器械/微信/自记)+ 千卡/时长,自记条目可删除),支持搜索过滤
![已记录分类](screenshots/exercise-checkin-recorded.jpg)
- 底部固定栏:购物袋图标(显示已选数量徽章)+ 已选统计两行("已选 X 项" / "消耗 XXX 千卡"+ "提交记录"按钮(固定宽度,靠右)
- 时长录入弹窗:运动信息 + 时长(+/- 步进)+ 预估消耗 + 确认添加
![时长录入弹窗](screenshots/exercise-checkin-duration.jpg)
- 已选运动弹窗:已选列表(穿戴/器械/微信/自记标签 + 时长 + 消耗 + 移除按钮)+ 完成记录
- 已选运动弹窗:标题"已选运动" + 汇总行(共 X 项,XXX 分钟 XXX 千卡)+ 已选列表(穿戴/器械/微信/自记标签 + 时长 + 消耗 + 移除按钮)+ 提交记录
![已选运动弹窗](screenshots/exercise-checkin-bag.jpg)
- 提交成功弹窗:今日日期 + "记录成功" + "今日运动消耗 XXX 千卡"(全部已选运动汇总)+ "确定"按钮
![提交成功弹窗](screenshots/exercise-checkin-success.jpg)
**交互说明:**
- 点击分类切换运动列表,支持搜索过滤
- 点击分类切换运动列表,支持搜索过滤;点击"已记录"分类查看已记录运动,可删除自记条目
- 点击运动"+"打开时长录入弹窗
- 点击购物袋打开已选运动汇总面板,可移除自记运动
- 点击"完成记录"提交当日运动记录
- 点击"提交记录"提交当日运动记录并弹出提交成功弹窗,点击"确定"返回上一页
- 点击返回按钮返回上一页
**业务规则:**
@@ -497,7 +526,7 @@ stateDiagram-v2
- 总消耗标尺:半圆进度弧,左端基础消耗、右端活动消耗、中心总消耗(实际/推荐),右上"智能运动计划"按钮
- 运动统计三列:今日步数 / 运动次数 / 运动时长
- 数据来源卡片:四张来源小卡(穿戴·步数 / 器械·千卡 / 微信·步数(右上角"同步"按钮)/ 自记·步数),未记录时显示“尚未记录”,点击自记卡片跳转运动录入页(exercise-checkin-v2.html
- 变化趋势卡片:时间范围(周/月/季/年/活动期间)+ 三 Tab(步数趋势/消耗趋势/时长趋势+ 折线图(数据点悬浮提示)
- 变化趋势卡片:时间范围(周/月/季/年/活动期间)+ 三 Tab(步数变化/消耗变化/时长变化+ 折线图(数据点悬浮提示)
- 运动明细列表:按日期分组,每条含运动名、消耗、运动详情、来源徽章
**交互说明:**
@@ -521,7 +550,7 @@ stateDiagram-v2
**功能说明:** 为员工提供个性化膳食指导方案,含"食谱建议"与"营养分析"两个 Tab。
**主要UI元素:**
- 导航栏:标题"今日饮食建议",右侧"修改偏好"按钮(跳转到膳食偏好设置页)
- 导航栏:标题"今日饮食建议"
- 周历(含月历弹窗):日期用圆环标记有无饮食数据
- Tab 栏:食谱建议 / 营养分析
@@ -549,7 +578,6 @@ stateDiagram-v2
- 点击注意事项标题折叠/展开
- 点击菜品打开餐品详情弹窗,查看推荐理由;点击弹窗内"不喜欢"关闭详情弹窗并打开"不再推荐"弹窗
- 在"不再推荐"弹窗中选填不喜欢的原因(可多选、可跳过),点击"确定"提交并弹出"已记录"提示;点击"取消"或遮罩关闭
- 点击"修改偏好"跳转到膳食偏好设置页(diet-questionnaire.html
- 点击返回按钮返回来源页
**业务规则:**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 93 KiB

+166 -105
View File
@@ -17,6 +17,9 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #1a1a1a; }
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; }
.nav-placeholder { width: 28px; height: 28px; }
.nav-camera { display: flex; align-items: center; gap: 4px; height: 28px; padding: 0 10px; border-radius: 14px; background: #e8fbfa; color: #36cfc9; font-size: 12px; font-weight: 700; cursor: pointer; flex-shrink: 0; }
.nav-camera svg { width: 14px; height: 14px; }
.nav-camera:active { opacity: 0.8; }
/* ===== 餐次选择 + 搜索行 ===== */
.filter-row { background: #fff; padding: 10px 12px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #f0f0f0; }
@@ -52,7 +55,6 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
/* ===== 分类列表 ===== */
.category-list { background: #fff; border-radius: 12px; overflow: hidden; height: 100%; display: flex; flex-direction: column; }
.category-list__title { font-size: 12px; font-weight: 700; color: #999; padding: 10px 10px 6px; letter-spacing: 1px; flex-shrink: 0; }
.category-list__items { flex: 1; overflow-y: auto; }
.category-list__items::-webkit-scrollbar { display: none; }
.category-item { padding: 12px 10px; font-size: 13px; color: #666; text-align: center; cursor: pointer; border-left: 3px solid transparent; transition: all .2s; }
@@ -79,6 +81,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.dish-item__btn:active { background: #f5f5f5; }
.dish-item__btn--minus { color: #ff4d4f; border-color: #ffccc7; }
.dish-item__btn--plus { color: #3ec46a; border-color: #b7ebc4; background: #f6fff6; }
.dish-item__btn--remove { color: #ff4d4f; border-color: #ffccc7; background: #fff1f0; }
/* ===== 底部固定栏 ===== */
.bottom-bar { height: 68px; background: #fff; border-top: 1px solid #f0f0f0; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; gap: 12px; }
@@ -86,12 +89,14 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.bottom-bar__bag svg { width: 22px; height: 22px; color: #1890ff; }
.bottom-bar__bag-badge { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; border-radius: 9px; background: #ff4d4f; color: #fff; font-size: 10px; font-weight: 700; display: none; align-items: center; justify-content: center; padding: 0 4px; line-height: 1; }
.bottom-bar__bag-badge.show { display: flex; }
.bottom-bar__submit { flex: 1; height: 40px; border-radius: 20px; border: none; background: linear-gradient(160deg, #6ee16e, #3ec46a); color: #fff; font-size: 15px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 8px rgba(62,196,106,0.3); transition: opacity .2s, transform .15s; }
.bottom-bar__stats { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 2px; }
.bottom-bar__stats-item { font-size: 12px; color: #666; line-height: 1.3; white-space: nowrap; }
.bottom-bar__stats-item em { font-style: normal; font-weight: 800; font-size: 14px; }
.bottom-bar__stats-item--count em { color: #36cfc9; }
.bottom-bar__stats-item--kcal em { color: #fa8c16; }
.bottom-bar__submit { width: 116px; flex-shrink: 0; height: 40px; border-radius: 20px; border: none; background: linear-gradient(160deg, #6ee16e, #3ec46a); color: #fff; font-size: 15px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 8px rgba(62,196,106,0.3); transition: opacity .2s, transform .15s; margin-left: auto; }
.bottom-bar__submit:active { transform: scale(0.97); }
.bottom-bar__submit:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; }
.bottom-bar__scan { width: 40px; height: 40px; border-radius: 10px; background: #f7f8fa; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
.bottom-bar__scan svg { width: 22px; height: 22px; color: #666; }
.bottom-bar__scan:active { background: #e8e8e8; }
/* ===== 克数录入弹窗 ===== */
.gram-mask { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 50; }
@@ -127,14 +132,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.bag-panel__head { padding: 16px 16px 8px; flex-shrink: 0; }
.bag-panel__handle { width: 36px; height: 4px; background: #ddd; border-radius: 2px; margin: 0 auto 12px; }
.bag-panel__title { font-size: 15px; font-weight: 700; color: #1a1a1a; text-align: center; margin-bottom: 8px; }
.bag-panel__body { flex: 1; overflow: hidden; display: flex; gap: 8px; padding: 0 16px; }
/* 左侧:餐次标签 */
.bag-panel__meals { width: 68px; flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; padding-bottom: 4px; }
.bag-meal-tab { padding: 10px 6px; font-size: 13px; color: #666; text-align: center; cursor: pointer; border-radius: 8px; transition: all .2s; position: relative; font-weight: 500; }
.bag-meal-tab.active { color: #36cfc9; font-weight: 700; background: #e8fbfa; }
.bag-meal-tab__count { display: none; position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; border-radius: 8px; background: #ff4d4f; color: #fff; font-size: 10px; font-weight: 700; align-items: center; justify-content: center; padding: 0 4px; line-height: 1; }
.bag-meal-tab__count.show { display: flex; }
/* 右侧:已选列表 */
.bag-panel__body { flex: 1; overflow: hidden; display: flex; padding: 0 16px; }
.bag-panel__list-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
.bag-panel__summary { font-size: 13px; color: #666; padding-bottom: 8px; flex-shrink: 0; }
.bag-panel__summary em { font-style: normal; font-weight: 800; color: #fa8c16; }
@@ -152,12 +150,27 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.bag-item__src { font-size: 9px; font-weight: 600; padding: 1px 6px; border-radius: 6px; white-space: nowrap; line-height: 1.5; }
.bag-item__src--auto { color: #1890ff; background: #e6f7ff; }
.bag-item__src--manual { color: #fa8c16; background: #fff7e6; }
.bag-item__src--meal { color: #36cfc9; background: #e8fbfa; }
.bag-item__remove { width: 24px; height: 24px; border-radius: 50%; border: none; background: #fff1f0; color: #ff4d4f; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 12px; line-height: 1; }
.bag-item__remove:active { background: #ffccc7; }
.bag-panel__footer { padding: 12px 16px 24px; flex-shrink: 0; }
.bag-panel__submit { width: 100%; height: 46px; border: none; border-radius: 23px; background: linear-gradient(160deg, #6ee16e, #3ec46a); color: #fff; font-size: 16px; font-weight: 700; cursor: pointer; }
.bag-panel__submit:active { opacity: 0.85; }
/* ===== 提交成功弹窗 ===== */
.success-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 60; opacity: 0; visibility: hidden; transition: opacity .25s ease, visibility .25s ease; }
.success-mask.show { opacity: 1; visibility: visible; }
.success-dialog { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); width: 296px; background: #fff; border-radius: 16px; padding: 28px 20px 20px; z-index: 61; text-align: center; transition: transform .25s ease; }
.success-mask.show .success-dialog { transform: translate(-50%, -50%) scale(1); }
.success-dialog__icon { width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(160deg, #6ee16e, #3ec46a); display: flex; align-items: center; justify-content: center; margin: 0 auto; box-shadow: 0 4px 12px rgba(62,196,106,0.35); }
.success-dialog__icon svg { width: 28px; height: 28px; color: #fff; }
.success-dialog__date { font-size: 13px; color: #999; margin-top: 12px; }
.success-dialog__title { font-size: 18px; font-weight: 700; color: #1a1a1a; margin-top: 4px; }
.success-dialog__kcal { font-size: 13px; color: #666; margin-top: 12px; padding: 10px; background: #fff7e6; border-radius: 10px; }
.success-dialog__kcal em { font-style: normal; font-weight: 800; color: #fa8c16; font-size: 18px; }
.success-dialog__btn { width: 100%; height: 44px; border: none; border-radius: 22px; background: linear-gradient(160deg, #6ee16e, #3ec46a); color: #fff; font-size: 16px; font-weight: 700; cursor: pointer; margin-top: 16px; box-shadow: 0 2px 8px rgba(62,196,106,0.3); }
.success-dialog__btn:active { opacity: 0.85; }
/* Toast */
.toast { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); background: rgba(0,0,0,0.78); color: #fff; font-size: 14px; padding: 10px 22px; border-radius: 20px; z-index: 100; pointer-events: none; opacity: 0; transition: all .25s ease; white-space: nowrap; }
.toast.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }
@@ -182,7 +195,10 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</div>
<div class="nav-title" id="pageTitle">2026年7月24日</div>
<div class="nav-placeholder"></div>
<div class="nav-camera" onclick="scanFood()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><line x1="7" y1="12" x2="17" y2="12"/></svg>
<span>拍照识别</span>
</div>
</div>
<!-- 餐次选择 + 搜索行 -->
@@ -216,7 +232,6 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<!-- 左侧:分类列表 -->
<div class="record-layout__left">
<div class="category-list">
<div class="category-list__title">餐品分类</div>
<div class="category-list__items" id="categoryList"></div>
</div>
</div>
@@ -237,10 +252,11 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
<div class="bottom-bar__bag-badge" id="bagBadge"></div>
</div>
<button class="bottom-bar__submit" id="submitBtn" disabled onclick="submitCheckin()">完成打卡</button>
<div class="bottom-bar__scan" onclick="scanFood()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><line x1="7" y1="12" x2="17" y2="12"/></svg>
<div class="bottom-bar__stats">
<div class="bottom-bar__stats-item bottom-bar__stats-item--count">已选 <em id="statCount">0</em></div>
<div class="bottom-bar__stats-item bottom-bar__stats-item--kcal">摄入 <em id="statKcal">0</em> 千卡</div>
</div>
<button class="bottom-bar__submit" id="submitBtn" disabled onclick="submitCheckin()">提交记录</button>
</div>
<!-- 克数录入弹窗 -->
@@ -273,36 +289,33 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="bag-panel" id="bagPanel" onclick="event.stopPropagation()">
<div class="bag-panel__head">
<div class="bag-panel__handle"></div>
<div class="bag-panel__title">记录餐品</div>
<div class="bag-panel__title" id="bagTitle">餐品</div>
</div>
<div class="bag-panel__body">
<!-- 左侧:餐次标签 -->
<div class="bag-panel__meals" id="bagMealTabs">
<div class="bag-meal-tab active" data-meal="breakfast" onclick="switchBagMeal('breakfast')">
早餐<span class="bag-meal-tab__count" id="bagCount_breakfast"></span>
</div>
<div class="bag-meal-tab" data-meal="lunch" onclick="switchBagMeal('lunch')">
午餐<span class="bag-meal-tab__count" id="bagCount_lunch"></span>
</div>
<div class="bag-meal-tab" data-meal="dinner" onclick="switchBagMeal('dinner')">
晚餐<span class="bag-meal-tab__count" id="bagCount_dinner"></span>
</div>
<div class="bag-meal-tab" data-meal="snack" onclick="switchBagMeal('snack')">
加餐<span class="bag-meal-tab__count" id="bagCount_snack"></span>
</div>
</div>
<!-- 右侧:已选列表 -->
<div class="bag-panel__list-wrap">
<div class="bag-panel__summary" id="bagSummary"></div>
<div class="bag-panel__list" id="bagItemList"></div>
</div>
</div>
<div class="bag-panel__footer">
<button class="bag-panel__submit" onclick="submitCheckin()">完成打卡</button>
<button class="bag-panel__submit" onclick="submitCheckin()">提交记录</button>
</div>
</div>
</div>
<!-- 提交成功弹窗 -->
<div class="success-mask" id="successMask">
<div class="success-dialog">
<div class="success-dialog__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="success-dialog__date" id="successDate"></div>
<div class="success-dialog__title">记录成功</div>
<div class="success-dialog__kcal"><span id="successMealName">本餐</span>摄入 <em id="successKcal">0</em> 千卡</div>
<button class="success-dialog__btn" onclick="closeSuccessDialog()">确定</button>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
@@ -416,7 +429,6 @@ var currentCategory = 'common';
var cart = { breakfast: {}, lunch: {}, dinner: {}, snack: {} };
var searchText = '';
var currentDishId = null;
var bagActiveMeal = 'breakfast';
function getMealFromURL() {
var m = (location.search || '').match(/meal=(\w+)/);
@@ -498,6 +510,7 @@ function loadCart() {
function renderCategories() {
var container = document.getElementById('categoryList');
var html = '';
html += '<div class="category-item' + (currentCategory === 'recorded' ? ' active' : '') + '" data-cat="recorded" onclick="selectCategory(\'recorded\')">已记录</div>';
var keys = ['common', 'staple', 'meat', 'vegetable', 'fruit', 'drink', 'snack'];
keys.forEach(function(key) {
var cat = foodData[key];
@@ -532,6 +545,13 @@ function findDish(dishId) {
function renderDishes() {
var container = document.getElementById('dishList');
var empty = document.getElementById('dishEmpty');
// "已记录"分类:展示各餐次已记录的餐品
if (currentCategory === 'recorded') {
renderRecordedDishes();
return;
}
var cat = foodData[currentCategory];
var dishes = cat.items;
@@ -543,6 +563,7 @@ function renderDishes() {
if (dishes.length === 0) {
container.innerHTML = '';
empty.textContent = '暂无匹配餐品';
empty.classList.add('show');
return;
}
@@ -570,6 +591,67 @@ function filterDishes() {
renderDishes();
}
/* ===== 已记录分类列表(汇总各餐次已记录餐品) ===== */
function renderRecordedDishes() {
var container = document.getElementById('dishList');
var empty = document.getElementById('dishEmpty');
var mealNames = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' };
var rows = [];
['breakfast', 'lunch', 'dinner', 'snack'].forEach(function(meal) {
var mealCart = cart[meal] || {};
Object.keys(mealCart).forEach(function(id) {
mealCart[id].forEach(function(e, idx) {
rows.push({ meal: meal, id: id, entry: e, idx: idx });
});
});
});
var kw = searchText.trim();
if (kw) {
rows = rows.filter(function(r) {
var dish = findDish(r.id);
return dish && dish.name.indexOf(kw) !== -1;
});
}
if (rows.length === 0) {
container.innerHTML = '';
empty.textContent = '暂无已记录餐品';
empty.classList.add('show');
return;
}
empty.classList.remove('show');
var html = '';
rows.forEach(function(r) {
var dish = findDish(r.id);
var isAuto = r.entry.source === 'auto';
var srcLabel = isAuto ? '智记' : '自记';
html += '<div class="dish-item">' +
'<div class="dish-item__thumb">' +
'<div class="dish-item__thumb-placeholder">' + (dish ? dish.emoji : '🍽') + '</div>' +
'</div>' +
'<div class="dish-item__info">' +
'<div class="dish-item__name">' + (dish ? dish.name : r.id) +
'<span class="bag-item__src bag-item__src--meal">' + mealNames[r.meal] + '</span>' +
'<span class="bag-item__src bag-item__src--' + (isAuto ? 'auto' : 'manual') + '">' + srcLabel + '</span>' +
'</div>' +
'<div class="dish-item__meta"><em>' + r.entry.kcal + ' 千卡</em> / ' + r.entry.grams + 'g</div>' +
'</div>' +
(isAuto ? '' : '<button class="dish-item__btn dish-item__btn--remove" onclick="removeRecordedItem(\'' + r.meal + '\', \'' + r.id + '\', ' + r.idx + ')">✕</button>') +
'</div>';
});
container.innerHTML = html;
}
function removeRecordedItem(meal, dishId, idx) {
var mealCart = cart[meal];
if (!mealCart || !mealCart[dishId]) return;
mealCart[dishId].splice(idx, 1);
if (mealCart[dishId].length === 0) delete mealCart[dishId];
saveCart();
updateBottomBar();
renderDishes();
if (document.getElementById('bagMask').classList.contains('show')) renderBagItems();
}
/* ===== 克数录入弹窗 ===== */
function openGramPanel(dishId) {
currentDishId = dishId;
@@ -639,16 +721,21 @@ function removeCartItem(dishId) {
}
}
/* ===== 更新底部栏(汇总所有餐次) ===== */
/* ===== 更新底部栏(当前餐次) ===== */
function updateBottomBar() {
var mealCart = cart[currentMeal] || {};
var totalItems = 0;
['breakfast', 'lunch', 'dinner', 'snack'].forEach(function(meal) {
var mealCart = cart[meal] || {};
Object.keys(mealCart).forEach(function(id) {
mealCart[id].forEach(function(e) { totalItems++; });
var totalKcal = 0;
Object.keys(mealCart).forEach(function(id) {
mealCart[id].forEach(function(e) {
totalItems++;
totalKcal += e.kcal;
});
});
document.getElementById('statCount').textContent = totalItems;
document.getElementById('statKcal').textContent = totalKcal;
var badge = document.getElementById('bagBadge');
if (totalItems > 0) {
badge.textContent = totalItems;
@@ -662,13 +749,10 @@ function updateBottomBar() {
submitBtn.disabled = totalItems === 0;
}
/* ===== 已选餐品弹窗 ===== */
/* ===== 已选餐品弹窗(当前餐次) ===== */
function toggleBagPanel() {
var hasAny = false;
['breakfast', 'lunch', 'dinner', 'snack'].forEach(function(meal) {
if (Object.keys(cart[meal] || {}).length > 0) hasAny = true;
});
if (!hasAny) {
var mealCart = cart[currentMeal] || {};
if (Object.keys(mealCart).length === 0) {
showToast('请先选择餐品');
return;
}
@@ -683,9 +767,6 @@ function toggleBagPanel() {
function openBagPanel() {
var mask = document.getElementById('bagMask');
mask.classList.add('show');
// 默认选中当前顶部选择的餐次
bagActiveMeal = currentMeal;
renderBagMealTabs();
renderBagItems();
}
@@ -693,35 +774,12 @@ function closeBagPanel() {
document.getElementById('bagMask').classList.remove('show');
}
function switchBagMeal(meal) {
bagActiveMeal = meal;
renderBagMealTabs();
renderBagItems();
}
function renderBagMealTabs() {
var tabs = document.querySelectorAll('.bag-meal-tab');
tabs.forEach(function(tab) {
var meal = tab.dataset.meal;
tab.classList.toggle('active', meal === bagActiveMeal);
var mealCart = cart[meal] || {};
var count = 0;
Object.keys(mealCart).forEach(function(id) { count += mealCart[id].length; });
var countEl = document.getElementById('bagCount_' + meal);
if (count > 0) {
countEl.textContent = count;
countEl.classList.add('show');
} else {
countEl.classList.remove('show');
countEl.textContent = '';
}
});
}
function renderBagItems() {
var mealCart = cart[bagActiveMeal] || {};
var mealNames = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' };
var mealCart = cart[currentMeal] || {};
var container = document.getElementById('bagItemList');
var summaryEl = document.getElementById('bagSummary');
document.getElementById('bagTitle').textContent = (mealNames[currentMeal] || '') + ' · 已选餐品';
var allIds = Object.keys(mealCart);
var totalItems = 0;
@@ -758,7 +816,7 @@ function renderBagItems() {
'<span class="bag-item__kcal">' + e.kcal + ' 千卡</span>' +
'</div>' +
'</div>' +
(isAuto ? '' : '<button class="bag-item__remove" onclick="removeBagItem(\'' + bagActiveMeal + '\', \'' + id + '\', ' + idx + ')">✕</button>') +
(isAuto ? '' : '<button class="bag-item__remove" onclick="removeBagItem(\'' + currentMeal + '\', \'' + id + '\', ' + idx + ')">✕</button>') +
'</div>';
});
});
@@ -770,17 +828,12 @@ function removeBagItem(meal, dishId, idx) {
if (!mealCart || !mealCart[dishId]) return;
mealCart[dishId].splice(idx, 1);
if (mealCart[dishId].length === 0) delete mealCart[dishId];
saveCart();
renderDishes();
updateBottomBar();
saveCart();
renderBagMealTabs();
renderBagItems();
// 如果所有餐次都为空则关闭弹窗
var hasAny = false;
['breakfast', 'lunch', 'dinner', 'snack'].forEach(function(m) {
if (Object.keys(cart[m] || {}).length > 0) hasAny = true;
});
if (!hasAny) closeBagPanel();
// 当前餐次删空后关闭弹窗
if (Object.keys(cart[meal] || {}).length === 0) closeBagPanel();
}
/* ===== 餐次下拉 ===== */
@@ -807,43 +860,51 @@ function selectMeal(mealKey, mealName, e) {
});
closeMealDropdown();
renderDishes();
updateBottomBar();
}
document.addEventListener('click', function(e) {
if (!e.target.closest('#mealSelector')) closeMealDropdown();
});
/* ===== 完成打卡(汇总所有餐次) ===== */
/* ===== 提交记录(汇总所有餐次) ===== */
function formatToday() {
var now = new Date();
var y = now.getFullYear();
var m = ('0' + (now.getMonth() + 1)).slice(-2);
var d = ('0' + now.getDate()).slice(-2);
return y + '/' + m + '/' + d;
}
function submitCheckin() {
var mealNames = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' };
var totalKcal = 0;
var mealCart = cart[currentMeal] || {};
var totalItems = 0;
var mealSummary = [];
['breakfast', 'lunch', 'dinner', 'snack'].forEach(function(meal) {
var mealCart = cart[meal] || {};
var mealKcal = 0;
var mealItems = 0;
Object.keys(mealCart).forEach(function(id) {
mealCart[id].forEach(function(e) {
totalKcal += e.kcal;
mealKcal += e.kcal;
mealItems++;
totalItems++;
});
var mealKcal = 0;
Object.keys(mealCart).forEach(function(id) {
mealCart[id].forEach(function(e) {
totalItems++;
mealKcal += e.kcal;
});
if (mealItems > 0) mealSummary.push(mealNames[meal] + ' ' + mealItems + '样');
});
if (totalItems === 0) {
showToast('请先选择餐品');
return;
}
showToast('已记录 ' + mealSummary.join('、') + ',共 ' + totalKcal + ' 千卡');
closeBagPanel();
document.getElementById('successDate').textContent = formatToday();
document.getElementById('successMealName').textContent = mealNames[currentMeal] || '本餐';
document.getElementById('successKcal').textContent = mealKcal;
document.getElementById('successMask').classList.add('show');
// 提交后重置为默认数据,保证下次打开仍有"上次记录"
cart = getDefaultCart();
saveCart();
setTimeout(function() {
history.back();
}, 1200);
updateBottomBar();
}
function closeSuccessDialog() {
document.getElementById('successMask').classList.remove('show');
history.back();
}
/* ===== 拍照识别 ===== */
@@ -52,7 +52,6 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
/* ===== 分类列表 ===== */
.category-list { background: #fff; border-radius: 12px; overflow: hidden; height: 100%; display: flex; flex-direction: column; }
.category-list__title { font-size: 12px; font-weight: 700; color: #999; padding: 10px 10px 6px; letter-spacing: 1px; flex-shrink: 0; }
.category-list__items { flex: 1; overflow-y: auto; }
.category-list__items::-webkit-scrollbar { display: none; }
.category-item { padding: 12px 10px; font-size: 13px; color: #666; text-align: center; cursor: pointer; border-left: 3px solid transparent; transition: all .2s; }
@@ -76,6 +75,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.exe-item__btn { width: 28px; height: 28px; border-radius: 50%; border: 1.5px solid #e8e8e8; background: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 16px; color: #666; line-height: 1; transition: all .15s; }
.exe-item__btn:active { background: #f5f5f5; }
.exe-item__btn--plus { color: #52c41a; border-color: #b7ebc4; background: #f6fff6; }
.exe-item__btn--remove { color: #ff4d4f; border-color: #ffccc7; background: #fff1f0; }
/* ===== 底部固定栏 ===== */
.bottom-bar { height: 68px; background: #fff; border-top: 1px solid #f0f0f0; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; gap: 12px; }
@@ -83,7 +83,12 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.bottom-bar__bag svg { width: 22px; height: 22px; color: #52c41a; }
.bottom-bar__bag-badge { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; border-radius: 9px; background: #ff4d4f; color: #fff; font-size: 10px; font-weight: 700; display: none; align-items: center; justify-content: center; padding: 0 4px; line-height: 1; }
.bottom-bar__bag-badge.show { display: flex; }
.bottom-bar__submit { flex: 1; height: 40px; border-radius: 20px; border: none; background: linear-gradient(160deg, #73d13d, #389e0d); color: #fff; font-size: 15px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 8px rgba(82,196,26,0.3); transition: opacity .2s, transform .15s; }
.bottom-bar__stats { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 2px; }
.bottom-bar__stats-item { font-size: 12px; color: #666; line-height: 1.3; white-space: nowrap; }
.bottom-bar__stats-item em { font-style: normal; font-weight: 800; font-size: 14px; }
.bottom-bar__stats-item--count em { color: #52c41a; }
.bottom-bar__stats-item--kcal em { color: #fa8c16; }
.bottom-bar__submit { width: 116px; flex-shrink: 0; height: 40px; border-radius: 20px; border: none; background: linear-gradient(160deg, #73d13d, #389e0d); color: #fff; font-size: 15px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 8px rgba(82,196,26,0.3); transition: opacity .2s, transform .15s; margin-left: auto; }
.bottom-bar__submit:active { transform: scale(0.97); }
.bottom-bar__submit:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; }
.bottom-bar__scan { width: 40px; height: 40px; border-radius: 10px; background: #f7f8fa; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
@@ -155,6 +160,20 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.bag-panel__submit { width: 100%; height: 46px; border: none; border-radius: 23px; background: linear-gradient(160deg, #73d13d, #389e0d); color: #fff; font-size: 16px; font-weight: 700; cursor: pointer; }
.bag-panel__submit:active { opacity: 0.85; }
/* ===== 提交成功弹窗 ===== */
.success-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 60; opacity: 0; visibility: hidden; transition: opacity .25s ease, visibility .25s ease; }
.success-mask.show { opacity: 1; visibility: visible; }
.success-dialog { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); width: 296px; background: #fff; border-radius: 16px; padding: 28px 20px 20px; z-index: 61; text-align: center; transition: transform .25s ease; }
.success-mask.show .success-dialog { transform: translate(-50%, -50%) scale(1); }
.success-dialog__icon { width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(160deg, #73d13d, #389e0d); display: flex; align-items: center; justify-content: center; margin: 0 auto; box-shadow: 0 4px 12px rgba(82,196,26,0.35); }
.success-dialog__icon svg { width: 28px; height: 28px; color: #fff; }
.success-dialog__date { font-size: 13px; color: #999; margin-top: 12px; }
.success-dialog__title { font-size: 18px; font-weight: 700; color: #1a1a1a; margin-top: 4px; }
.success-dialog__kcal { font-size: 13px; color: #666; margin-top: 12px; padding: 10px; background: #fff7e6; border-radius: 10px; }
.success-dialog__kcal em { font-style: normal; font-weight: 800; color: #fa8c16; font-size: 18px; }
.success-dialog__btn { width: 100%; height: 44px; border: none; border-radius: 22px; background: linear-gradient(160deg, #73d13d, #389e0d); color: #fff; font-size: 16px; font-weight: 700; cursor: pointer; margin-top: 16px; box-shadow: 0 2px 8px rgba(82,196,26,0.3); }
.success-dialog__btn:active { opacity: 0.85; }
/* Toast */
.toast { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); background: rgba(0,0,0,0.78); color: #fff; font-size: 14px; padding: 10px 22px; border-radius: 20px; z-index: 100; pointer-events: none; opacity: 0; transition: all .25s ease; white-space: nowrap; }
.toast.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }
@@ -198,7 +217,6 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<!-- 左侧:分类列表 -->
<div class="record-layout__left">
<div class="category-list">
<div class="category-list__title">运动分类</div>
<div class="category-list__items" id="categoryList"></div>
</div>
</div>
@@ -219,7 +237,11 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<div class="bottom-bar__bag-badge" id="bagBadge"></div>
</div>
<button class="bottom-bar__submit" id="submitBtn" disabled onclick="submitCheckin()">完成记录</button>
<div class="bottom-bar__stats">
<div class="bottom-bar__stats-item bottom-bar__stats-item--count">已选 <em id="statCount">0</em></div>
<div class="bottom-bar__stats-item bottom-bar__stats-item--kcal">消耗 <em id="statKcal">0</em> 千卡</div>
</div>
<button class="bottom-bar__submit" id="submitBtn" disabled onclick="submitCheckin()">提交记录</button>
</div>
<!-- 时长录入弹窗 -->
@@ -252,7 +274,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="bag-panel" id="bagPanel" onclick="event.stopPropagation()">
<div class="bag-panel__head">
<div class="bag-panel__handle"></div>
<div class="bag-panel__title">记录运动</div>
<div class="bag-panel__title">运动</div>
</div>
<div class="bag-panel__body">
<div class="bag-panel__list-wrap" style="flex:1;">
@@ -261,11 +283,24 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
</div>
<div class="bag-panel__footer">
<button class="bag-panel__submit" onclick="submitCheckin()">完成记录</button>
<button class="bag-panel__submit" onclick="submitCheckin()">提交记录</button>
</div>
</div>
</div>
<!-- 提交成功弹窗 -->
<div class="success-mask" id="successMask">
<div class="success-dialog">
<div class="success-dialog__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="success-dialog__date" id="successDate"></div>
<div class="success-dialog__title">记录成功</div>
<div class="success-dialog__kcal">今日运动消耗 <em id="successKcal">0</em> 千卡</div>
<button class="success-dialog__btn" onclick="closeSuccessDialog()">确定</button>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
@@ -373,12 +408,16 @@ renderCategories();
selectCategory('common');
updateBottomBar();
function setPageTitle() {
function formatToday() {
var now = new Date();
var y = now.getFullYear();
var m = ('0' + (now.getMonth() + 1)).slice(-2);
var d = ('0' + now.getDate()).slice(-2);
document.getElementById('pageTitle').textContent = '添加' + y + '/' + m + '/' + d + '运动';
return y + '/' + m + '/' + d;
}
function setPageTitle() {
document.getElementById('pageTitle').textContent = '添加' + formatToday() + '运动';
}
/* ===== 持久化:localStorage ===== */
@@ -417,6 +456,7 @@ function loadCart() {
function renderCategories() {
var container = document.getElementById('categoryList');
var html = '';
html += '<div class="category-item' + (currentCategory === 'recorded' ? ' active' : '') + '" data-cat="recorded" onclick="selectCategory(\'recorded\')">已记录</div>';
var keys = ['common', 'cardio', 'strength', 'ball', 'daily', 'flexibility'];
keys.forEach(function(key) {
var cat = exerciseData[key];
@@ -451,6 +491,13 @@ function findExercise(exeId) {
function renderExercises() {
var container = document.getElementById('exeList');
var empty = document.getElementById('exeEmpty');
// "已记录"分类:展示已记录的运动
if (currentCategory === 'recorded') {
renderRecordedExercises();
return;
}
var cat = exerciseData[currentCategory];
var exercises = cat.items;
@@ -462,6 +509,7 @@ function renderExercises() {
if (exercises.length === 0) {
container.innerHTML = '';
empty.textContent = '暂无匹配运动';
empty.classList.add('show');
return;
}
@@ -489,6 +537,62 @@ function filterExercises() {
renderExercises();
}
/* ===== 已记录分类列表 ===== */
function renderRecordedExercises() {
var container = document.getElementById('exeList');
var empty = document.getElementById('exeEmpty');
var srcLabels = { wearable: '穿戴', equip: '器械', wechat: '微信', manual: '自记' };
var rows = [];
Object.keys(cart).forEach(function(id) {
cart[id].forEach(function(e, idx) {
rows.push({ id: id, entry: e, idx: idx });
});
});
var kw = searchText.trim();
if (kw) {
rows = rows.filter(function(r) {
var exe = findExercise(r.id);
return exe && exe.name.indexOf(kw) !== -1;
});
}
if (rows.length === 0) {
container.innerHTML = '';
empty.textContent = '暂无已记录运动';
empty.classList.add('show');
return;
}
empty.classList.remove('show');
var html = '';
rows.forEach(function(r) {
var exe = findExercise(r.id);
var src = r.entry.source || 'manual';
var isManual = src === 'manual';
html += '<div class="exe-item">' +
'<div class="exe-item__thumb">' +
'<div class="exe-item__thumb-placeholder">' + (exe ? exe.emoji : '🏃') + '</div>' +
'</div>' +
'<div class="exe-item__info">' +
'<div class="exe-item__name">' + (exe ? exe.name : r.id) +
'<span class="bag-item__src bag-item__src--' + src + '">' + (srcLabels[src] || '自记') + '</span>' +
'</div>' +
'<div class="exe-item__meta"><em>' + r.entry.kcal + ' 千卡</em> / ' + r.entry.duration + '分钟</div>' +
'</div>' +
(isManual ? '<button class="exe-item__btn exe-item__btn--remove" onclick="removeRecordedItem(\'' + r.id + '\', ' + r.idx + ')">✕</button>' : '') +
'</div>';
});
container.innerHTML = html;
}
function removeRecordedItem(exeId, idx) {
if (!cart[exeId]) return;
cart[exeId].splice(idx, 1);
if (cart[exeId].length === 0) delete cart[exeId];
saveCart();
renderExercises();
updateBottomBar();
if (document.getElementById('bagMask').classList.contains('show')) renderBagItems();
}
/* ===== 时长录入弹窗 ===== */
function openDurPanel(exeId) {
currentExeId = exeId;
@@ -561,10 +665,17 @@ function removeCartItem(exeId) {
/* ===== 更新底部栏 ===== */
function updateBottomBar() {
var totalItems = 0;
var totalKcal = 0;
Object.keys(cart).forEach(function(id) {
cart[id].forEach(function(e) { totalItems++; });
cart[id].forEach(function(e) {
totalItems++;
totalKcal += e.kcal;
});
});
document.getElementById('statCount').textContent = totalItems;
document.getElementById('statKcal').textContent = totalKcal;
var badge = document.getElementById('bagBadge');
if (totalItems > 0) {
badge.textContent = totalItems;
@@ -661,15 +772,13 @@ function removeBagItem(exeId, idx) {
if (Object.keys(cart).length === 0) closeBagPanel();
}
/* ===== 完成记录 ===== */
/* ===== 提交记录 ===== */
function submitCheckin() {
var totalKcal = 0;
var totalItems = 0;
var totalDuration = 0;
Object.keys(cart).forEach(function(id) {
cart[id].forEach(function(e) {
totalKcal += e.kcal;
totalDuration += e.duration;
totalItems++;
});
});
@@ -677,12 +786,18 @@ function submitCheckin() {
showToast('请先选择运动');
return;
}
showToast('已记录 ' + totalItems + ' 项运动,共 ' + totalDuration + '分钟 ' + totalKcal + ' 千卡');
closeBagPanel();
document.getElementById('successDate').textContent = formatToday();
document.getElementById('successKcal').textContent = totalKcal;
document.getElementById('successMask').classList.add('show');
cart = getDefaultCart();
saveCart();
setTimeout(function() {
history.back();
}, 1200);
updateBottomBar();
}
function closeSuccessDialog() {
document.getElementById('successMask').classList.remove('show');
history.back();
}
/* ===== 扫码 ===== */
@@ -428,9 +428,9 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
</div>
<div class="trend-tabs">
<div class="trend-tab active" onclick="switchTrendTab(this, 'steps')">步数趋势</div>
<div class="trend-tab" onclick="switchTrendTab(this, 'kcal')">消耗趋势</div>
<div class="trend-tab" onclick="switchTrendTab(this, 'duration')">时长趋势</div>
<div class="trend-tab active" onclick="switchTrendTab(this, 'steps')">步数变化</div>
<div class="trend-tab" onclick="switchTrendTab(this, 'kcal')">消耗变化</div>
<div class="trend-tab" onclick="switchTrendTab(this, 'duration')">时长变化</div>
</div>
<!-- 步数趋势 Tab -->
@@ -162,6 +162,28 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.report-card__progress-label { display: flex; justify-content: space-between; font-size: 10px; color: #999; margin-top: 4px; }
.hidden { display: none !important; }
/* ===== 修改目标弹窗 ===== */
.modal-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 100; display: flex; align-items: center; justify-content: center; padding: 24px; }
.modal { width: 100%; background: #fff; border-radius: 16px; padding: 20px 16px 16px; max-height: 80%; overflow-y: auto; }
.modal__title { font-size: 16px; font-weight: 700; color: #1a1a1a; text-align: center; margin-bottom: 8px; }
.modal__tip { font-size: 12px; color: #faad14; background: #fffbe6; border-radius: 8px; padding: 8px 10px; margin-bottom: 14px; text-align: center; }
.modal__field { margin-bottom: 12px; }
.modal__field label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; }
.modal__field input { width: 100%; height: 40px; border: 1px solid #e0e0e0; border-radius: 10px; padding: 0 12px; font-size: 14px; color: #1a1a1a; outline: none; }
.modal__field input:focus { border-color: #36cfc9; }
.modal__btns { display: flex; gap: 10px; margin-top: 16px; }
.modal__btn { flex: 1; height: 40px; border: none; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
.modal__btn--cancel { background: #f0f0f0; color: #666; }
.modal__btn--primary { background: #36cfc9; color: #fff; }
.modal__data-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid #f5f5f5; }
.modal__data-row:last-child { border-bottom: none; }
.modal__data-label { font-size: 14px; color: #666; }
.modal__data-val { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.goal-review-note { margin-top: 10px; padding: 10px 12px; background: #fffbe6; border-radius: 10px; }
.goal-review-note__text { font-size: 12px; color: #faad14; font-weight: 600; margin-bottom: 8px; }
.goal-review-note__data { font-size: 12px; color: #666; line-height: 1.8; }
.bottom-safe { height: 16px; flex-shrink: 0; }
</style>
</head>
@@ -334,7 +356,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<span class="goal-data__label">体重</span>
<span class="goal-data__init">73.5<span class="goal-data__unit">kg</span></span>
<span class="goal-data__arrow"></span>
<span class="goal-data__target" style="color:#ff4d4f;">65.0<span class="goal-data__unit">kg</span></span>
<span class="goal-data__target">65.0<span class="goal-data__unit">kg</span></span>
</div>
<div class="goal-data__row">
<span class="goal-data__label">体脂率</span>
@@ -371,7 +393,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
<div class="card">
<div class="card__title">目标信息</div>
<div class="card__title" style="display:flex;align-items:center;justify-content:space-between;"><span>目标信息</span><button id="btnEditGoal" class="sign-btn sign-btn--primary" onclick="openEditGoalModal()" style="width:auto;height:30px;border-radius:15px;font-size:12px;letter-spacing:1px;padding:0 14px;">修改目标</button></div>
<div class="goal-data">
<div class="goal-data__header">
<span class="goal-data__header-spacer"></span>
@@ -406,6 +428,10 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<span class="goal-data__target">78<span class="goal-data__unit">cm</span></span>
</div>
</div>
<div id="goalReviewNote" class="goal-review-note hidden">
<div class="goal-review-note__text">修改目标已提交,正在审核中</div>
<div class="goal-review-note__data" id="goalReviewData"></div>
</div>
</div>
</div>
@@ -415,7 +441,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<span style="display:flex;align-items:center;gap:10px;flex:1;min-width:0;">
<span class="status-banner__icon">🏁</span>
<div>
<div style="font-size:14px;font-weight:700;white-space:nowrap;">活动已结束,恭喜完成目标!</div>
<div style="font-size:14px;font-weight:700;white-space:nowrap;">活动已结束</div>
</div>
</span>
<button class="sign-btn sign-btn--primary" onclick="location.href='my-promise.html'" style="width:auto;height:32px;border-radius:16px;font-size:12px;letter-spacing:1px;padding:0 14px;flex-shrink:0;">承诺书</button>
@@ -854,6 +880,30 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="bottom-safe"></div>
</div>
<!-- 修改目标弹窗 -->
<div id="editGoalModal" class="modal-mask hidden">
<div class="modal">
<div class="modal__title">修改目标</div>
<div class="modal__tip">当前已参加活动,仅限修改一次目标数据</div>
<div class="modal__field">
<label>目标体重(kg</label>
<input id="editGoalWeight" type="number" step="0.1" placeholder="请输入目标体重">
</div>
<div class="modal__field">
<label>目标体脂(%</label>
<input id="editGoalFat" type="number" step="0.1" placeholder="请输入目标体脂">
</div>
<div class="modal__field">
<label>目标腰围(cm</label>
<input id="editGoalWaist" type="number" step="0.1" placeholder="请输入目标腰围">
</div>
<div class="modal__btns">
<button class="modal__btn modal__btn--cancel" onclick="closeEditGoalModal()">取消</button>
<button class="modal__btn modal__btn--primary" onclick="submitEditGoal()">提交</button>
</div>
</div>
</div>
</div>
<script>
@@ -867,7 +917,7 @@ var currentState = getParam('state') || 'review';
/* ===== 初始化视图 ===== */
function initView() {
document.getElementById('navTitle').textContent = currentView === 'activity' ? '活动目标详情' : '个人目标详情';
document.getElementById('navTitle').textContent = '目标详情';
document.querySelectorAll('.type-menu__item').forEach(function(item) {
item.classList.toggle('active', item.dataset.type === currentView);
});
@@ -933,6 +983,29 @@ function switchRecTabP(tab) {
}
initView();
/* ===== 修改目标弹窗 ===== */
function openEditGoalModal() {
document.getElementById('editGoalWeight').value = '65.0';
document.getElementById('editGoalFat').value = '22.0';
document.getElementById('editGoalWaist').value = '78';
document.getElementById('editGoalModal').classList.remove('hidden');
}
function closeEditGoalModal() {
document.getElementById('editGoalModal').classList.add('hidden');
}
function submitEditGoal() {
var w = document.getElementById('editGoalWeight').value || '-';
var f = document.getElementById('editGoalFat').value || '-';
var wa = document.getElementById('editGoalWaist').value || '-';
document.getElementById('goalReviewData').innerHTML =
'目标体重:<em style="font-style:normal;font-weight:700;color:#1a1a1a;">' + w + ' kg</em> ' +
'目标体脂:<em style="font-style:normal;font-weight:700;color:#1a1a1a;">' + f + ' %</em> ' +
'目标腰围:<em style="font-style:normal;font-weight:700;color:#1a1a1a;">' + wa + ' cm</em>';
document.getElementById('goalReviewNote').classList.remove('hidden');
document.getElementById('btnEditGoal').classList.add('hidden');
closeEditGoalModal();
}
</script>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>历史计划 - 员工端 | 健康CQ原型 v2</title>
<title>历史目标 - 员工端 | 健康CQ原型 v2</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; }
@@ -27,7 +27,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.scroll-content { flex: 1; overflow-y: auto; background: #f5f7fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* ===== 计划卡片 ===== */
/* ===== 目标卡片 ===== */
.plan-card { background: #fff; border-radius: 16px; margin: 12px 12px 0; padding: 16px; box-shadow: 0 4px 14px rgba(0,0,0,0.04); cursor: pointer; transition: transform .15s; }
.plan-card__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 10px; }
.plan-card__name { font-size: 15px; font-weight: 700; color: #1a1a1a; flex: 1; min-width: 0; line-height: 1.4; }
@@ -60,7 +60,11 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.empty-state__text { font-size: 14px; }
.hidden { display: none !important; }
.bottom-safe { height: 16px; flex-shrink: 0; }
.bottom-safe { height: 84px; flex-shrink: 0; }
/* ===== 底部按钮 ===== */
.bottom-action { position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 12px 24px; background: linear-gradient(to top, #f5f7fa 70%, rgba(245,247,250,0)); z-index: 10; }
.bottom-action__btn { width: 100%; height: 44px; border: none; border-radius: 22px; background: #36cfc9; color: #fff; font-size: 15px; font-weight: 600; letter-spacing: 1px; cursor: pointer; }
</style>
</head>
<body>
@@ -81,25 +85,25 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="nav-back" onclick="history.back()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</div>
<div class="nav-title">历史计划</div>
<div class="nav-title">历史目标</div>
<div class="nav-placeholder"></div>
</div>
<!-- Tab 切换 -->
<div class="tab-bar">
<div class="tab-item active" data-tab="activity" onclick="switchTab('activity')">活动计划</div>
<div class="tab-item" data-tab="personal" onclick="switchTab('personal')">个人计划</div>
<div class="tab-item active" data-tab="activity" onclick="switchTab('activity')">活动目标</div>
<div class="tab-item" data-tab="personal" onclick="switchTab('personal')">个人目标</div>
</div>
<div class="scroll-content">
<!-- ===== 活动计划列表 ===== -->
<!-- ===== 活动目标列表 ===== -->
<div id="tabActivity">
<div class="plan-card" onclick="location.href='goal-detail-v2.html?type=activity&state=signed'">
<div class="plan-card__header">
<span class="plan-card__name">2026年二季度体重管理活动</span>
<span class="plan-card__status plan-card__status--active">进行中</span>
</div>
</div>
<div class="plan-card__meta">
<span>起止:<em>2026-04-01 ~ 2026-09-30</em></span>
<span>机构:<em>公司机关</em></span>
@@ -121,7 +125,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
</div>
<div class="plan-card__footer">
<span class="plan-card__date">签署时间:2026-04-05 09:15</span>
<span class="plan-card__date">参加时间:2026-04-05 09:15</span>
<a class="plan-card__link" href="goal-detail-v2.html?type=activity&state=signed" onclick="event.stopPropagation()">详情 <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></a>
</div>
</div>
@@ -159,7 +163,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="plan-card" onclick="location.href='goal-detail-v2.html?type=activity&state=ended'">
<div class="plan-card__header">
<span class="plan-card__name">全员健康体重管理计划</span>
<span class="plan-card__name">全员健康体重管理目标</span>
<span class="plan-card__status plan-card__status--done">已结束</span>
</div>
<div class="plan-card__meta">
@@ -191,7 +195,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="bottom-safe"></div>
</div>
<!-- ===== 个人计划列表 ===== -->
<!-- ===== 个人目标列表 ===== -->
<div id="tabPersonal" class="hidden">
<div class="plan-card" onclick="location.href='goal-detail-v2.html?type=personal&state=signed'">
<div class="plan-card__header">
@@ -225,7 +229,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="plan-card" onclick="location.href='goal-detail-v2.html?type=personal&state=ended'">
<div class="plan-card__header">
<span class="plan-card__name">2025年四季度塑形计划</span>
<span class="plan-card__name">2025年四季度塑形目标</span>
<span class="plan-card__status plan-card__status--done">已完成</span>
</div>
<div class="plan-card__meta">
@@ -258,6 +262,10 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
<div class="bottom-action">
<button class="bottom-action__btn" onclick="location.href='goal-management-v2.html'">新建目标</button>
</div>
</div>
<script>
@@ -405,10 +405,10 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="confirm-dialog" onclick="event.stopPropagation()">
<div class="confirm-dialog__title">确认提交</div>
<div class="confirm-dialog__section">
<div class="confirm-dialog__label">活动信息</div>
<div class="confirm-dialog__row"><span>活动名称</span><strong id="cfmActName">--</strong></div>
<div class="confirm-dialog__label" id="cfmInfoLabel">活动信息</div>
<div class="confirm-dialog__row"><span id="cfmNameLabel">活动名称</span><strong id="cfmActName">--</strong></div>
<div class="confirm-dialog__row"><span>起止日期</span><span id="cfmActPeriod">--</span></div>
<div class="confirm-dialog__row"><span>组织机构</span><span id="cfmActOrg">--</span></div>
<div class="confirm-dialog__row" id="cfmOrgRow"><span>组织机构</span><span id="cfmActOrg">--</span></div>
</div>
<div class="confirm-dialog__section">
<div class="confirm-dialog__label">初始数据</div>
@@ -494,6 +494,9 @@ function submitGoal() {
if (type === 'activity') {
if (!selectedActId) { alert('请先选择活动'); return; }
var el = document.querySelector('.sheet-item[data-id="' + selectedActId + '"]');
document.getElementById('cfmInfoLabel').textContent = '活动信息';
document.getElementById('cfmNameLabel').textContent = '活动名称';
document.getElementById('cfmOrgRow').classList.remove('hidden');
document.getElementById('cfmActName').textContent = el.dataset.name;
document.getElementById('cfmActPeriod').textContent = el.dataset.period;
document.getElementById('cfmActOrg').textContent = el.dataset.org;
@@ -506,6 +509,11 @@ function submitGoal() {
if (!sd) { alert('请选择开始日期'); return; }
if (!ed) { alert('请选择结束日期'); return; }
if (ed <= sd) { alert('结束日期必须晚于开始日期'); return; }
document.getElementById('cfmInfoLabel').textContent = '个人目标信息';
document.getElementById('cfmNameLabel').textContent = '目标名称';
document.getElementById('cfmOrgRow').classList.add('hidden');
document.getElementById('cfmActName').textContent = name;
document.getElementById('cfmActPeriod').textContent = sd + ' ~ ' + ed;
}
document.getElementById('confirmDialog').classList.add('show');
document.getElementById('confirmMask').classList.add('show');
@@ -311,6 +311,21 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.sheet__submit-bar { padding: 14px 16px 0; flex-shrink: 0; }
.sheet__submit { display: block; width: 100%; height: 46px; background: linear-gradient(135deg, #5ad9d3, #36cfc9); color: #fff; border: none; border-radius: 23px; font-size: 16px; font-weight: 600; cursor: pointer; letter-spacing: 6px; }
.sheet__submit:active { opacity: 0.85; }
/* ===== 保存成功弹窗 ===== */
.success-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 60; opacity: 0; visibility: hidden; transition: opacity .25s ease, visibility .25s ease; }
.success-mask.show { opacity: 1; visibility: visible; }
.success-dialog { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); width: 296px; background: #fff; border-radius: 16px; padding: 28px 20px 20px; z-index: 61; text-align: center; transition: transform .25s ease; }
.success-mask.show .success-dialog { transform: translate(-50%, -50%) scale(1); }
.success-dialog__icon { width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #5ad9d3, #36cfc9); display: flex; align-items: center; justify-content: center; margin: 0 auto; box-shadow: 0 4px 12px rgba(54,207,201,0.35); }
.success-dialog__icon svg { width: 28px; height: 28px; color: #fff; }
.success-dialog__date { font-size: 13px; color: #9aa3ad; margin-top: 12px; }
.success-dialog__title { font-size: 18px; font-weight: 700; color: #1a1a1a; margin-top: 4px; }
.success-dialog__data { font-size: 13px; color: #666; margin-top: 12px; padding: 10px; background: #e8fbfa; border-radius: 10px; }
.success-dialog__data em { font-style: normal; font-weight: 800; color: #36cfc9; font-size: 20px; }
.success-dialog__sub { font-size: 12px; color: #9aa3ad; margin-top: 8px; }
.success-dialog__btn { width: 100%; height: 44px; border: none; border-radius: 22px; background: linear-gradient(135deg, #5ad9d3, #36cfc9); color: #fff; font-size: 16px; font-weight: 700; cursor: pointer; margin-top: 16px; box-shadow: 0 2px 8px rgba(54,207,201,0.3); }
.success-dialog__btn:active { opacity: 0.85; }
</style>
</head>
<body>
@@ -343,7 +358,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
</div>
</div>
<span class="nav-action" onclick="location.href='goal-history-v2.html'">历史计划</span>
<span class="nav-action" onclick="location.href='goal-history-v2.html'">历史目标</span>
</div>
<div class="type-mask" id="typeMask" onclick="closeTypeMenu()"></div>
@@ -874,6 +889,20 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</div>
</div>
<!-- ====== 保存成功弹窗 ====== -->
<div class="success-mask" id="successMask">
<div class="success-dialog">
<div class="success-dialog__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div class="success-dialog__date" id="successDate"></div>
<div class="success-dialog__title">记录成功</div>
<div class="success-dialog__data">今日体重 <em id="successWeight">0</em> kg</div>
<div class="success-dialog__sub" id="successSub" style="display:none;"></div>
<button class="success-dialog__btn" onclick="closeSuccessDialog()">确定</button>
</div>
</div>
</div>
<script>
@@ -1128,6 +1157,21 @@ function submitRecord() {
if (latestDateEl) latestDateEl.textContent = dateStr;
closeRecordSheet();
// 保存成功弹窗,展示本次记录的体重数据
var subParts = [];
if (rec.fat !== undefined) subParts.push('体脂率 ' + rec.fat + '%');
if (rec.waist !== undefined) subParts.push('腰围 ' + rec.waist + 'cm');
var subEl = document.getElementById('successSub');
subEl.textContent = subParts.join(' · ');
subEl.style.display = subParts.length ? '' : 'none';
document.getElementById('successDate').textContent = dateStr.replace(/-/g, '/');
document.getElementById('successWeight').textContent = rec.weight;
document.getElementById('successMask').classList.add('show');
}
function closeSuccessDialog() {
document.getElementById('successMask').classList.remove('show');
}
function renderRecordFields() {
@@ -95,10 +95,6 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
.summary-compare.hide-target td:nth-child(4) { display: none; }
/* 标签 */
.summary-tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; margin-left: 4px; }
.summary-tag--good { background: #e6fffb; color: #36cfc9; }
.summary-tag--warn { background: #fffbe6; color: #faad14; }
.summary-tag--info { background: #e6f7ff; color: #1890ff; }
.bottom-safe { height: 20px; flex-shrink: 0; }
</style>
@@ -195,7 +191,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</tr>
<tr>
<td>BMI</td><td>27.7</td><td>23.6</td><td class="val-target">21.1</td>
<td class="val-down">↓ 4.1 <span class="summary-tag summary-tag--good">已正常</span></td>
<td class="val-down">↓ 4.1</td>
</tr>
<tr>
<td>体脂率 (%)</td><td>28.5</td><td>20.0</td><td class="val-target">18.0</td>
@@ -203,7 +199,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
</tr>
<tr>
<td>腰围 (cm)</td><td>89.5</td><td>76.5</td><td class="val-target">72.0</td>
<td class="val-down">↓ 13.0 <span class="summary-tag summary-tag--good">已达标</span></td>
<td class="val-down">↓ 13.0</td>
</tr>
</tbody>
</table>