Compare commits

...
86 Commits
Author SHA1 Message Date
liuyunqin 2fc599fabe feat: 体检模块侧边栏消歧与统计页重构;一线医疗床位页筛选交互升级
- 健康体检:侧边栏「员工体检报告」按类型消歧为常规/胃肠镜/冠脉CTA体检报告
- 「结果动态综合统计」更名「项目结果综合统计」,新增图表/列表双 Tab、级联筛选、表格分页与报告详情弹窗
- 新增「体检影像管理」分组(用户体检信息/影像文件分类/影像文件详细/文件处理日志)
- 员工体检大表新增高级查询;单位体检项目统计等页面同步侧边栏文案
- 一线医疗:楼栋/床位/站点相关页面单位部门筛选改为级联组件+站点搜索下拉,新增地图选点,调整表格列
2026-09-20 18:01:48 +08:00
liuyunqin 80b0c78bdd fix: 体检预约跟踪侧边栏菜单位置调整至已检员工之后 2026-09-20 10:50:41 +08:00
fengpu 4767c24d3d Merge branch 'master' of https://gitea.rk-health.com/yixiong/platform-prototype 2026-09-20 10:48:54 +08:00
fengpu 44d7d0b219 feat: 导航页样式优化 2026-09-20 10:48:51 +08:00
liuyunqin 8e6c10f0bc feat: 健康体检检后新增结果动态综合统计;补全6个页面文档并重拍截图;体重管理「身高秤」统一更名「体重秤」
- 检后「体检统计汇总」新增「结果动态综合统计」页面(2×2 网格4张堆积柱状图,维度固定性别/年龄/部门/岗位,纯手写SVG),23个检后页面侧边栏及 index.html SITE_MAP 同步新增入口
- 按实际页面补全6个此前未入册的页面文档并配截图:检中体检预约跟踪、检后单位群体报告、检后体检影像管理4页;PRD/CHANGELOG/页面清单编号顺延
- 体重管理「身高秤」来源统一更名为「体重秤」(员工端/后台/应急端 PRD 及页面同步)
2026-09-20 10:34:12 +08:00
fengpu a900886061 Merge branch 'master' of https://gitea.rk-health.com/yixiong/platform-prototype 2026-09-18 17:45:32 +08:00
fengpu 71ddc7fa0a feat: 健康服务APP中新增营养师角色 ,设计营养师工作台页面原型 2026-09-18 17:45:29 +08:00
fengpu 63407a20c2 feat:健康体检后台检后新增影像数据管理;基础配置中新增前置服务器菜单 2026-09-18 17:44:26 +08:00
liuyunqin 5e479bc6fe feat: 健康体检与一线医疗PRD同步
健康体检:
- 新增「体检预约跟踪」「单位群体报告」「单位体检项目统计」三个页面并接入侧边栏与首页导航
- 员工体检大表增加结果/单位/参考范围三列、统计行与按结果排序
- 侧边栏统一「员工体检报告」命名,移除「患病率」列
- 单位体检费用统计移除部门列并加宽费用明细弹窗

一线医疗:
- 救护车配置表单字段重组,新增年份选择控件、GPS/车辆照片,移除批量删除
- 救护车出勤筛选改为单位→医疗点级联 + 车牌号搜索,出勤类型扩展为六类
- 侧边栏「救护车出勤」菜单项调整位置
2026-09-18 17:30:07 +08:00
fengpu b0479ee9d1 feat: 检后模块新增影像数据管理菜单 2026-09-18 11:51:58 +08:00
lianlonggang c3c51978de docs: 体检须知预约短信在模板中体现,这里不用 2026-09-18 08:52:14 +08:00
liuyunqin 3ae4e98f9a docs: 健康体检PRD同步17个页面最新改动并重截16张截图(报告弹窗三页签专业版式、封面图/视频上传提示、同步医院体检项目弹窗、三类计划体检年份锁定、组合模板金额列、批量导入人员、预选统计移除医院Tab、预约信息列后移、实时预约日期默认当日起30日、CTA统计移除问卷列、常规报告补录与确认弹窗、员工费用明细筛选导出、胃肠镜查看报告、结果异常精简);screenshot-prd.js支持传入页面名前缀只重截指定页 2026-09-17 18:31:16 +08:00
liuyunqin cbf21d1817 feat: 医疗PAD健康宣教抽屉移除时长字段 2026-09-17 18:10:30 +08:00
liuyunqin f1f412d897 feat: 体重管理基础信息移除身份证号与手机号展示 2026-09-17 18:10:30 +08:00
liuyunqin 05f83885ac feat: 健康体检后台单位体检费用统计按单位×体检医院拆行
- 列表增加体检医院/签约医院列并去掉总人数列,原6条单位汇总拆为16条医院明细行
- 缺少项目弹窗增加项目费用/折扣后费用列,统计明细与员工明细的缺少项目数标蓝可点击
- 员工费用明细弹窗新增筛选栏与导出列表数据按钮
- 同步更新 PRD 与 CHANGELOG
2026-09-17 18:10:24 +08:00
liuyunqin 9cd970a31e feat: 一线医疗身份证与联系电话去除脱敏 · 员工分配统计概览卡改员工口径
- 全站演示数据身份证号与联系电话由脱敏改为完整展示
- config-allocate-stat 概览卡改为员工总数量/已分配/未分配三卡,名册统计文案上移
- 同步更新 CHANGELOG
2026-09-17 18:10:16 +08:00
liuyunqin a54d933841 feat: 健康体检员工端取消预约二次确认、胃肠镜预选可重填及报告筛选调整
- 预约日期页取消预约改为二次确认
- 胃肠镜医院预选支持重新填写调整(至少2家/最多3家)
- 现场体检状态口径改为已预约/无预约
- 报告列表新增年份/类型筛选
- 同步更新 PRD 与演示截图
2026-09-17 18:10:07 +08:00
fengpuandClaude Opus 4.7 489a72838e feat: 健康体检后台端与员工端导航页标记为已完成并挂载后台PRD
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-15 19:58:16 +08:00
fengpuandClaude Opus 4.7 2dfc61bedf feat: 健康体检新增前置机管理(常规体检前置服务)页面及PRD
参照老系统 hospitalAgent 复刻医院前置机设备运维管理,新增常规体检前置服务页面(设备列表+6弹窗),同步 SITE_MAP 与 PRD 文档。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-15 19:46:14 +08:00
fengpu c85d9c124e feat: 体检系统医院前置机、影像中心、体检医生端需求说明文档 2026-09-15 18:47:05 +08:00
liuyunqin 3e7cd268fa feat: 健康体检员工端新增页面总览并纳入站点地图 2026-09-15 17:59:47 +08:00
liuyunqin d5756021b6 feat: 健康体检精简页面并统一术语为已参检
- 删除到院体检(hospital-checkin)与未开展计划(index-inactive)页面及对应截图
- 术语统一:已体检→已参检,操作流程讲解→操作说明
- 选择体检类型页优化:移除体检状态徽标,字段改为计划起止日期/预约开放日期/当前所属群体
- 补充预约项目AI解读、提交及胃肠镜预选/问卷相关截图
- 同步更新PRD文档与全站导航配置
2026-09-15 14:51:00 +08:00
lianlonggangandClaude Code bdc44daf24 docs: 健康体检PRD补充6处需求澄清说明及CHANGELOG记录
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 14:21:26 +08:00
liuyunqin dcacbe289b feat: 健康体检体检年份筛选控件统一及PRD文档与截图补充
- 管理端健康体检各页体检年份筛选由原生select替换为自定义下拉控件
- 导航标签精简(体检预约/已选项目/已检员工等)
- 单位报表未检人数弹窗移除未检原因列
- 补充健康体检管理端与员工端PRD文档、截图脚本及页面截图
2026-09-14 19:20:51 +08:00
fengpuandClaude Opus 4.7 39547a8bb6 feat: 餐品定价规格拆分为餐品规格与餐品净菜包规格
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-14 15:45:34 +08:00
fengpuandClaude Opus 4.7 bf08a9614b feat: Web管理端新增系统管理模块18页原型及全站脱敏修正
- 系统管理模块首版:用户管理5页、权限管理3页、机构管理2页、系统配置4页、通知管理4页,配套新建模块 CHANGELOG
- 站点地图 SITE_MAP 新增系统管理模块条目(入口 user-admin.html)
- 全站脱敏修正:健康长庆规划文档、一线医疗合作医院、应急大屏 CHANGELOG 中「长庆油田」统一为「CQ能源」
- 蓝牙秤小程序资料完善页按钮文案「完成并进入首页」改为「开始使用」

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-11 17:51:51 +08:00
liuyunqin 17595d5fb2 feat: 一线医疗员工/非员工就诊融合为日常诊疗管理及医疗PAD体检报告完善 2026-09-11 15:11:11 +08:00
liuyunqin 3800631e88 feat: 单位费用统计页新增导出员工明细按钮 2026-09-11 08:55:00 +08:00
fengpuandClaude Opus 4.7 18dcb35851 feat: 记忆体系同步更新
- project_progress:补记一线医疗/健康体检/医疗点PAD/文档规范等 11 条里程碑,更新模块状态与总计
- project_overview/user_profile:补 frontmatter,三端架构新增 frontline-medical 模块
- MEMORY.md:Base commit 更新,补 project_database_postgresql 索引条目
- lint:修复 slug 断链引用,收敛 frontmatter 缺失与过期问题
- CLAUDE.md:记忆引导区块补充 project_database_postgresql

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-10 17:19:00 +08:00
fengpuandClaude Opus 4.7 b5a8b83e6d feat: 文档技能包与模板规范完善
- 截图规范统一:JPEG 质量85% + 分终端目录 + 只截主界面/详情页 + 全屏视口(1440×900)
- 内容通用化:全文去除客户名称与项目名称,文档可供任意客户阅读
- 交付格式规范:.md 为源 + 竖版 A4 PDF 最终交付,明确分页三原则
- PRD 合并优化:系统功能清单 HTML 表格(rowspan)替代页面清单,清理 .html 文件名与来源说明区
- 业务架构图模板重构:应用与服务层改横排竖版板块 + 层间横向分隔线
- .gitignore 忽略 /交付文档/ 目录;记忆新增操作手册截图规范

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-10 17:07:54 +08:00
liuyunqin 256b741c8f feat: 员工端体检指标对比详情移除BMI/总胆固醇/甘油三酯的恶化趋势标签 2026-09-10 14:27:14 +08:00
liuyunqin 97652d797f feat: 员工端新增一线医疗模块及医疗点PAD用药信息调整
- 新增 employee-app/frontline-medical 一线医疗模块共22个页面
- index.html 站点地图挂载一线医疗入口与页面清单
- 医疗点PAD端随访/日常用药卡片信息对齐(用法用量、价格、数量)
2026-09-10 14:00:43 +08:00
fengpu 9434681a80 feat:完善文档编写的技能包和模板文件 2026-09-10 11:53:07 +08:00
fengpu 982cb1c068 feat:整理产品文档生成技能包以及文档模块 2026-09-09 18:17:21 +08:00
liuyunqin 8a3a116501 feat: 医疗点PAD端细节优化与管理端体检统计二级/三级单位切换
医疗点PAD端(other-module/medical-pad/):
- 退出登录统一加二次确认(confirm弹窗后再回登录页)
- 全站字段名规范:工号统一为员工编号
- 随访抽屉:新增/编辑双模式,任务中心带参进入编辑态隐藏搜索并回填员工信息、标题联动编辑随访
- 健康宣教抽屉:签字方式改为手写签名/签字图片二选一(图片上传预览),去掉负责人字段,活动资料加文件不超过10M提示
- 报告解读抽屉:新增查看详情模式(?view=1纯显示只读,隐藏删除/重新解读,签字静态化)
- 非员工就诊抽屉:手机号改联系电话、去掉身份证自动解析的性别/年龄只读字段
- 员工档案详情:新增体检历史/分析报告/就诊记录/随访记录/就医推荐Tab,体检报告查看弹窗,就医推荐地图医疗点卡片
- 健康监测页:事件列表由表格改为双列卡片按事件类型着色,新增定位地图,搜索栏改为更多筛选展开收起,移除仅A01A34医疗点可见提示
- 床头呼叫等页面搜索栏统一重构为更多筛选展开收起

管理端健康体检(web-admin/health-checkup/):
- 冠脉CTA/胃肠镜统计页新增二级/三级单位切换TAB,二级按单位聚合并隐藏部门列与部门筛选,三级按部门展示
- 新增受检率/检出率/患病率三列,表头问号悬浮气泡说明计算口径
2026-09-09 18:08:00 +08:00
fengpu 03a186fc7d Merge branch 'master' of https://gitea.rk-health.com/yixiong/platform-prototype 2026-09-09 15:16:25 +08:00
fengpu 91efe28d41 feat:一线医疗点后台原型-细节对齐和菜单重组 2026-09-09 15:16:22 +08:00
fengpu bc77229275 feat:之前同事误改的厨配秤恢复成净菜加工秤 2026-09-09 15:15:32 +08:00
liuyunqin 26419bc1de feat: 健康体检三端与医疗点PAD、体重管理多模块更新(员工端冠脉CTA体检接入:首页Banner新增第四张CTA暖红渐变卡片与圆点随张变色、reports页新增CTA报告条目共12份、新增cta-date/cta-hospital/report-cta四个CTA页面;医疗点PAD端:随访抽屉改为携带员工信息跳转goFollow带参回显、下次随访/随访效果/签字方式(手写+签字图片)/新增用药/巡诊任务(频次/单次/申请随诊)重构、单位名称规范化为采油一厂-信息部等、登录去掉校验直接进入系统、删除五类人群登记页;管理端健康体检:档案单位报表表头重做单层23列新增体检年份列与过程漏斗/疾病专项/费用三块口径、人数列点击弹窗人员名单+导出CSV、个人体检数据新增操作列查看报告弹窗、员工大表与个人档案新增异常员工下拉筛选、检前组合模板/例外配置/必检项目防癌筛查TAB融合、医院体检项目费用更名医院体检项目、检前侧边栏统一重建与CTA分组;管理端体重管理:结果异常页表格错位修复+异常判定收敛为五类指标+异常等级移除、PRD结果异常节同步;各模块CHANGELOG记录) 2026-09-08 17:56:05 +08:00
fengpu 60b4e38166 feat: 本地暂存一线医疗点细节补全 2026-09-08 17:21:57 +08:00
fengpu 55819e077a feat: 支持用户按餐次自定义营养素配比 2026-09-08 15:25:36 +08:00
fengpu 0bc9a33aec feat: 记忆文件 2026-09-08 13:58:11 +08:00
fengpuandClaude Opus 4.7 47ab38e28d feat: 一线医疗管理端二线迭代(45页改版+3页新增:侧栏二级分组改造——基础配置(药配/分配/救护车)、日常业务(日常诊疗/用药记录/任务管理)、床头呼叫(设备管理/宿舍管理)收敛为可展开收起子组,默认收起带交互注释;床头呼叫新增站点管理页(单位部门筛选+台账列表+双列表单弹窗)并更名楼栋/宿舍/床位信息、分配管理、告警记录、汇总统计,顶栏入口调整为设备列表;数据统计重排——救护车统计两页由基础配置移入,表格/图表双页收拢为单一入口页内图表/报表view-switch互切,新增救助物品统计图表页(三环形占比图)与报表页(小药棒/药品箱/急救包分组表头本期累计明细)共享导入工具条;全站视觉尺寸升级font-xs→font-sm、控件高32→36、表格padding与min-width上调、表单双列栅格与下拉箭头/上传框通用样式;首页SITE_MAP同步新增/移域/更名条目并移除done状态回退设计中,CHANGELOG记录)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-08 13:52:07 +08:00
fengpu b4f3d5cb8c feat: 一线医疗管理端首版原型(45页覆盖档案/基础配置/日常业务/床头呼叫/数据统计/健康小屋/异常事件七大分区:就医与单位报表数据、算法模型、医疗点/合作医院/医护人员/药品与库存/分配/救护车配置及统计双视图、员工与非员工就诊、出入库、随访与慢病干预、员工档案、解读任务、健康宣教、楼栋宿舍床位设备四级管理与分配/呼叫/超时告警/统计、医疗点统计双视图、小屋轮播/健康室/预约/排班/大屏/小屋管理与统计、脏数据与结果异常;首页SITE_MAP同步一线医疗全量页面并将医疗点PAD端入口调整为任务中心;新增模块CHANGELOG,移除目录占位.gitkeep) 2026-09-07 18:56:08 +08:00
liuyunqin d76285aa4a feat: 健康体检管理端新增员工体检大表与冠脉CTA体检分组(检后新增员工体检大表页,三层合并表头95列含重点项目85列与异常红色高亮、冻结前三列,个人体检数据档案页整体替换照搬该大表;检前/检中/检后侧边栏各新增冠脉CTA体检二级分组共复刻8个页面,医院承检范围/单位计划管理/预约跟踪/单位预约统计/CTA报告(专业报告版式弹窗含钙化积分与CAD-RADS)/CTA统计,特殊人员配置下冠脉CTA体检人员页挪入新分组;检后新增单位体检费用统计页含单位/部门TAB与员工费用明细及项目列表二层弹窗;单位体检人数统计新增受检率/检出率/患病率三列带公式气泡并移除大病人数列;20+检后/13检中/21检前页面侧边栏统一重建,首页站点地图同步新增入口,CHANGELOG记录) 2026-09-07 17:55:06 +08:00
fengpu fbe7defafa feat : 贴标秤缩略版标签设计 2026-09-07 17:03:59 +08:00
fengpu 708f1c29e2 feat:更新AI记忆文件 2026-09-07 17:03:19 +08:00
fengpuandClaude Opus 4.7 891678d97e feat: 净菜类型拆分独立管理(食物成分页操作区食材分类管理后新增净菜管理入口;净菜类型新增规格行级可食部(%)字段,位于规格别名后,逐规格独立维护并同步新增/编辑/详情三页;新增净菜管理二级页:统计卡片、食材名称下拉搜索/食材编码/净菜类型三源关键字/切配形状筛选、异步导出按钮组(导出列表数据+查看导出任务,样式对齐列表页惯例)、组级列rowspan合并+可食部/规格别名/切配规格/刀工按规格逐行拆分(组内虚线分隔、规格列组纵向分隔线)、先选食材回显已有净菜类型再新增/编辑的维护弹窗;首页SITE_MAP与营养模块CHANGELOG同步)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-07 16:47:00 +08:00
liuyunqin 7f184709f3 fix: 全仓截图脚本与截图质量修复(assistant/expert/health-monitor 多处截图脚本中 index-v2.html 等失效页面引用改回现存文件名并清理孤儿任务与无引用截图;weight-main-v2 初始化改为解析 ?type= 参数修复设计总览六种目标状态截图雷同问题,蓝牙秤截图任务补 #paired 参数并重拍 weight-main 系列与 bluetooth-scale 共10张截图、删除 weight-main-menu;专家端与管理端补拍 PRD 引用但从未生成的视频进行中截图 video-in-progress.jpg 与 video-detail-in-progress.jpg;体重管理 PRD 修复两处截图断链,设计总览主页节移除切换菜单卡片,各模块 CHANGELOG 同步记录) 2026-09-07 10:37:08 +08:00
fengpuandClaude Opus 4.7 15ca37b892 feat: 医疗点PAD端首版原型(1280×800横屏20页含登录/任务中心/日常诊疗/随访/慢病干预/健康宣教/员工档案/健康监测/床头呼叫/报告解读,抽屉交互页内实现,站点地图同步与项目背景尺寸规范更新,新增模块CHANGELOG);全站演示数据脱敏(示例人名王建国改石维国、周明远改曲志明,机构长庆油田改CQ油田,覆盖应急/体检/体重/营养/餐厅终端/大屏等约30页);现场体检签到码由图标切换改为条形码/二维码双选项卡切换;新增健康长寿系统升级改造详细规划V8文档
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-04 17:51:21 +08:00
liuyunqin 29a09e9402 docs: 体重管理员工端设计总览重构(总览页按目标承诺与签署/体重主页/每日打卡/历史记录/建议与报告/蓝牙设备六大主线重排并改密卡片尺寸与横屏卡片样式,页面清单全面切换到 weight-main-v2/goal-management-v2 等 v2 新版页面,新增 playwright 统一截图脚本 screenshot-weight.mjs 并按其重新生成全部截图,首页站点地图体重管理条目同步更新为 v2 页面列表) 2026-09-04 16:17:00 +08:00
liuyunqin 05e157e667 feat: 健康体检原型调整(体检报告模板全站更名体检导入模板管理、模板额度配置更名群体额度配置且同步选项改为忽略手动修改,大病人员信息新增新增大病人员弹窗含员工搜索下拉与信息卡片且通知列改标签并新增发送通知操作,指标异常通知页固定前三列与操作列并新增发送通知/发送短信,单位/胃肠镜单位预约统计改用页面顶部单位级别TAB且三级单位显示部门列并新增体检年份筛选,胃肠镜预约跟踪移除体检总费用列与详情费用块,胃肠镜问卷新增就诊卡号列,已选项目前置机体检状态筛选改为前置机预约状态,已参检员工信息移除录入大病信息按钮) 2026-09-04 11:20:10 +08:00
fengpuandClaude Opus 4.7 118d143767 feat: 健康体检员工端原型完善(选项目页费用横幅重做为体检额度卡片含额度使用进度条与必检/选检/可用额度三格、年度额度调至1600并新增心脏彩超与胃幽门螺杆菌两项选检、提交确认弹窗精简为项目总额/必检项目/体检总额度/可用额度四行;常规与胃肠镜选医院页移除医院承检类型标签;选日期页文案改为"预约开放日期";胃肠镜预约结果页新增成功/失败两态演示(结果横幅、失败原因详情弹窗、重新预约按钮),常规与胃肠镜结果页失败态隐藏体检须知;"我的预约"页面更名重做为"现场体检"(移除已取消状态面板与已选项目展示,胃肠镜项目列表简化为静态已检/未检),首页站点地图/功能卡、到院体检页返回按钮、非活动首页、办理流程文案同步更名;首页未开启计划文案2027调整为2026;指标纵向对比详情异常结论去红字高亮并两行截断;消息中心AI风险预警示例由痛风改为2型糖尿病)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-04 10:44:10 +08:00
liuyunqin 2213106961 feat: 健康体检原型调整(检中侧边栏员工预约跟踪更名员工预约体检跟踪,已选项目页筛选项合并单位部门并精简日期筛选项、新增医院体检状态列、移除同步预约按钮,特殊项目/大病/指标异常等页面统一新增员工编号筛选与列,体检编号更名体检编码,系统体检状态选项补齐未体检,胃肠镜特殊项目页体检年份改为下拉默认当年) 2026-09-03 20:01:27 +08:00
liuyunqin 3a838d04a2 feat: 健康体检原型完善(胃肠镜体检计划管理新增新增/编辑/详情抽屉支持承检医院多选与机构树选人,医院基础信息列表加图片列并统一常规/胃肠镜医院编辑抽屉,报告模板详情与编辑复用同一弹窗含只读模式,组合模板/例外配置/医院费用/单位费用新增详情弹窗与编辑抽屉及生成费用确认弹窗,单位体检计划新增抽屉含允许预约日期范围,名额分组与材料操作说明弹窗抽屉,检中页面筛选项重排、预约跟踪多级表头,特殊项目页列表字段调整,档案页筛选排序统一) 2026-09-03 14:43:25 +08:00
fengpuandClaude Opus 4.7 37ae0543a1 feat: 健康体检胃肠镜预选与预约流程完善(新增胃肠镜医院预选征集页与已提交页、检前首页预选通知条,唐都医院问卷新增就诊卡必填题与办理流程弹窗,选日期页新增预约状态条与可约窗口限制(常规近30天/胃肠镜7~30天)与提交loading过渡,预约结果页新增成功/失败演示切换与取消预约二次确认弹窗,预约类型/我的预约页精简,胃肠镜报告页影像报告整合并新增下载入口,首页站点地图同步)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-03 14:25:13 +08:00
fengpuandClaude Opus 4.7 a443c61907 feat: 健康体检员工端我的预约整合为单页并新增指标纵向对比与消息中心(删除按状态拆分的预约详情/列表页,合并为"我的预约"单页;新增指标纵向对比总览与详情页;新增消息列表与详情页;预约类型/医院/项目页与体检报告页、检前首页同步调整;首页站点地图更新)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-02 20:10:48 +08:00
liuyunqin 0d1c770ee9 feat: 健康体检原型完善(新增大病人员信息/指标异常通知/已选项目员工信息页面,员工病种信息勾选病种数弹窗查看并精简操作栏,单位预约人数统计弹窗按年龄段性别重做,员工预约跟踪/已参检员工信息操作栏精简并新增勾选列,病种信息管理菜单更名,医院基础信息与必检项目配置页多处调整,首页站点地图与侧边栏同步) 2026-09-02 17:42:32 +08:00
fengpu 3ae6cd855a feat : 记忆文件同步 2026-09-02 11:14:02 +08:00
liuyunqin 12f54cdbd9 feat: 健康体检原型完善(单位体检计划新增新增/详情弹窗与员工选择弹窗,医院承检范围编辑抽屉改为弹窗并新增详情弹窗、预约量改为按日配置含跳过节假日/周末,医院项目费用列表加图片列与更新已生成费用等操作及新增关联项目弹窗,医院实时预约人数加日期范围筛选默认当月) 2026-09-01 18:04:57 +08:00
liuyunqin db8c2d1d08 feat: 健康体检检中/检后侧边栏改为二级目录并将胃肠镜Tab拆分独立成页(新增员工预约跟踪/单位预约统计胃肠镜页与胃肠镜医院承检范围、单位体检计划管理页),体重管理蓝牙称成员弹窗新增移除操作,急救地图弹窗卡片名称补充说明,首页站点地图与文档同步 2026-09-01 16:40:55 +08:00
zhangyanandClaude Opus 4.6 deb366e19c feat: 餐线营养秤四Tab分页 + 日期默认当天 + 人脸识别数据更名视频数据
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-01 16:31:13 +08:00
zhangyanandClaude Opus 4.6 2996e835c7 feat: 净菜订单生成历史加建议熟重/规格列 + 食谱快速创建份数改规格
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-01 11:33:08 +08:00
zhangyanandClaude Opus 4.6 7333189799 feat: 餐线营养秤概览筛选栏下沉 + 重量变化折线图
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-31 08:37:33 +08:00
liuyunqin ab27a394f3 fix: 健康监护设备详情页水质/花粉Tab隐藏右上角综合指数与等级文字(PRD/设计总览同步、截图重截) 2026-08-28 17:44:26 +08:00
liuyunqin 61103fc4e7 feat: 健康体检检后新增病种信息管理/重点指标异常统计/胃肠镜体检报告与统计页面,侧边栏与首页站点地图同步调整 2026-08-28 17:43:12 +08:00
liuyunqin 1689707ab2 refactor: 删除膳食偏好设置页(diet-questionnaire.html)及关联截图,PRD/设计总览同步移除引用并调整章节序号,今日饮食建议改为直接跳转 2026-08-28 08:46:11 +08:00
liuyunqin 1d31d1c53c docs: 体重管理PRD补充首页波浪水球水位与目标虚线计算规则 2026-08-27 17:50:07 +08:00
liuyunqin f177c05119 feat: 健康体检原型更新(员工体检报告更名为常规体检报告并新增报告查看/补录/影像提取等操作,未匹配项目页完善新增与批量交互,删除医生检查人数统计与体检结果名册页面,侧边栏导航同步调整) 2026-08-27 17:50:07 +08:00
fengpuandClaude Opus 4.7 b96b936604 feat: 营养小程序个人中心新增意见反馈页(类型chips/截图上传/三态反馈记录列表)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-27 14:03:37 +08:00
liuyunqin 0af98f6144 docs: 体重管理PRD补充目标审核发送短信规则(10点/15点两次提醒待审核人员) 2026-08-27 10:59:20 +08:00
liuyunqin 539a2ba232 feat: 应急救援求助弹窗左侧抽屉改版(求助信息紧凑化、健康画像关键项置顶、体检/穿戴告警折叠简化) 2026-08-27 10:29:34 +08:00
fengpu f90f260769 feat:更新体重管理PRD说明 2026-08-27 10:25:50 +08:00
fengpu 76dbca9fb7 chore: memory-sync 记忆体系同步(Base commit 173e849)
- decisions.md: 新增健康生产菜单「净菜组配」与设备端「净配秤」术语边界决策 + 补 frontmatter
- project_progress.md: 新增 2026-08-21~27 九条里程碑
- lint_report.md: 更新健康检查结果(AUTO-FIX 1 项,NEED-HUMAN 7 项)
- MEMORY.md / CLAUDE.md: 刷新 base commit 与元数据
2026-08-27 10:10:03 +08:00
fengpu 173e849b5e fix: 健康生产菜单「净配」改回「净菜组配」+ 营养秤多维状态矩阵文档移至 doc 目录
- 健康生产模块侧边栏菜单净配改净菜组配,共10个页面同步修正(prod-proc-combo含title与表格标题、prod-proc-cook、prod-proc-serve、prod-proc-surplus、prod-proc-stats、prod-clean-order、prod-proc-cabinet、prod-recipe-plan、prod-recipe-plan-detail、prod-recipe-plan-create)
- 营养秤多维状态矩阵.md 移至 web-admin/nutrition/doc/ 目录
2026-08-27 09:59:34 +08:00
zhangyan 11415da744 feat: 餐线营养秤字段与时间筛选调整 + 净配菜单名改净菜组配
- din-terminal.html: 重量变化数据提交重量改较上次变化(正数绿/负数红/0灰)、当前重量改当前余量并取消两列筛选;四维度时间列筛选改年月日/时间(到分钟)日期时间组件
- prod-qc-daily.html: 侧边栏菜单净配改净菜组配
2026-08-27 09:45:09 +08:00
zhangyan 81cddba76e refactor: 餐线营养秤概览字段调整
- din-terminal.html: 重量变化数据Tab字段改为日期/时间(取开始时间)/用户/提交重量/当前重量;人脸识别数据Tab删除重量变化/总重量两列
2026-08-27 09:23:44 +08:00
zhangyan 40e0b311c4 feat: 订餐终端设备概览新增餐线营养秤Tab + 净菜订单新增生成历史/生成方式列/就餐人数
- din-terminal.html: 设备概览新增「餐线营养秤」Tab(四维度子页切换),表头列筛选 + 人脸识别视频回放弹窗 + 运行日志报错详情
- prod-clean-order.html: 新增「净菜订单生成历史」按钮与弹窗、主列表「生成方式」列、详情抽屉自动订单「就餐人数」列
- 新增文档: 异常场景素材-秤体视角 / 加工厂硬件流程说明 / 角色场景流程图 / 营养秤多维状态矩阵
2026-08-27 09:00:12 +08:00
liuyunqin 059d1c82ab feat: 体重管理原型更新(管理端方案编辑审核人分组、员工端目标/打卡调整及PRD、截图同步)
- 管理端:方案编辑审核人按医疗点/管理员人员分组且均可添加多个;体重排行、设备添加页及监测页调整
- 员工端:历史计划更名为历史目标;目标详情新增编辑态;吃/动打卡新增成功与已记录态;体重记录成功提示
- PRD 与截图同步更新
2026-08-26 19:25:51 +08:00
liuyunqin e76ca89ae6 feat: 体重管理原型更新(员工端运动建议v2、管理端员工数据记录页调整及PRD、截图同步) 2026-08-25 14:32:22 +08:00
liuyunqin 9f8a80eae9 feat: 体重管理原型更新(管理端行动总览页面调整及PRD、截图同步) 2026-08-25 14:03:38 +08:00
liuyunqin 52aca0a74b feat: 体重秤/蓝牙称/体脂仪监测数据详情弹窗字段对齐员工端报告口径,弹窗标题由序号改为姓名 2026-08-25 13:42:43 +08:00
liuyunqin da410f71b0 feat: 体重管理原型更新(三测口径调整、设备厂商统一为蚁熊、历史方案详情改版等) 2026-08-25 13:32:12 +08:00
liuyunqin e663982b77 feat: 员工端记录详情页周日历新增来源标签,运动/体重页新增数据来源卡片并移除底部记录按钮;体重页身高秤更名为体重秤 2026-08-25 11:58:43 +08:00
fengpu c4ca35ec60 fix:合并 2026-08-25 11:43:02 +08:00
fengpu e698c874e8 1 2026-08-25 11:42:25 +08:00
726 changed files with 91070 additions and 10540 deletions
+14 -10
View File
@@ -1,11 +1,15 @@
_Last synced: 2026-08-21 | Base commit: `b62d183`_
# Memory Index
> **单一基准**: 此目录是唯一权威来源,其他位置的记忆以此为准同步。
> _Last synced: 2026-09-10 | Base commit: `b5a8b83`_
# 记忆索引
| 文件 | 描述 | 类型 | 最后更新 |
|------|------|------|----------|
| project_overview.md | 项目技术栈、架构与模块结构 | project | 2026-05-14 |
| project_progress.md | 各模块原型开发进度 | project | 2026-08-21 |
| decisions.md | 关键设计决策与架构约定 | project | 2026-08-14 |
| feedback.md | 协作规范与用户纠正记录 | feedback | 2026-06-15 |
| user_profile.md | 用户角色与偏好 | user | 2026-04-21 |
| lint_report.md | 记忆健康检查报告 | lint | 2026-08-10 |
| 文件 | 描述 | 类型 | 更新日期 | Commit |
|------|------|------|----------|--------|
| user_profile.md | 用户角色与偏好 | user | 2026-09-10 | b5a8b83 |
| project_overview.md | 项目技术栈、架构与模块结构 | project | 2026-09-10 | b5a8b83 |
| project_database_postgresql.md | 项目数据库为 PostgreSQL(非 MySQL),docx 标书选型与实际不符 | project | 2026-09-09 | — |
| project_progress.md | 各模块原型开发进度与里程碑 | project | 2026-09-10 | b5a8b83 |
| decisions.md | 关键设计决策与架构约定 | project | 2026-09-07 | 891678d |
| feedback.md | 协作规范与用户纠正记录 | feedback | 2026-09-10 | b5a8b83 |
| feedback_medical_pad_migration.md | 医疗点PAD迁移纠正:无前缀英文名、无模块导航页、简单弹窗页内实现、1280×800外壳 | feedback | 2026-09-04 | 15ca37b |
| feedback_shell_sidebar_scope.md | 壳层规范:顶栏=一级分组、侧栏仅当前分组二级菜单、页头不放面包屑与菜单说明 | feedback | 2026-09-07 | — |
| lint_report.md | 记忆健康检查报告 | lint | 2026-09-07 | — |
+45
View File
@@ -1,3 +1,11 @@
---
name: 关键决策
description: 关键设计决策与架构约定
type: project
last_updated: 2026-09-07
commit: 891678d
---
# 关键决策
## 卫生供应仓储页面区域选择层级约定
@@ -80,6 +88,7 @@ body {
**See Also**
- [[feedback.md#PAD 终端页面内禁止可见滚动条]]
- [[feedback_medical_pad_migration.md]](医疗点 PAD 沿用本外壳模式,尺寸变体 1280×800,壳内浅色背景)
- **日期**2026-06-12
- **首次落地**:餐线营养秤 9 页 + canteen-device/index.htmlcommit 2373704
@@ -209,3 +218,39 @@ body {
- **日期**2026-08-10
- **首次落地**commit ff48f38
## 健康生产菜单「净菜组配」与设备端「净配秤」术语边界
**结论**:管理端营养模块「健康生产」一级菜单下的净菜组合子菜单固定命名为「净菜组配」(`prod-proc-combo.html`);餐厅终端设备 `ratio-scale/` 命名为「净配秤」、`clean-veg-ratio-scale/` 命名为「厨配秤」。两者分属不同场景,术语不可混用。
**Why**2026-08-25 提交 2922834 做设备端术语统一(组配秤→净配秤、加工秤/配比秤→厨配秤)时,把管理端健康生产菜单「净菜组配」一并改成了「净配」,属于误改。2026-08-27 提交 173e849 修正回「净菜组配」。设备端术语统一不应波及管理端菜单命名。
**How to apply**
- 管理端健康生产菜单固定用「净菜组配」,不要简写为「净配」
- 设备端 `ratio-scale/` = 净配秤,`clean-veg-ratio-scale/` = 厨配秤
- 做术语统一类改动时先确认边界,不跨端、跨模块连带修改
**See Also**
- [[project_progress.md]] — 2026-08-27 里程碑条目记录修正范围
**日期**2026-08-27
## 净菜类型数据模型与净菜管理页展示模式
**结论**:净菜类型沿用三页共享的平铺数据模型(`vegTypes[]``groupId`):组级字段(净菜俗名/净菜标准名/切配形状/出净率)跨规格共享,行级字段(规格别名/可食部(%)/切配尺寸/刀工)逐规格独立维护。净菜管理列表采用「组级列 rowspan 合并 + 行级四列按规格逐行拆分」渲染模式;多规格同列表页禁止塞入单一单元格换行展示。
**Why**:需求方要求把净菜类型从食物成分中拆出独立管理。可食部(%) 最初实现在净菜类型组级,被用户纠正为逐规格维护——同一净菜类型不同规格出成率客观不同(如马铃薯薄片/厚片 94%、薯片 92%),字段语义属于规格行。列表若把多规格挤在一个单元格内换行,行间无分隔不可读;拆成每规格一行、组级列 rowspan 合并后才有清晰的分界。
**How to apply**
- 净菜相关三页(`din-ingredient.html` / `din-ingredient-add.html` / `din-clean-veg-mgmt.html`)共享同一数据结构口径;给该模型加字段时先确认粒度落在组级还是规格行级
- 混合粒度列表渲染:主 `<tr>` 组级单元格 `rowspan=规格数`,其余行只放行级四列;组内规格间用虚线(`.td-spec--inner { border-bottom:1px dashed }`),组边界保留实线;行级四列区左侧加纵向分隔线(`.td-spec-start { border-left:1px solid }`)与组级列区分
- 空态 colspan 与表头列数需随拆分列数同步调整
- 异步导出按钮组遵循全仓惯例:`btn-primary` + 下载图标「导出列表数据」+ `btn-default` + 文档图标「查看导出任务」(参考 `farm-purchase.html``post-illness-personnel.html`
**See Also**
- [[feedback.md#rowspan 表格新增字段先确认组级与行级粒度]]
- [[feedback.md#功能按钮放列表操作区而非标题行]]
- [[project_progress.md]] — 2026-09-07 里程碑条目记录落地范围
**日期**2026-09-07
**首次落地**commit 891678d
+43
View File
@@ -1,3 +1,11 @@
---
name: 协作规范
description: 用户纠正与确认的协作规范(含 Why + How to apply
type: feedback
last_updated: 2026-09-10
commit: b5a8b83
---
# 协作规范
## Agent 行为约束
@@ -50,3 +58,38 @@ PAD 终端(餐厅终端设备各子设备)页面内不得出现可见滚动
**See Also**
- [[decisions.md#餐厅终端设备 PAD 居中外壳设计模式]]
## 功能按钮放列表操作区而非标题行
页面级功能入口按钮应放在列表面板操作区(`.panel-left__actions`)内、相邻同类按钮之后,不放页面标题行上方。
**Why:** 2026-09-07 净菜管理入口初版放在页面标题行,用户纠正"净菜管理按钮放在食材分类管理按钮后面,不要放到那上面"——操作区是页面动作按钮的既定位置,标题行只承载页面标识。
**How to apply:** 在既有列表页新增功能跳转入口时,先查看该页操作区已有按钮,按业务相近顺序插入;不要往页头/标题区加按钮。
**See Also:** [[decisions.md#净菜类型数据模型与净菜管理页展示模式]]
## rowspan 表格新增字段先确认组级与行级粒度
在「组级合并单元格 + 行级明细」的数据模型或表格上新增字段时,必须先确认字段挂在组级还是行级,不能默认沿用组级。
**Why:** 2026-09-07 可食部(%) 最初实现在净菜类型组级,用户纠正"再新增规格的时候要随着不同的规格维护这个字段的,你现在做的这个逻辑有问题"——字段粒度决定数据结构、表单与列表渲染三处实现,猜错即返工。
**How to apply:** 凡父实体下挂多个子行(规格/明细/条目)的模型加字段时,先判断"这个值是否随子行变化";随子行变化则行级维护(新增子行时同步出现该字段输入),否则组级。列表渲染同步调整:行级字段按子行逐行拆列展示。
**See Also:** [[decisions.md#净菜类型数据模型与净菜管理页展示模式]]
## 操作手册截图规范(2026-09-10
操作手册(doc-operation-manual)及配套截图 skill 的截图必须遵守四条规范:
1. **只截主界面 + 重要表单/详情页**:弹窗、筛选下拉、选项列表等轻量交互不单独截图,在功能描述/注意事项中用文字或表格说明。
2. **固定压缩比例(清晰度优先)**:输出 JPEG(质量 85%),APP 端缩放为 390px 宽;**用压缩比例限制,不设文件大小上限**(不用 KB 大小限制,保证清晰度的同时兼顾体积),参考 `项目背景.md` 7.4 节 PRD 截图规范。
3. **分终端目录存放**`交付文档/{子系统名}/操作手册/screenshots/{终端目录}/`web-admin / employee-app / medical-pad 等),避免多终端截图混在同一目录。
4. **Web 端截图前确保浏览器全屏/正确视口**:窗口未全屏会导致截图不完整、比例失调,截图前先 `resize_page` 设 1440×900 视口。
**Why:** 2026-09-10 生成健康监测 Web 管理端操作手册时,实际截图用了 PNG(体积大)、未分终端目录、截了过多页面,且浏览器未全屏导致截图不完整、比例失调。根因是 `web-capture/SKILL.md` 未同步规范(仍写 `.png`),doc-operation-manual 已更新但 web-capture 滞后。
**How to apply:** 涉及操作手册截图时,先确认 web-capture / android-capture / doc-operation-manual 三个 skill 及 operation-manual-template.md 的截图规范是否一致;不一致时以「JPEG 85%(压缩比例,清晰度优先)+ 分终端目录 + 只截主界面/详情页 + 全屏视口」为准。
**See Also:** [[project_progress.md]]
@@ -0,0 +1,23 @@
---
name: feedback-medical-pad-migration
description: 医疗点PAD端原型迁移的三条用户纠正(英文名无前缀、无模块导航页、简单弹窗页内实现、1280×800外壳)
metadata:
node_type: memory
type: feedback
originSessionId: 2b914133-b4ca-4fe5-8982-8bd381b51120
---
医疗点PAD端原型(other-module/medical-pad,迁移自 D:\Work\medical-points Vue 项目)迁移时用户的明确纠正:
1. **文件命名用功能英文名,不加数字前缀**(如 `login.html``task.html`),不要参考餐厅硬件原型的 `00-`/`01-` 前缀式命名。
2. **不建模块级 index.html 导航页**:登录后直达主页 `task.html`(任务中心即主页),业务入口走左导航常驻壳。
3. **同一页面内的简单弹窗与交互直接在该 HTML 内实现(隐藏 overlay + JS 显隐),不得拆成独立文件**;不必对齐原 Vue 项目的组件式划分。
**Why**: 用户认为弹窗与页面强耦合,拆文件反而不协调、破坏原型阅读流;原项目的组件拆分是工程结构不是页面结构。
**How to apply**: 编辑类小弹窗(改截止日期、选择任务、详情查看、选人、加药、签名)内联在父页面;仅「全屏抽屉表单/独立流程页」保留为独立文件(interpret-drawer、follow-drawer、daily-drawer、chronic-disease-drawer、health-education-drawer、five-class-drawer、bed-detail、employee-file-detail、chronic-disease-detail)。
4. **PAD 外壳尺寸 1280×80016:10**,替代项目背景.md 原规定的 1024×768(用户认为 4:3 比例"矮胖"不协调)。深色画布 + 居中 device-framegrid),body min-width 1320 / min-height 840。
另见 [[decisions.md#餐厅终端设备 PAD 居中外壳设计模式]](居中外壳模式沿用,仅尺寸不同;医疗点外壳内背景为浅色而非餐厅的深色)。
**See Also:**
- [[project_progress.md]] — 2026-09-04 里程碑记录医疗点PAD端首版 20 页落地范围(commit 15ca37b
- [[feedback_shell_sidebar_scope.md]] — 同批原型会话产出的 Web 管理端壳层导航规范
@@ -0,0 +1,17 @@
---
name: feedback-shell-sidebar-scope
description: Web管理端壳层规范:顶栏=一级分组、侧栏只显示当前分组二级菜单、页面顶部不放面包屑与菜单说明
metadata:
node_type: memory
type: feedback
originSessionId: ead44db5-2c22-433e-a8c8-08635801a2bd
---
Web 管理端模块壳层的导航与页头规范:
1. **两级导航**:顶栏 top-bar__nav 放一级分组(带 active 高亮),左侧 sidebar 只放当前一级分组的标题(`sidebar-menu__title`)+ 该分组下的二级菜单项,绝不把全部分组菜单树罗列在侧边栏。二级页(如慢病干预)不单列菜单项,侧边栏高亮归到其父菜单,页内保留「← 返回父页」链接。
2. **页头**:页面 main 区**不放**「模块 / 分组 / 页面名」面包屑目录与页面功能说明(desc)文字,内容直接从筛选栏开始(2026-09-07 用户第二次纠正,一线医疗 45 页全部移除)。
**Why:** 2026-09-07 一线医疗模块 45 页先犯了侧栏堆全量菜单的错误,又犯了每页顶部放面包屑+菜单说明的错误,两次都被用户打回批量返工。健康体检等既有模块的壳层是正确参照,新建模块应直接对照其 top-bar+sidebar 段,不自行加页头信息层。
**How to apply:** 新建 web-admin 模块壳层时以已完成模块为模板;批量建页后自检:①每页 sidebar 分组标题数=1、二级项数与分组配置一致、侧栏/顶栏各仅一处 active;②main 首个元素是 filter-bar(或卡片),body 内不出现「模块名 / 分组名」目录文本。参见 [[feedback_medical_pad_migration.md]]。
+29 -40
View File
@@ -2,12 +2,12 @@
name: 记忆健康检查报告
description: memory-lint 最新一次执行的检查结果与待处理项
type: lint
last_updated: 2026-08-21
last_updated: 2026-09-10
---
# 记忆健康检查报告
> _执行时间: 2026-08-21 | Base commit: `b62d183` | Last synced: 2026-08-21_
> _执行时间: 2026-09-10 | Base commit: `b5a8b83` | Last synced: 2026-09-10_
## 健康概览
@@ -15,53 +15,42 @@ last_updated: 2026-08-21
|--------|------|----------|------------|
| 孤儿检测 | ✅ | — | — |
| 幽灵检测 | ✅ | — | — |
| 引用存在性检测(3A) | ✅ | | — |
| 双链对称性检测(3B | | — | |
| 引用存在性检测(3A) | ✅ | 1 已修复 | — |
| 双链对称性检测(3B | | — | 1 待处理(遗留) |
| 内容矛盾检测 | ✅ | — | — |
| 过期检测 | | — | 3 待处理 |
| 可推断内容污染 | ⚠ | — | 1 待处理 |
| frontmatter 完整性 | | — | 4 待处理 |
| 过期检测 | | 2 已刷新 | — |
| 可推断内容污染 | ⚠ | — | 1 待处理(遗留) |
| frontmatter 完整性 | | 2 已补齐 | — |
**AUTO-FIX 已执行 0 项 | NEED-HUMAN 待处理 8 项(均为前期遗留)**
**AUTO-FIX 已执行 5 项 | NEED-HUMAN 待处理 2 项(均为前期遗留)**
> 本次为 2026-08-21 提交(b62d183 溯源链路详情卡片优化)后的 sync 检查。memory-update 更新了 project_progress.md(新增 2026-08-21 溯源详情卡片优化 1 条里程碑)并刷新 MEMORY.md 索引。索引与磁盘一致(7 文件),全部交叉引用有效且闭环,无新问题。前期遗留 NEED-HUMAN 维持原判定
> 本次为 2026-09-10 sync891678d → b5a8b83,覆盖一线医疗管理端首版+二线迭代、健康体检管理端大表/CTA、员工端一线医疗、医疗点PAD多轮优化、文档技能包模板规范共 14 个提交)后的检查。memory-update 更新了 project_progress.md、project_overview.md、feedback.md、MEMORY.md。上次报告的 6 项 NEED-HUMAN 中:frontmatter 缺失(2 个)、project_overview.md 过期 ERROR、引用存在性 slug 断链均已解决;剩余 2 项为延续性 WARN
## AUTO-FIX 已执行清单
- [x] 3A 断链修复:`feedback_shell_sidebar_scope.md``[[feedback-medical-pad-migration]]`(中划线 slug)统一为文件名式 `[[feedback_medical_pad_migration.md]]`
- [x] frontmatter 补齐 + 过期刷新:`project_overview.md`(补 frontmatterlast_updated 刷新为 2026-09-10,消除 116 天过期 ERROR
- [x] frontmatter 补齐 + 过期刷新:`user_profile.md`(补 frontmatterlast_updated 刷新为 2026-09-10,消除 139 天过期 WARN
## NEED-HUMAN 待处理清单
### [ERROR] 过期 — project_overview.md(遗留)
- **文件**`project_overview.md`last_updated: 2026-05-14,距今 99 天,超 90 天 ERROR 阈值)
- **说明**:项目技术栈(纯 HTML 自包含 + 墨刀兼容)与三端架构稳定,期间变更均为模块内页面迭代,内容并未失真
- **建议**若确认架构无变化,下次 sync 时仅刷新 last_updated 时间戳即可消除 ERROR(需同时补 frontmatter
### [WARN] 过期 — user_profile.md(遗留)
- **文件**`user_profile.md`last_updated: 2026-04-21,距今 122 天)
- **维持降级为 WARN**:用户角色画像(产品经理/原型设计师、简体中文、分步确认)属结构性稳定内容,不随代码迭代过期
- **建议**:用户确认角色/偏好是否仍准确,有变化时执行 `/memory-update` 更新
### [WARN] 过期 — feedback.md(遗留)
- **文件**`feedback.md`last_updated: 2026-06-15,距今 67 天,超 30 天 WARN 阈值)
- **建议**:确认会话期间是否产生新的协作规范需沉淀(本次 sync 未触发新 feedback 写入)
### [WARN] 双链对称性 — decisions.md#餐厅终端设备 PAD 居中外壳设计模式(遗留)
- **问题**`project_progress.md` 里程碑(2026-08-14 条目)引用 `[[decisions.md#餐厅终端设备 PAD 居中外壳设计模式]]`,该章节 See Also 仍无 `[[project_progress.md]]` 回引
- **不自动修改的原因**:decisions.md 文件内存在多处 `[[project_progress.md]]` 引用(仅不在该章节),按 lint 边界规则「若有但不在目标章节,仅记录 WARN 不自动修改」
- **建议**下次人工编辑该章节时顺手补一行回引
### [WARN] 可推断内容污染 — project_progress.md(遗留)
- **文件**`project_progress.md`「近期里程碑」区段(历史条目)
- **问题**历史里程碑条目仍含大量具体文件路径与 commit hash,按 memory-update 规则「Git 历史(可用 git log 查)」不应写入记忆文件
- **本次执行**:新增的六条里程碑延续既有格式保留文件名(属变更范围说明),历史污染条目未清理
- **建议**人工审查历史里程碑,保留结论性描述、删除具体文件路径与 commit hash
### [WARN] frontmatter 缺失 — 4 个文件(遗留)
- **文件**`decisions.md``feedback.md``project_overview.md``user_profile.md`(均无 frontmatter
- **说明**:按 memory-update 规则应有五字段 frontmatter;目前仅 `project_progress.md``lint_report.md`
- **建议**MEMORY.md 索引已含类型与日期,可视为冗余备份;如需统一可在下次 sync 时一并补全(可与 project_overview 刷新时间戳合并处理)
- **文件**`project_progress.md`「近期里程碑」区段
- **问题**:里程碑条目具体文件路径与 commit hash,按「Git 历史可查不应写入记忆」规则属边缘污染
- **本次执行**:新增条目延续仓库既有的详细里程碑格式(属变更范围说明,且为跨会话恢复工作的关键索引),历史条目未清理
- **建议**若嫌冗长可人工压缩 2026-08 月以前的早期条目为一句话结论,近期条目保留细节
---
## 交叉引用验证清单
## 交叉引用验证清单(本次新增/受影响部分)
- `decisions.md#餐厅终端设备 PAD 居中外壳设计模式``[[feedback.md#PAD 终端页面内禁止可见滚动条]]` ✅ 目标存在,双向闭环
- `feedback.md#PAD 终端页面内禁止可见滚动条``[[decisions.md#餐厅终端设备 PAD 居中外壳设计模式]]` ✅ 目标存在,双向闭环
- `decisions.md#规格体系统一化改造(跨设备端 + Web 端 + 辅助页)``[[feedback.md]]` ✅ 目标存在(占位引用,非对称但属预期)
- `decisions.md#收银设备用户类型徽标三态体系``[[project_progress.md]]` ✅ 目标存在,双向闭环
- `decisions.md#补齐明细页业务路径修正:少记来源 = 未识别取餐记录``[[project_progress.md]]` ✅ 目标存在,双向闭环
- `decisions.md#待机页副屏人脸识别监控区(收银员侧实时反馈)``[[project_progress.md]]` ✅ 目标存在,双向闭环
- `decisions.md#餐次管理模块业务路径与交互约定``[[project_progress.md]]` ✅ 目标存在,双向闭环
- `project_progress.md#近期里程碑`2026-08-14 条目)→ `[[decisions.md#餐厅终端设备 PAD 居中外壳设计模式]]` ✅ 目标存在
- `project_progress.md`2026-09-10 文档规范里程碑)→ `[[feedback.md#操作手册截图规范(2026-09-10]]` ✅ 目标存在,双向闭环
- `project_progress.md`2026-09-09 厨配秤恢复里程碑)→ `[[decisions.md#健康生产菜单「净菜组配」与设备端「净配秤」术语边界]]` ✅ 目标存在,双向闭环
- `feedback_shell_sidebar_scope.md``[[feedback_medical_pad_migration.md]]` ✅ 本次 AUTO-FIX 修复断链
- `project_database_postgresql.md` 已正式入索引(本次 memory-update 从远程补充本地独有文件)
- 其余历史引用与上次报告一致,未变动 ✅
@@ -0,0 +1,14 @@
---
name: project-database-postgresql
description: 项目数据库统一使用 PostgreSQL(非 MySQL),docx 标书的 MySQL 选型与实际不符
metadata:
node_type: memory
type: project
originSessionId: ca84d82d-2718-4e31-8ade-f80032318c0e
---
项目所有子系统的数据库均为 **PostgreSQL**(非 MySQL)。
**Why:** 用户明确纠正「这个项目用到的数据库都是 pg,不是 mysql」。实际 SQL(如 `doc/platform-weight.sql`)为 PostgreSQL 格式:schema `platform-weight`,类型 `int8`/`int4`/`varchar(n)`/`numeric(p,s)`/`timestamp(0)`,注释走 `COMMENT ON TABLE/COLUMN ... IS`。而 `doc/体重管理项目系统设计说明.docx` 标书里的技术选型表写的是 MySQL,与研发实际不符,不能以标书为准。
**How to apply:** 生成任何子系统的《数据库设计说明书》时一律按 PostgreSQL 整理(UTF-8 字符集、雪花 `id` 主键、无外键由程序保证关联),优先用 `_templates/docs/parse-sql.js` 脚本解析 SQL 自动生成 Markdown。
+10 -2
View File
@@ -1,3 +1,11 @@
---
name: 项目概览
description: 项目技术栈、架构与模块结构
type: project
last_updated: 2026-09-10
commit: b5a8b83
---
# 项目概览
## 项目名称
@@ -16,8 +24,8 @@ CQ能源公司内部员工健康管理服务平台,整合健康咨询、应急
## 三端架构
| 端 | 目录 | 说明 |
|----|------|------|
| Web 管理端 | `web-admin/` | 平台管理员使用,16 个模块(已完成:health-consult、emergency-dispatch、health-checkup;设计中:nutrition、health-monitor |
| 员工端 APP | `employee-app/` | 能源员工使用,17 个模块(已完成:health-consult、emergency-medical、health-checkup;设计中:health-monitor |
| Web 管理端 | `web-admin/` | 平台管理员使用,17 个模块(已完成:health-consult、emergency-dispatch、health-checkup、frontline-medical;设计中:nutrition、health-monitor |
| 员工端 APP | `employee-app/` | 能源员工使用,18 个模块(已完成:health-consult、emergency-medical、health-checkup、frontline-medical;设计中:health-monitor |
| 健康应急 APP | `emergency-app/` | 专家/应急人员使用,6 个角色(expert/assistant/operator/professional/onsite + 共享模块 ai-assistant/message |
| 其他终端 | `other-module/` | 微信小程序、应急大屏(西安/标准版/九厂)、医疗点PAD端 |
+46 -6
View File
@@ -2,8 +2,8 @@
name: 项目进度
description: 各模块原型开发进度与里程碑
type: project
last_updated: 2026-08-21
commit: b62d183
last_updated: 2026-09-10
commit: b5a8b83
---
# 项目进度
@@ -23,6 +23,10 @@ commit: b62d183
### 员工端 APPemployee-app
- `health-consult/` — 健康咨询(32 页)✅
- `emergency-medical/` — 应急就医(25+ 页)✅
- `frontline-medical/` — 一线医疗(22 页)✅
- 医疗点/健康小屋:`hut-list`/`hut-detail`/`hut-booking-*`(预约全/已约/详情/信息/完成)/`hut-measure-list`/`hut-measure-report`/`hut-qrcode`/`hut-device-detail`
- 随访:`follow-list`/`follow-detail`/`follow-apply-records`;就诊:`visit-list`/`visit-detail`
- 用药计划:`medication-plan`;附近医疗:`nearby-list`/`nearby-detail``medical-point`/`medical-point-staff`/`index`
- `health-checkup/` — 健康体检(20+ 页)✅
- `weight-management/` — 体重管理(29 页)✅
- `weight-main.html` — 体重管理主页(三态切换/英雄区/三态卡片/吃动平衡/体重记录/排行榜/推荐视频)
@@ -68,6 +72,7 @@ commit: b62d183
- 数据分析子菜单组:`din-biz-analysis.html`(经营分析)、`din-meal-analysis.html`(餐品分析)
- 用户体系:`din-user-mgmt.html`(用户管理)、`din-user-account.html`(账户管理)、`din-user-recharge.html`(批量充值,2026-08-06 抽离)
- 溯源详情卡片优化(2026-08-21):`trace-timeline-ingredient.html`/`trace-timeline-dish.html` 去副标题与重复图标、详情属性逐行独占、时间并入详情、去重复溯源码
- 净菜类型拆分独立管理(2026-09-07):新增 `din-clean-veg-mgmt.html` 净菜管理二级页(统计卡片/四条件筛选/异步导出/规格逐行拆分列表/维护弹窗);`din-ingredient.html`/`din-ingredient-add.html` 规格行级「可食部(%)」字段(见 [[decisions.md#净菜类型数据模型与净菜管理页展示模式]]
- `weight-management/` — 体重管理(40 页)🔨 设计中
- 档案:`archive-personal-weight-data.html``archive-personal-weight-detail.html``archive-unit-report.html``archive-algorithm-model.html`
- 计划管理:`plan-list.html``plan-edit.html``registration-tracking.html``target-weight-review.html`
@@ -82,9 +87,10 @@ commit: b62d183
- `environment/` — 环境监测(25 页:室内/室外/花粉/水质各含设备类型+设备列表+数据监测+历史数据+APP记录+异常告警,花粉知识)
- `health-checkup/` — 健康排查(7 页:问卷分类/模板/题目/评分/格言/部门记录/用户记录)
- 根级页面(5 页:档案管理个人/单位/算法、异常事件脏数据/结果)
- `frontline-medical/` — 一线医疗管理端 🔨 设计中(47 页,覆盖档案/基础配置/日常业务/床头呼叫/数据统计/健康小屋/异常事件七大分区;侧栏二级分组可展开收起;床头呼叫楼栋-宿舍-床位-设备四级管理;救助物品统计图表/报表双视图)
### 其他终端(other-module
- `mini-program/nutrition/` — 微信小程序营养管理用户端(28 页)✅
- `mini-program/nutrition/` — 微信小程序营养管理用户端(41 页)✅
- `meal-delivery-h5/` — 营养管理餐品配送 H5(配送员端 11 页:登录/工作台/派单来电/抢单池/订单详情[含自助取货/到店取货双弹窗]/配送中/完成签收/历史/个人中心)✅ + CHANGELOG;称谓统一"配送员",派单不设倒计时
- `canteen-device/` — 餐厅终端设备(多设备实页持续补齐)🔨 设计中
- `canteen-nutrition-scale/` — 餐线营养秤(11 页:含登录页、欢迎页,双屏取餐合并为单页 03-meal-pickup-dual 同页左右双屏布局)+ PRD + FLOW + CHANGELOG
@@ -99,16 +105,50 @@ commit: b62d183
- 其余设备页仍有部分占位,待继续补齐
- `emergency-screen/` — 应急大屏(西安/标准版/九厂)🔨 设计中
- `xian-center/index.html` — 西安应急中心大屏(应急指挥大屏,含 KPI 指标/穿戴告警/心脑血管 AI 预警/地理地图/资源调度/急救路线;新增穿戴告警/心脑血管预警/应急求助三个业务记录弹窗含筛选与分页,地图资源标记弹窗,告警弹窗红色闪烁边框)
- `medical-pad/` — 医疗点PAD端 ⏳ 待开发
- `medical-pad/` — 医疗点PAD端 ✅ 19 页(1280×800 横屏:登录/任务中心/日常诊疗/随访/慢病干预/健康宣教/员工档案/健康监测/床头呼叫/报告解读,抽屉交互页内实现;随访抽屉携带员工信息跳转 goFollow 带参回显、巡诊任务重构、删除五类人群登记页)+ CHANGELOG
## 待开发模块
Web 管理端和员工端 APP 的其余模块(系统管理、运动管理、健康评估、知识普及、健康数据、心脑血管/糖尿病/癌症预防等)均为待开发状态。
## 总计
- 已完成/设计中:580+ 个 HTML 原型页面
- 覆盖模块:18 个(含三端+其他终端)
- 已完成/设计中:900+ 个 HTML 原型页面
- 覆盖模块:20 个(含三端+其他终端)
## 近期里程碑
- 2026-09-10:文档技能包与模板规范完善(commit b5a8b83)——①截图规范统一:JPEG 质量 85% + 分终端目录(screenshots/{终端目录}/)+ 只截主界面/重要表单详情页(弹窗/筛选下拉不单独截图)+ Web 端全屏视口 1440×900android-capture/web-capture/doc-operation-manual 三 skill 及模板对齐);②内容通用化:全文去除客户名称与项目名称(甲方/委托方/油田等一律不写),产出文档可供任意客户阅读;③交付格式规范:.md 为源 + 竖版 A4 PDF 最终交付,明确分页三原则(标题不孤行/图文不割裂/表行不切割);④PRD 合并优化:以「系统功能清单」HTML 表格(rowspan 三列:功能模块/功能子模块/功能点)替代「页面导航结构」树与「页面清单」章节,清理 .html 文件名与来源说明区;⑤业务架构图模板重构:应用与服务层改横排竖版板块(.domain 分组,.d-item 管理端 / .d-item--app 员工端 / .domain-model 智能模型)+ 层间横向分隔线替代竖线箭头;⑥.gitignore 忽略 /交付文档/;记忆新增操作手册截图规范(见 [[feedback.md#操作手册截图规范2026-09-10]])。
- 2026-09-10:员工端体检指标对比详情移除 BMI/总胆固醇/甘油三酯的恶化趋势标签(commit 256b741)。
- 2026-09-10:员工端新增一线医疗模块 + 医疗点PAD用药信息调整(commit 97652d7)——员工端新建 `frontline-medical/` 22 页(医疗点/健康小屋预约与测量、随访、就诊、用药计划、附近医疗);医疗点PAD用药信息调整。
- 2026-09-09:医疗点PAD端细节优化 + 管理端体检统计二级/三级单位切换(commit 8a3a116)。
- 2026-09-09:一线医疗后台原型细节对齐和菜单重组(commit 91efe28)。
- 2026-09-09:厨配秤恢复成净菜加工秤(commit bc77229)——修正同事误改的术语(见 [[decisions.md#健康生产菜单「净菜组配」与设备端「净配秤」术语边界]])。
- 2026-09-08:健康体检三端与医疗点PAD、体重管理多模块更新(commit 26419bc)——①员工端冠脉CTA体检接入(首页 Banner 第四张 CTA 卡片、reports 页 12 份 CTA 报告、cta-date/cta-hospital/report-cta 四页);②医疗点PAD:随访抽屉携带员工信息跳转 goFollow 带参回显、下次随访/随访效果/签字方式/新增用药/巡诊任务重构、单位名称规范化、登录去校验、删除五类人群登记页;③管理端体检:档案单位报表表头重做单层 23 列、人数列点击弹窗人员名单+导出CSV、个人体检数据查看报告弹窗、异常员工下拉筛选、检前组合模板/例外配置/防癌筛查 TAB 融合、医院体检项目费用更名、检前侧边栏重建与 CTA 分组;④管理端体重管理结果异常页收敛为五类指标。
- 2026-09-08:营养小程序按餐次自定义营养素配比(commit 55819e0)。
- 2026-09-08:一线医疗管理端二线迭代(commit 47ab38e)——45 页改版 + 3 页新增:侧栏二级分组改造(基础配置/日常业务/床头呼叫收敛为可展开收起子组);床头呼叫新增站点管理页并更名楼栋/宿舍/床位信息、分配管理、告警记录、汇总统计;数据统计重排(救护车统计移入、表格/图表收拢为入口页内 view-switch、新增救助物品统计图表/报表双页);全站视觉尺寸升级;SITE_MAP 同步。
- 2026-09-07:一线医疗管理端首版原型(commit b4f3d5c)——新建 `web-admin/frontline-medical/` 45 页,覆盖档案/基础配置/日常业务/床头呼叫/数据统计/健康小屋/异常事件七大分区;SITE_MAP 同步一线医疗全量页面并将医疗点PAD入口调整为任务中心;新增模块 CHANGELOG。
- 2026-09-07:健康体检管理端新增员工体检大表与冠脉CTA分组(commit d76285a)——检后新增员工体检大表页(三层合并表头 95 列、冻结前三列);检前/检中/检后侧边栏各新增冠脉CTA体检二级分组共复刻 8 页(CTA 报告专业版式弹窗含钙化积分与 CAD-RADS);检后新增单位体检费用统计页;单位体检人数统计新增受检率/检出率/患病率三列;20+ 检后/13 检中/21 检前页面侧边栏统一重建。
- 2026-09-07:贴标秤缩略版标签设计(commit fbe7def)。
- 2026-09-07:净菜类型拆分独立管理(commit 891678d)——①`din-ingredient.html`/`din-ingredient-add.html` 净菜类型按**规格行级**维护「可食部(%)」字段(位于规格别名后,新增规格时随行出现,新增/编辑/详情三页同步);②食材列表操作区「食材分类管理」按钮后新增「净菜管理」入口;③新建 `din-clean-veg-mgmt.html` 净菜管理二级页:统计卡片 + 食材名称(下拉搜索)/食材编码/净菜类型(净菜俗名/净菜标准名/规格俗名三源关键字)/切配形状筛选 + 异步导出按钮组(导出列表数据+查看导出任务,样式对齐全仓列表页惯例)+ 列表组级列 rowspan 合并、可食部/规格别名/切配规格/刀工按规格逐行拆分(组内虚线分隔、规格列区纵向分隔线)+ 先选食材回显已有净菜类型再新增/编辑的维护弹窗;SITE_MAP 与营养模块 CHANGELOG 同步。(相关决策:[[decisions.md#净菜类型数据模型与净菜管理页展示模式]]
- 2026-09-07:全仓截图脚本与截图质量修复(commit 7f18470)——assistant/expert/health-monitor 截图脚本失效页面引用改回现存文件名并清理孤儿截图;weight-main-v2 初始化改为解析 `?type=` 参数,修复设计总览六种目标状态截图雷同问题,重拍 weight-main/bluetooth-scale 共 10 张;补拍 PRD 引用但从未生成的视频进行中截图;体重管理 PRD 修复两处截图断链。
- 2026-09-04:医疗点PAD端首版原型(commit 15ca37b)——新建 `other-module/medical-pad/` 20 页(1280×800 横屏,抽屉交互页内实现),SITE_MAP 与项目背景尺寸规范同步,新增模块 CHANGELOG;同提交:全站演示数据脱敏(示例人名王建国→石维国、周明远→曲志明,机构CQ能源→CQ油田,覆盖约 30 页)、现场体检签到码改条形码/二维码双选项卡切换、新增健康长寿系统升级改造详细规划 V8 文档。(相关规范:[[feedback_medical_pad_migration.md]]
- 2026-09-04:体重管理员工端设计总览重构(commit 29a09e9)——总览页按目标承诺与签署/体重主页/每日打卡/历史记录/建议与报告/蓝牙设备六大主线重排,页面清单全面切换到 weight-main-v2/goal-management-v2 等 v2 新版页面,新增 playwright 统一截图脚本 `screenshot-weight.mjs` 并重新生成全部截图。
- 2026-09-03~09-04:健康体检员工端与管理端多轮完善(commit 118d143、05e157e)——员工端:选项目页费用横幅重做为体检额度卡片(额度使用进度条+必检/选检/可用额度三格,年度额度 1600,新增心脏彩超与胃幽门螺杆菌两项选检),"我的预约"更名重做为"现场体检",胃肠镜预约结果页成功/失败两态演示;管理端:体检报告模板更名体检导入模板管理、模板额度配置更名群体额度配置、大病人员信息新增弹窗(员工搜索下拉+信息卡片)、指标异常通知固定列+发送通知/短信、单位/胃肠镜预约统计改单位级别 TAB。
- 2026-09-02~09-03:健康体检预约流程与检中页面完善(commit a443c61、0d1c770、37ae054、3a838d0、2213106)——员工端"我的预约"合并单页+新增指标纵向对比与消息中心;胃肠镜医院预选征集/已提交页、检前首页预选通知条、选日期页预约状态条与可约窗口限制(常规近30天/胃肠镜7~30天)、预约结果页成功/失败演示与取消预约二次确认;管理端新增大病人员信息/指标异常通知/已选项目员工信息页,胃肠镜体检计划管理新增/编辑/详情抽屉(承检医院多选+机构树选人),组合模板/例外配置/医院费用/单位费用统一详情弹窗与编辑抽屉,检中页更名员工预约体检跟踪、统一员工编号筛选与列、体检编号更名体检编码。
- 2026-09-01:健康体检计划与医院配置增强 + 检中检后侧边栏二级目录(commit 12f54cd、db8c2d1)——单位体检计划新增/详情弹窗与员工选择弹窗,医院承检范围抽屉改弹窗+详情弹窗,预约量按日配置(含跳过节假日/周末);检中/检后侧边栏改二级目录,胃肠镜 Tab 拆分独立成页(员工预约跟踪/单位预约统计/医院承检范围/单位体检计划管理)。
- 2026-08-31~09-01:餐线营养秤与净菜订单小迭代(commit deb366e、2996e83、7333189)——餐线营养秤四 Tab 分页、日期默认当天、人脸识别数据更名视频数据、概览筛选栏下沉+重量变化折线图;净菜订单生成历史加建议熟重/规格列、食谱快速创建份数改规格。
- 2026-08-28:健康体检检后三页 + 原型清理(commit 61103fc、1689707、ab27a39)——检后新增病种信息管理/重点指标异常统计/胃肠镜体检报告与统计页;删除膳食偏好设置页(diet-questionnaire.html)及关联截图,今日饮食建议改直接跳转;健康监护设备详情页水质/花粉 Tab 隐藏右上角综合指数。
- 2026-08-27:营养小程序新增意见反馈页(commit b96b936)——新增 `me-feedback.html`(反馈类型单选 chips / 500 字描述计数 / 截图上传上限 3 张 / 选填联系方式 / 提交校验 Toast);「我的反馈」历史列表三态徽标(已提交/处理中/已回复),点击展开官方回复,提交成功后 prepend 至列表顶部并重置表单;`me-home.html` 菜单插入「📮 意见反馈」入口(操作日志与设置之间,9→10 项);SITE_MAP 同步。
- 2026-08-27:应急救援求助弹窗左侧抽屉改版(commit 539a2ba)——求助信息紧凑化、健康画像关键项置顶、体检/穿戴告警折叠简化。
- 2026-08-27:体重管理 PRD 更新(commit f90f260、0af98f6)——PRD 说明更新;补充目标审核发送短信规则(10 点/15 点两次提醒待审核人员)。
- 2026-08-27:健康生产菜单「净配」改回「净菜组配」(commit 173e849)——「健康生产」模块 10 个页面侧边栏菜单「净配」→「净菜组配」(prod-proc-combo 含浏览器 title 与表格主标题、prod-proc-cook、prod-proc-serve、prod-proc-surplus、prod-proc-stats、prod-clean-order、prod-proc-cabinet、prod-recipe-plan、prod-recipe-plan-detail、prod-recipe-plan-create),修正 2922834 设备端术语统一时的误改;营养秤多维状态矩阵.md 移至 web-admin/nutrition/doc/ 目录。(相关决策:[[decisions.md#健康生产菜单「净菜组配」与设备端「净配秤」术语边界]])
- 2026-08-27:餐线营养秤字段与时间筛选调整(commit 11415da、81cddba)——din-terminal.html 设备概览「餐线营养秤」Tab:重量变化数据「提交重量」→「较上次变化」、「当前重量」→「当前余量」并取消两列筛选;四维度时间类列筛选由去重下拉改为日期时间组件。
- 2026-08-27:订餐终端设备概览新增餐线营养秤 Tab + 净菜订单增强(commit 40e0b31)——din-terminal.html 设备概览新增「餐线营养秤」Tab(四维度子页);prod-clean-order.html 新增「生成历史」按钮、「生成方式」列、详情就餐人数列。
- 2026-08-26:体重管理原型更新(commit 059d1c8)——管理端方案编辑审核人分组、员工端目标/打卡调整及 PRD、截图同步。
- 2026-08-25:体重管理多轮更新(commit e76ca89、9f8a80e、52aca0a、da410f7、e663982)——员工端运动建议 v2、管理端行动总览/员工数据记录页调整、体重秤/蓝牙秤/体脂仪监测详情弹窗字段对齐、三测口径调整、设备厂商统一蚁熊、员工端记录详情周日历来源标签等。
- 2026-08-25:净菜术语统一(commit 2922834)——设备端「净菜配比秤/组配秤」→「净配秤」、「净菜组配秤/配比秤」→「厨配秤」;管理端健康生产菜单「净菜组配」误改为「净配」,已于 2026-08-27 修正。
- 2026-08-25:餐线设置新增用户范围必填复选 + 食谱订单餐品明细表份数调整(commit 00cd565)。
- 2026-08-24:健康体检检前新增签约单位应检人数页 + 检中年份筛选(commit ef2dc7d);任务中心页重构(统计卡片/分段Tab/动态筛选)+ 仓库页新增净菜包柜(commit 564dd1c)。
- 2026-08-21:健康体检新增胃肠镜专项管理(单位预选计划/员工预选信息/预选统计)+ 胃肠镜问卷信息查询页,并完善中检预约跟踪/参与度/单位预约等页面(commit d13eb78)。
- 2026-08-21:净菜切配术语统一 + 净菜规格别名展示优化(commit 2d8b9e1)——①`din-ingredient-add.html` 切配形状选项「原状」统一为「原材」(含编辑模式 mock 数据 `cutSpec.shape`);②`din-dish-own.html` 净菜规格别名展示优化(去掉左边距,别名前加 `.` 点号前缀)。
- 2026-08-20:卫生供应新增任务中心 + 订餐终端净菜包货柜 + 净菜加工包装规格 + 包装标签状态列(commit 3bdc476)——①新增 `sup-task-center.html` 任务中心(4 个 Tab 分设备查看毛菜加工/净菜加工/净菜组配/净菜包装四类终端任务,统一列表字段与状态配色,支持筛选/导出/详情抽屉),同步更新 10 个卫生供应页面侧边栏 + SITE_MAP;②`din-terminal.html` 货柜 Tab 新增「净菜包货柜」,货柜数据模型新增 `itemLabel`/`unit` 字段支持包装单位展示;③`prod-clean-order.html` 食谱餐品明细表新增「包装规格」列;④`sup-pkg-label.html` 两个列表新增「完成时间」「状态」列。
- 2026-08-20:体重管理新增身高秤独立报告页(commit a155c0d)——新增 `height-scale-report.html` 身高秤独立报告页(复用 scale 报告模板),`report-detail.html` 同步支持身高秤设备切换。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

+8
View File
@@ -1,3 +1,11 @@
---
name: 用户画像
description: 用户角色与偏好
type: user
last_updated: 2026-09-10
commit: b5a8b83
---
# 用户画像
## 角色
+156
View File
@@ -0,0 +1,156 @@
---
name: android-capture
description: 通过 Android 真机 + adb 无线调试截取原生 APP 实际页面截图(用于操作手册)。当用户要求「截取 APP 实际页面」「真机调试截图」「生成 XX 端 APP 操作手册截图」时使用。
---
# SkillAndroid 真机 APP 截图
## 触发方式
用户说「截取 XX APP 的实际页面截图」「真机调试截图」时触发。
用于原生 Android / Flutter 混开 APPiOS 同款只做 Android)的实际页面截图。
## 连接方式:无线调试(云桌面场景首选)
**关键背景**:本环境是云桌面 + 云终端盒子,无本地物理机。手机 USB 插在云终端盒子上,但盒子只传画面不传 USB 设备,**云桌面的 Windows 看不到手机**,`adb devices` 永远为空。因此必须走 **adb over WiFi(无线调试)**USB 方式在此环境不可用。
### 无线调试流程(Android 11+
```bash
# 1. 手机:设置 → 开发者选项 → 无线调试 → 打开
# 2. 手机点「使用配对码配对设备」,拿到 6 位配对码 + 配对 IP:端口
# 3. 云桌面执行配对(配对码时效短,拿到后立刻执行)
echo "6位配对码" | adb pair 手机IP:配对端口
# 4. 配对成功后,看手机「无线调试」主页面的「IP 地址和端口」(调试端口,与配对端口不同)
adb connect 手机IP:调试端口
# 5. 验证(显示 device 即成功)
adb devices -l
```
- 配对码有 30 秒~几分钟时效,超时需手机重新点「使用配对码配对设备」
- 配对端口与调试端口是**两个不同端口**,别混用
-`adb pair``protocol fault`,通常是配对码过期,重新获取
## 前置:环境检测(每次先跑)
```bash
adb version # adb 是否可用
adb devices -l # 真机是否连接(显示 device 才算连上)
adb shell wm size # 屏幕分辨率(用于坐标换算)
```
- `adb` 不在 PATH:见「环境准备」装 platform-tools
- `adb devices` 空:无线调试未连上,回到「无线调试流程」重新配对/连接
## 环境准备(首次需要)
### 1. 安装 adbWindowscurl 一键)
```bash
curl -L -o /d/Work/platform-tools.zip https://dl.google.com/android/repository/platform-tools-latest-windows.zip
unzip -q -o /d/Work/platform-tools.zip -d /d/Work/android-sdk/
/d/Work/android-sdk/platform-tools/adb.exe version # 验证
```
- 或手动下载 [Android SDK Platform-Tools](https://developer.android.com/tools/releases/platform-tools) 解压
- 或安装 Android Studio(自带 adb
> 本机已装:`D:\android-sdk\platform-tools\adb.exe`,已加入用户 PATH(注册表已配,但云桌面需**注销重登**才在 cmd 生效;脚本执行时用 `export PATH="$PATH:/d/Work/android-sdk/platform-tools"` 临时生效即可)
### 2. 手机开启无线调试
1. 「设置 → 关于手机 → 版本号」连点 7 次,开启「开发者选项」
2. 「设置 → 开发者选项」打开「无线调试」(需 Android 11+
3. 手机连 WiFi(与云桌面同一网络)
### 3. 确认目标 APP 包名
```bash
adb shell pm list packages | grep -iE "health|yixiong|healthy|jk"
```
本机实际包名:`com.sw.healthyclients`(健康客户端)、`com.sw.healthexpertclient.dev`(健康专家客户端 dev
## 截图流程
### Step 1:启动 APP
```bash
adb shell monkey -p 包名 1 # 无需知道 Activity,直接拉起
sleep 3 # 等待加载
```
### Step 2:截图 + 读界面文字验证(每个页面都做)
```bash
# 截图
adb exec-out screencap -p > 页面名.png
# 读界面文字,确认截到的是哪个页面(用 adb shell cat 直接读,避免 pull 的路径坑)
adb shell "uiautomator dump /sdcard/ui.xml"
adb shell cat /sdcard/ui.xml | grep -oE 'text="[^"]+"' | grep -v 'text=""'
```
> **必须读文字验证画面**:截图前先用 uiautomator 读文字,确认当前页面/弹窗是目标,再截图,避免截错页面。
### Step 3:模拟操作
```bash
adb shell input tap 540 1200 # 点击坐标
adb shell input swipe 540 1600 540 600 300 # 上滑
adb shell input text "admin" # 输入英文/数字(中文需 adbkeyboard 或 broadcast
adb shell input keyevent 4 # 返回
```
### Step 4:元素定位(确定点击坐标)
```bash
adb shell "uiautomator dump /sdcard/ui.xml"
adb shell cat /sdcard/ui.xml | grep -oE 'text="[^"]+"[^>]*bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"'
# 从 bounds="[x1,y1][x2,y2]" 取中心点:tap_x=(x1+x2)/2, tap_y=(y1+y2)/2
```
### Step 5:截图后处理(压缩 + 分目录)
真机截图原始为 1264×2800 的 PNG(体积可达 1.5MB),需压缩后才能用于操作手册:
1. **转 JPEG + 缩宽**:用 PowerShell System.Drawing 将 PNG 缩放为 390px 宽、质量 85% 的 JPEG(参考 `项目背景.md` 7.4 节 PRD 截图规范,目标单张约 80KB 以内)
2. **分目录存放**:按终端分目录 `交付文档/{子系统名}/操作手册/screenshots/{终端目录}/`(如 `employee-app`),命名用中文页面名
```powershell
# 示例:将 PNG 缩放为 390px 宽、JPEG 质量 85%
Add-Type -AssemblyName System.Drawing
$img = [System.Drawing.Image]::FromFile("源.png")
$w = 390; $h = [int]($img.Height * $w / $img.Width)
$bmp = New-Object System.Drawing.Bitmap($w, $h)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$g.DrawImage($img, 0, 0, $w, $h)
$enc = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
$ep = New-Object System.Drawing.Imaging.EncoderParameters(1)
$ep.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, [long]85)
$bmp.Save("目标.jpg", $enc, $ep)
```
### Step 6:交给操作手册 skill
截图压缩完成后,交给 `doc-operation-manual` skill 组织成操作手册。
## 输出物
- `交付文档/{子系统名}/操作手册/screenshots/{终端目录}/*.jpg`APP 实际页面截图,JPEG 390px
## 注意事项
- **git bash 路径坑**`/sdcard/ui.xml` 会被 MSYS 转义成 Windows 路径,用 `adb shell "uiautomator dump /sdcard/ui.xml"`(加引号)+ `export MSYS_NO_PATHCONV=1` 规避
- **读文字验证画面**:截图前务必用 uiautomator 读 text 确认页面,避免截错
- **配对码时效短**:无线调试配对码几十秒就失效,拿到立刻配对
- **截图命名用中文页面名**,与操作手册章节一一对应
- **截图必须压缩**:真机截图原始 PNG 体积大,需缩放为 390px 宽 JPEG(质量 85%)再交付,不直接用原图
- **截图按终端分目录**`screenshots/{终端目录}/`,避免多终端截图混在同一目录
- **点击坐标依分辨率换算**:先 `adb shell wm size` 确认(本机 1264×2800
- **中文输入受限**`adb shell input text` 只支持 ASCII,中文需 adbkeyboard 或 `am broadcast`
- 登录页等需要验证码/密码的场景,输入密码可自动化,验证码需人工识别
- 若 APP 未适配真机(自定义 ROM 兼容问题),先与开发确认
@@ -1,3 +1,8 @@
---
name: design-overview
description: 生成模块设计总览页(页面形态清单 + 截图复用 + 设计总览 HTML)。当用户要求「生成设计总览」「design overview」时使用。
---
# Skill: 生成模块设计总览页
## 触发方式
+72
View File
@@ -0,0 +1,72 @@
---
name: doc-brochure
description: 生成指定子系统的《产品宣传册》(横版 A4,HTML 生成后自动转 PDF 交付)。当用户要求「生成 XX 系统的宣传册/产品手册」时使用。
---
# Skill:生成产品宣传册
## 触发方式
用户说「生成 XX 系统的宣传册」「做一份 XX 系统产品手册」时触发。
参数「XX 系统」为业务域子系统名。
## 输入参数
- **子系统名**(必须,中文):如 `体重管理系统`
- **Slogan / 价值主张**(可选):缺省由 skill 依据子系统概述提炼
- **平台品牌名**(可选):缺省用通用名「企业健康管理平台」,不得含客户信息
## 模板与素材
- 模板:`_templates/docs/brochure-template.html`
- 素材:该子系统各终端 `doc/PRD.md` 的模块概述、页面清单;`项目背景.md` 的终端/角色说明
## 执行流程
### Step 1:采集子系统信息
1. 定位子系统各端目录(方法见 doc-detailed-design Step 1
2. 读各端 PRD「模块概述」提炼:产品定位、核心价值、功能模块清单
3. 从页面清单统计:终端数、功能模块数、页面数(用于概述页的指标卡片)
4.`项目背景.md` 6.2 用户群体表提炼「终端覆盖 + 服务人群」
### Step 2:提炼宣传内容(精炼,忌堆砌)
- **封面**:产品名 = 子系统名,Slogan 一句话价值主张,3 个关键词 tag
- **概述**:定位一句话 + 价值 2~3 句 + 4 个量化指标(终端/模块/页面/人群)
- **核心功能**:选 4~6 个最有代表性的功能模块,每个配 1 个 emoji 图标 + 简介 + 2 个亮点
- **特色优势**:6 条优势(01~06 编号卡片)
- **终端覆盖**:列出该子系统覆盖的终端(每个配 emoji + 一句说明)
- **界面展示**:放 5 张 APP 竖版截图(手机边框 130×282)+ 3 张 Web 端截图(浏览器边框 290×181),分两行(上方 5 张 APP、下方 3 张 Web),从各端 `doc/screenshots/` 选核心页面(长截图用 object-fit 顶部裁剪)
### Step 3:填充 HTML 模板
1. 复制 `brochure-template.html`,将 `{占位符}` 替换为采集到的内容
2. 功能模块页如超过 6 个功能,复制 `.feat` 卡片或新增一页「核心功能」
3. 保持内联 CSS 与设计令牌不变,仅替换文字与 emoji
4. 确保所有 `{...}` 占位符都被替换,不留占位符残留
### Step 4:写入 HTML 源文件并检查
1. 输出 HTML 到 `交付文档/{子系统名}/{子系统名}产品宣传册.html`
2. 用浏览器打开检查:内容、截图是否加载、分页、无 `{占位符}` 残留
### Step 5:生成 PDF(必做)
1. 检查 HTML 无误后,运行 `node _templates/docs/pdf-export.js <html文件>` 转出同名 PDF
2. PDF 为最终交付物,HTML 作为源文件保留
3. 转 PDF 参数已内置:横版 A4(landscape+ 保留背景色(printBackground
## 输出物
- `交付文档/{子系统名}/{子系统名}产品宣传册.pdf`(交付物)
- `交付文档/{子系统名}/{子系统名}产品宣传册.html`(源文件)
## 注意事项
- **美观精炼**:宣传册面向客户,语言要有感染力,忌长篇技术描述;每页信息密度适中,元素丰富(图标/装饰/渐变卡片),避免单一
- **横版分页**:每 `.page` 一屏一页(297×210mm 横版 A4),内容横向铺满,避免纵向留白;同一版块的文字与截图不跨页(内容超出一页时精简或拆页,不硬塞导致切割)
- **配色统一**:沿用模板品牌蓝 `#1890FF` 设计令牌,不另造配色
- **内容通用**:宣传册必须通用,不得出现客户名称与项目名称(客户公司名、甲方名称、委托方、油田等一律不写)
- emoji 图标用于视觉占位,正式交付如需替换为专业图标,告知用户
- 文档名称「{子系统名}产品宣传册.html / .pdf」为中文,符合甲方要求
@@ -0,0 +1,77 @@
---
name: doc-database-design
description: 生成指定子系统的《数据库设计说明书》,根据研发提供的 SQL 文件解析表结构。当用户要求「生成 XX 系统的数据库设计说明书」并提供 SQL 文件/目录时使用。
---
# Skill:生成数据库设计说明书
## 触发方式
用户说「生成 XX 系统的数据库设计说明书」并给出 SQL 文件或目录时触发。
参数「XX 系统」为业务域子系统名。
## 输入参数
- **子系统名**(必须,中文):如 `体重管理系统`
- **SQL 文件或目录**(必须):研发提供的建表 SQL(单个 `.sql` 文件或含多个 SQL 的目录)
## 模板与素材
- 模板:`_templates/docs/database-design-template.md`
- 解析脚本:`_templates/docs/parse-sql.js`(自动解析 PostgreSQL SQL,直接生成 Markdown
- 表结构格式沿用 `doc/体重管理项目系统设计说明.docx` 5.3 节(注意:docx 为 MySQL 示例,本项目实际为 PostgreSQL
## 执行流程
### Step 1:定位 SQL 源
1. 用户指定的 SQL 文件/目录路径;目录则 Glob `**/*.sql` 收集全部 SQL 文件
2. 逐个读取,定位 `CREATE TABLE` 语句
### Step 2:解析表结构(PostgreSQL
**本项目数据库为 PostgreSQL**,优先用脚本自动解析:
```bash
node _templates/docs/parse-sql.js <SQL文件> <输出md路径> <子系统名> PostgreSQL
```
脚本自动解析:`CREATE TABLE` 块、表注释、字段注释、主键、索引,并直接生成 Markdown。
**手动解析时**PostgreSQL 格式规则:
- 表名:`CREATE TABLE "schema"."table" (` 中的 `table`
- 表注释:`COMMENT ON TABLE "schema"."table" IS '...'`
- 字段注释:`COMMENT ON COLUMN "schema"."table"."col" IS '...'`
- 主键:`ALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY ("col")`(本项目主键统一为雪花 `id`
- 允许空值:`NOT NULL``NO`,否则 `YES`
- 数据类型+长度:`varchar(255)` → varchar/255`numeric(6,2)` → numeric/6,2`int8`/`int4`/`int2`/`timestamp(0)`/`date`/`text` → 长度空
### Step 3:按模板生成
`database-design-template.md` 生成:
- **1. 概述**:数据库环境(PostgreSQL / UTF-8+ 命名规范 + 设计原则(直接复用模板通用条文)
- **2. 数据表清单**:汇总所有表「表中文名(取表注释,无注释则暂留空)+ 英文表名 + 说明」,按业务模块分组
- **3. 数据表结构说明**:每表一个小节,标题 `### 3.x {表中文名}({英文表名})`,字段表 7 列:
`| 序号 | 列名 | 数据类型 | 长度 | 主键 | 允许空值 | 列说明 |`
- **4. 表关系与 ER 图**:根据外键/关联字段推断表关系,生成 Mermaid `erDiagram`;无法推断则列出主外键关联说明
- **5. 索引与约束设计**:整理主键、唯一索引、普通索引
- **6. 数据字典**:整理枚举类字段(如状态、类型)的取值字典,从字段注释中的「0-xx,1-xx」描述提取
### Step 4:写入输出文件
输出到 `交付文档/{子系统名}/{子系统名}数据库设计说明书.md`
## 输出物
- `交付文档/{子系统名}/{子系统名}数据库设计说明书.md`(源文件)
- `交付文档/{子系统名}/{子系统名}数据库设计说明书.pdf`(最终交付物,竖版 A4,由使用方从 .md 转出)
## 注意事项
- SQL 注释是字段中文说明的唯一来源,`COMMENT ON COLUMN` 缺失时列说明留空并汇总提示用户补充
- 表结构字段表严格沿用 7 列格式(序号/列名/数据类型/长度/主键/允许空值/列说明)
- 若 SQL 无表注释,表中文名先留空,提醒用户补齐
- **内容通用**:全文不得出现客户名称与项目名称,只介绍数据库自身设计,产出文档可供任意客户与人群阅读
- **交付格式**:最终交付为竖版 A4 PDF(.md 为源,由使用方导出),分页遵循模板「PDF 导出规范」三原则,避免表行切割
- 文档全部使用简体中文
@@ -0,0 +1,97 @@
---
name: doc-detailed-design
description: 生成指定子系统的《详细设计说明书》(8 章正式框架)。当用户要求「生成/编写 XX 系统的详细设计说明书」「系统设计说明」「详细设计」时使用。参考 doc/体重管理项目系统设计说明.docx 的章节结构。
---
# Skill:生成详细设计说明书
## 触发方式
用户说「生成 XX 系统的详细设计说明书」「编写 XX 系统设计说明书」「XX 系统详细设计」时触发。
参数「XX 系统」为业务域子系统名,如「体重管理系统」「健康体检系统」。
## 输入参数
- **子系统名**(必须,中文):如 `体重管理系统`
- **补充信息**(可选):技术架构说明等,缺省用模板默认值(不得含甲方名称等客户信息)
## 模板与素材
- 模板:`_templates/docs/detailed-design-template.md`
- 架构图模板:`_templates/docs/architecture-business.html`(业务架构图)、`_templates/docs/architecture-technical.html`(技术架构图)
- 公共章节素材:`_templates/docs/common-sections-template.md`
- 项目背景:根目录 `项目背景.md`
- 各终端 PRD`{模块目录}/doc/PRD.md`(提供模块概述、导航结构、逐页说明)
## 执行流程
### Step 1:定位子系统目录(多端)
1. 读根目录 `index.html`grep `SITE_MAP``name:'{子系统名}'` 的模块条目(同一子系统可能出现在多个端,如 Web 管理端与员工端 APP 各一条)
2. 每个条目提取两个字段:
- `dir`:模块英文目录(如 `web-admin/weight-management`
- `prd`PRD 文档路径(如 `web-admin/weight-management/doc/PRD.md`,部分模块可能缺失)
3. 汇总得到「端目录列表 + 各端 PRD 路径」
4. 兜底:某端 `prd` 字段缺失时,用 Glob 检查 `{dir}/doc/PRD.md` 是否存在;SITE_MAP 未收录的独立产物(如 other-module 子设备)用 Glob 扫描补全
### Step 2:扫描页面清单
1. 对每个端目录,从 SITE_MAP 的 `pages` 数组读取页面(`file` + `label` 中文名)
2. 若 SITE_MAP 未覆盖(如 other-module 子设备),用 Glob 列目录下 `.html` 文件
3. 读各端 `doc/PRD.md` 的「模块概述」「页面导航结构」段,获取业务主线分组(一级菜单/侧边栏组)
### Step 3:填充模板
`detailed-design-template.md` 逐章填充:
- **封面**:系统名称 = `{子系统名}`,日期 = 当天(信息表不写委托方,见模板)
- **一、项目背景**:综合 `项目背景.md` 6.1 节 + 各端 PRD 模块概述
- **二、建设目标**:分条,参考 docx「建设目标」句式(构建生态/打造引擎/实现追踪/建立评估)
- **三、需求说明与功能设计**
- 3.1 需求概述:按端分列需求点
- 3.2 功能清单:列表罗列各端菜单名称(后台 + 移动端分开)
- 3.3 业务架构设计:四层描述(交互层/应用服务层/整合层/数据服务层)+ 业务架构图(H5 生成后截图,见 Step 4)
- 3.4 核心业务详细设计:**按「端 → 功能模块(一级菜单)→ 功能点」三级展开**,每个功能点写「功能描述 + 功能点列表」
- **四、技术架构**:基于实际技术栈(后端 Spring Cloud Alibaba + Nacos + PostgreSQL + Redis + RocketMQ + XXL-Job + MinIO + Feign + Sentinel + Sa-Token;前端 Qiankun 微前端 + Vue3 + Ant Design Vue+ 技术架构图(H5 生成后截图)
- **五、数据库**:写概述 + 引用《{子系统名}数据库设计说明书》
- **六、安全与可用性**:补全安全措施(身份认证/权限/加密/传输/审计/脱敏)+ 可用性设计(性能指标表,参考共性需求说明.md 性能要求)
- **七、部署与运维**:补全部署架构、环境划分、部署步骤、运维方案、告警机制
- **八、售后服务**:可选,甲方无要求则标注「本章略」
### Step 4:并入公共章节
`common-sections-template.md` 复制「登录与认证 / 消息 / AI 助手 / 个人中心 / 组织架构」章节,作为「三、需求说明」下的「公共部分」小节,或按需融入各端功能设计。
### Step 5:架构图与截图引用
**架构图生成(业务架构图 + 技术架构图)**
1. 复制 `_templates/docs/architecture-business.html` / `architecture-technical.html`,按子系统内容修改各层盒子的标题与子项
- **业务架构图·应用与服务层**:按一级菜单分组为横排竖版板块(`.domain`),板块内挂二级菜单或提炼的核心功能项;管理端功能用 `.d-item`、员工端 APP 功能用 `.d-item--app`(建议 📱 前缀);智能模型类用 `.domain-model` 虚线板块;底部保留双端图例(`.legend`
- 业务架构图层与层之间用横向分隔线(`.divider`)分隔,不使用竖线箭头
2. 用浏览器打开 HTML,设置视口宽度 1008px、高度设为内容实际高度(用 JS 获取最后一个盒子底部位置 + body padding
3. 截图保存为 `交付文档/{子系统名}/业务架构图.png``技术架构图.png`
4. 文档中引用 `![业务架构图](业务架构图.png)``![技术架构图](技术架构图.png)`
**界面原型截图**
- 沿用各端 `doc/screenshots/` 现有截图
- 路径从输出文档位置指回:`../../{端目录}/doc/screenshots/{页面名}.jpg`
- 无截图则省略「界面原型」图,保留文字说明
### Step 6:写入输出文件
输出到 `交付文档/{子系统名}/{子系统名}详细设计说明书.md`,目录不存在则创建。
## 输出物
- `交付文档/{子系统名}/{子系统名}详细设计说明书.md`(源文件)
- `交付文档/{子系统名}/{子系统名}详细设计说明书.pdf`(最终交付物,竖版 A4,由使用方从 .md 转出)
## 注意事项
- **截图沿用现有**,不重新截取;无现成截图就不放图
- 技术架构/安全/部署/售后等通用章节可跨子系统复用,不必每次从零写
- 3.3 核心业务详细设计是本说明书的主体,功能点要与页面清单一一对应,不得遗漏页面
- **内容通用**:全文不得出现客户名称与项目名称(客户公司名、甲方名称、委托方等一律不写),只介绍系统自身概况,产出文档可供任意客户与人群阅读
- **交付格式**:最终交付为竖版 A4 PDF(.md 为源,由使用方导出),分页遵循模板「PDF 导出规范」三原则,避免图文割裂、表行切割
- 文档全部使用简体中文,表格、标题层级严格按模板
- 不改动任何现有文件,只新建交付文档
@@ -0,0 +1,119 @@
---
name: doc-operation-manual
description: 生成指定子系统、指定终端的《操作手册》。当用户要求「生成 XX 系统 XX 端操作手册」「编写 XX 端使用手册/操作说明」时使用。操作手册按「子系统 + 终端」分册,截图必须用实际系统截图(不用原型图)。
---
# Skill:生成操作手册
## 触发方式
用户说「生成 XX 系统的 XX 端操作手册」「编写 XX 端操作说明/使用手册」时触发。
需同时指定「子系统名」和「终端名」。
## 输入参数
- **子系统名**(必须,中文):如 `健康监测系统`
- **终端名**(必须,中文):如 `Web管理端` / `员工端APP` / `医疗点PAD`/ `健康应急APP`
- **适用对象**(可选):用户角色,缺省按终端推断
## 核心原则:截图必须用实际系统截图
**操作手册严禁用原型图**。原型和实际交付页面有差异,必须用测试环境真实页面截图。截图来源按终端区分:
| 终端 | 截图方式 | 对应 skill |
|------|---------|-----------|
| Web 管理端 | 浏览器自动化截图 | `web-capture` |
| 员工端 APP(原生 Android/Flutter | 真机 adb 截图 | `android-capture`(待编写) |
| 医疗点 PAD(横屏 Web) | 浏览器横屏视口截图 | `web-capture` |
| 微信小程序 | 浏览器模拟手机视口(若为 H5) | `web-capture` |
## 模板与素材
- 模板:`_templates/docs/operation-manual-template.md`
- 公共章节素材:`_templates/docs/common-sections-template.md`
- **文字素材**:该终端 `doc/PRD.md` 的「逐页说明」(功能说明 + 交互说明 + 业务规则),用于提炼操作步骤与注意事项
- **截图素材**:由截图 skill(`web-capture` / `android-capture`)生成的实际系统截图,按终端分目录存放 `交付文档/{子系统名}/操作手册/screenshots/{终端目录}/`
## 执行流程
### Step 0:先截图(前置,必须)
1. **Web 管理端**:调用 `web-capture` skill,登录测试环境 → 导航到目标模块 → 逐页面截图
2. **员工端 APP**:调用 `android-capture` skill,真机 adb 逐页面截图
3. **截图范围**:只截「主界面」和「重要表单/详情页」,弹窗、筛选下拉等轻量交互不单独截图(在功能描述中用文字/表格说明)
4. **分目录存放**:截图按终端分目录,`交付文档/{子系统名}/操作手册/screenshots/{终端目录}/`,终端目录用英文(`employee-app` / `web-admin` / `medical-pad` 等),命名用中文页面名
5. **截图规格**:输出 JPEG(质量 85%,清晰度优先,不设文件大小上限),APP 端宽度 390px、Web 端视口 1440×900;截图前确保浏览器全屏或设置正确视口,避免截图不完整、比例失调(参考 `项目背景.md` 7.4 节 PRD 截图规范)
6. 若截图 skill 尚未就绪或模块未上线,**暂停并告知用户**,不退回用原型图
### Step 1:定位模块文字素材
1. 读根目录 `index.html`grep `SITE_MAP``name:'{子系统名}'` 的条目
2. 根据「终端名」匹配所属端,定位模块目录与 `doc/PRD.md`
3. 读 PRD 的「模块概述」「页面清单」「逐页说明」,获取**菜单结构 + 功能说明 + 交互说明 + 业务规则**(用于文字,不用其截图)
### Step 2:整理运行环境与适用对象
- 运行环境引用 `项目背景.md` 2.x 视觉规范对应终端尺寸:
- Web 管理端:浏览器,最小宽度 1280px
- 员工端 APP390×844iPhone 14
- 医疗点 PAD1280×800 横屏
- 微信小程序:375×812
- 适用对象按终端推断(平台管理员/企业员工/医疗点医护人员等)
### Step 3:按业务线路组织操作手册(核心)
**先梳理业务线路**(不是按页面/菜单顺序罗列):
1. 从 PRD 的「模块概述」「页面导航结构」提炼**业务主线**——即用户完成某个业务目标的完整操作流程
2. 每条业务线路对应手册一个章节(通常对应一个一级菜单/业务域,但不是简单按菜单排)
3. 示例(健康监测):① 档案查询 → ② 健康现状查看 → ③ 健康排查 → ④ 穿戴监测 → ⑤ 环境监测 → ⑥ 异常处理
**选择性覆盖原则**(与 PRD 前后呼应,但绝不完全一样):
- 只覆盖**核心操作**(高频、关键业务操作),不罗列所有小交互
- **截图只截主界面 + 重要表单/详情页**:操作手册的截图仅覆盖「主界面」和「重要表单/详情页」;弹窗、筛选条件下拉、选项列表等轻量交互**不单独截图**,在「功能说明」或「注意事项」中用文字/表格说明即可
- **页面状态选择性说明**:只说明用户常见的关键状态,不穷举所有状态(如只讲"进行中"和"已结束",不讲所有中间态)
`operation-manual-template.md` 生成:
- **1. 概述**:系统简介 + 适用对象 + 运行环境 + 登录说明(步骤化)
- **2. 界面总览**:布局说明 + 首页/工作台截图 + 导航结构(含业务线路导读)
- **3. 功能操作指南**:按业务线路分组,每条线路写核心操作:
- **操作步骤**(编号列表,从「进入某页面」到「完成核心操作」)
- **界面截图**(核心页面 + 主要弹窗,引用实际系统截图)
- **注意事项**(关键业务规则,用户须知)
- **4. 常见问题与故障处理**:核心操作的高频问题(表格)
- **5. 附录**:术语表 + 快捷操作
### Step 4:并入公共操作章节
`common-sections-template.md` 复制「登录与认证 / 消息 / AI 助手 / 个人中心」相关操作说明,并入第 3 章相应位置。
### Step 5:截图引用
- 引用 `交付文档/{子系统名}/操作手册/screenshots/{终端目录}/` 下的实际系统截图
- 路径相对操作手册文件:`screenshots/{终端目录}/{页面名}.jpg`
- 若某页面截图缺失,标注「待补」,不退回用原型图
### Step 6:写入输出文件
输出到 `交付文档/{子系统名}/操作手册/{子系统名}{终端名}操作手册.md`
## 输出物
- `交付文档/{子系统名}/操作手册/{子系统名}{终端名}操作手册.md`(源文件)
- `交付文档/{子系统名}/操作手册/{子系统名}{终端名}操作手册.pdf`(最终交付物,竖版 A4,由使用方从 .md 转出)
- `交付文档/{子系统名}/操作手册/screenshots/{终端目录}/*.jpg`(实际系统截图,JPEG 390px
## 注意事项
- **截图必须实际系统截图**,严禁原型图;模块未上线或截图 skill 未就绪时,暂停并告知用户
- **截图只截主界面 + 重要表单/详情页**:弹窗、筛选下拉等轻量交互不单独截图,在功能描述中用文字/表格说明
- **截图规格**:JPEG 质量 85%(清晰度优先,不设文件大小上限),APP 端宽度 390px、Web 端视口 1440×900(截图前确保浏览器全屏,参考 `项目背景.md` 7.4 节)
- **截图按终端分目录**`screenshots/{终端目录}/`,终端目录用英文,避免多终端截图混在同一目录
- **与 PRD 前后呼应**:手册章节对应 PRD 功能模块,但只覆盖核心操作、主要弹窗、核心二级页面,不逐页罗列所有弹窗/小交互/页面状态
- 操作手册面向**终端用户**,语言通俗、步骤化,避免实现细节(字段名、接口名)
- 每个子系统的每个终端单独一份手册,公共操作(登录/消息等)在各手册中重复体现(甲方要求)
- **内容通用**:全文不得出现客户名称与项目名称,只介绍系统自身操作,产出文档可供任意客户与人群阅读
- **交付格式**:最终交付为竖版 A4 PDF(.md 为源,由使用方导出),分页遵循模板「PDF 导出规范」三原则,避免图文割裂、表行切割
- 文档全部使用简体中文
+81
View File
@@ -0,0 +1,81 @@
---
name: doc-prd-merge
description: 将同一子系统的多个终端 PRD 合并成一份《产品需求文档》。当用户要求「合并 XX 系统的 PRD」「生成 XX 系统产品需求文档」时使用。只合成新文件,不改动现有各终端 PRD,沿用现有截图。
---
# Skill:合并产品需求文档(PRD)
## 触发方式
用户说「合并 XX 系统的 PRD」「生成 XX 系统产品需求文档」「整理 XX 系统 PRD」时触发。
参数「XX 系统」为业务域子系统名。
## 输入参数
- **子系统名**(必须,中文):如 `体重管理系统`
## 模板与素材
- 模板:`_templates/docs/prd-merge-template.md`
- 素材:各终端已有的 `{模块目录}/doc/PRD.md`**只读取,不改动**
## 执行流程
### Step 1:定位子系统各终端的 PRD
1. 读根目录 `index.html`grep `SITE_MAP``name:'{子系统名}'` 的条目,提取每条目的 `prd` 字段,得到「终端名 → PRD 路径」映射,如:
- Web管理端 → `web-admin/weight-management/doc/PRD.md`
- 员工端APP → `employee-app/weight-management/doc/PRD.md`
2. `prd` 字段缺失的端,用 Glob 检查 `{dir}/doc/PRD.md` 是否存在
3. 确认每条 PRD 路径文件真实存在后再读取
### Step 2:读取各终端 PRD 内容
对每个 PRD,提取四类内容:
1. **模块概述**`## 模块概述` 段 + 页面导航结构树)
2. **页面清单**`## 页面清单` 段,表格)
3. **逐页说明**`## 逐页说明` 段,含截图引用、功能说明、业务规则、交互说明)
4. **表单字段表**`## 表单字段表` 段)
### Step 3:按模板合成新文档
`prd-merge-template.md` 合成。**交付定位:单独交付、不与原型代码一起交付,正文与标题中不得出现页面文件路径与页面文件名(`.html` 文件名、目录路径一律清理)**
- **标题**`{子系统名}产品需求文档`;**不写**「本文档由…合并生成」来源说明区
- **1. 子系统概述**:整合各终端「模块概述」为统一一段 + 按端分组的「系统功能清单」
- 「系统功能清单」用 **HTML 表格 + rowspan 合并单元格**,三列:功能模块 / 功能子模块 / 功能点(一级=功能模块,二级=功能子模块,叶子=功能点;无二级的模块列2留空)
- 取代原「页面导航结构」树与「页面清单」章节,不再单列页面清单
- **2. 逐页说明**:按端分组沿用原文,但:
- 每页标题仅保留功能名,**去掉「(文件名.html)」**
- 正文中「跳转到 XXfilename.html)」等引用,**去掉括号内文件名**
- 截图路径改为「从本文档位置指回原模块 doc/screenshots/ 的相对路径」,截图本身保留
- **3. 表单字段表**:仅当各终端原 PRD 存在独立「表单字段表」段时才合并,否则省略
### Step 4:重算截图相对路径
- 原 PRD 内截图引用为 `screenshots/xxx.jpg`(相对其所在 `doc/` 目录)
- 合并后文档位于 `交付文档/{子系统名}/{子系统名}产品需求文档.md`
- 指向 `web-admin/weight-management/doc/screenshots/xxx.jpg` 的相对路径为:
`../../web-admin/weight-management/doc/screenshots/xxx.jpg`
- 逐一改写合并文档中的截图路径,**沿用现有图片文件,不复制、不重新截图**
### Step 5:写入输出文件
输出到 `交付文档/{子系统名}/{子系统名}产品需求文档.md`
## 输出物
- `交付文档/{子系统名}/{子系统名}产品需求文档.md`(合并版源文件,新文件)
- `交付文档/{子系统名}/{子系统名}产品需求文档.pdf`(最终交付物,竖版 A4,由使用方从 .md 转出)
## 注意事项
- **绝对不改动现有各终端 PRD 文件**,只新建合并文档
- **截图沿用现有**:用相对路径指回原位置,不复制图片、不重新截图
- **交付文档单独交付,不与原型一起交付**:正文与标题中**不得出现页面文件路径与页面文件名**(`.html` 文件名、目录路径一律清理);截图是必需交付内容,其引用路径保留相对路径指回原 `doc/screenshots/`
- **不写**「本文档由以下终端 PRD 合并生成」来源说明区
- **系统功能清单**替代「页面导航结构」树与「页面清单」章节,用 HTML 表格 + rowspan 合并单元格(三列:功能模块 / 功能子模块 / 功能点)
- 逐页说明、表单字段表均按「端」分组,保持各端内容完整可追溯
- **内容通用**:全文不得出现客户名称与项目名称,只介绍产品需求本身,产出文档可供任意客户与人群阅读
- **交付格式**:最终交付为竖版 A4 PDF(.md 为源,由使用方导出),分页遵循模板「PDF 导出规范」三原则;「系统功能清单」rowspan 合并表尤需保证合并单元整体不跨页切割
- 文档全部使用简体中文
+97
View File
@@ -0,0 +1,97 @@
---
name: web-capture
description: 截取 Web 管理端测试环境的实际系统页面截图(用于操作手册)。当用户要求「截取 XX 模块的实际页面」「生成 XX 模块操作手册截图」「登录测试环境截图」时使用。
---
# Skill:Web 管理端实际系统截图
## 触发方式
用户说「截取 XX 模块的实际页面截图」「登录测试环境截图」「生成 XX 模块操作手册」时触发。
## 输入参数
- **测试环境地址**(必须):如 `https://dev.yixiong-tech.com:8086/login`
- **账号 / 密码**(必须):如 `admin` / `Aa987654321!`
- **目标模块**(必须,中文):如 `健康监测``营养管理`
## 关键约束(本系统特有,务必遵守)
1. **验证码需人工识别**:登录页有 4 位图片验证码,Claude 无法自动识别图片。流程是——截图验证码存成文件 → 请用户看图报出验证码 → 填入登录。
2. **微前端子应用**:各业务模块是 Qiankun 子应用,URL 为 `/subXxx/...`,不是简单的一级路径。
3. **纯 JS 路由,无 `<a href>`**:页面用 Vue Router,菜单是 Ant Design 组件,**不能用 `navigate_page` 直接跳菜单**,也不能找 `<a href>`
4. **popover 菜单项 click 不跳转**:首页模块卡片 hover 弹出的 `ant-popover` 菜单项,直接 click 不生效。**可靠导航方式是 `router.push()`**(见下)。
## 模块 → 子应用 URL 映射
| 模块名 | 子应用路由前缀 |
|--------|--------------|
| 系统管理 | `/subSystemManagement/` |
| 健康监测 | `/subHealthMonitoring/` |
| 健康评估 | `/subHealthAssessment/` |
| 专家咨询 | `/subHealthExpertConsult/` |
| 健康助手 | `/subHealthHelper/` |
| 应急就医 | `/subEmergencyMedical/` |
| 营养管理 | `/subNutritionManagement/` |
| 体重管理 | `/subWeightManagement/` |
## 执行流程
### Step 1:打开登录页并截图验证码
1. 用 chrome-devtools `new_page` 打开测试环境登录页
2. `fill_form` 填账号、密码
3. `take_screenshot` 截图(含验证码),保存到临时文件(如 `_login_captcha.png`
4. **请用户看图报出 4 位验证码**
### Step 2:登录
1. 用户报验证码后,`fill` 验证码输入框 + `click` 登录按钮
2.`evaluate_script` 检查 `location.href` 是否从 `/login` 跳到 `/home`
3. 若失败(验证码错误/过期),重新截图验证码请用户再报
### Step 3:导航到目标模块
`evaluate_script` 调用 Vue Router 的 `push`
```js
() => {
const router = document.querySelector('#app').__vue_app__.config.globalProperties.$router;
router.push('/subHealthMonitoring/archiveModule/personalEmergencyData');
return '已导航';
}
```
- 子应用内部具体路径需先进入模块后,从浏览器地址栏读取实际 URL
- 导航后页面跳转到子应用,`location.href` 会变成 `/subXxx/...`
### Step 4:摸清模块菜单结构
1. `take_snapshot` 获取侧边栏菜单:一级菜单(`menuitem`+ 二级菜单
2. 记录菜单层级,规划截图清单——**只截「主界面」+「重要表单/详情页」**,弹窗、筛选下拉、选项列表等轻量交互不单独截图
### Step 5:逐页面截图
1. **确保视口正确(避免截图不完整/比例失调)**:截图前先 `resize_page` 将浏览器窗口设置为 1440×900(或更大以匹配完整页面),确保窗口已全屏、无横向滚动条遗漏;页面加载后再截图
2. 对每个目标页面:`router.push` 到对应 URL(或点击侧边栏菜单项),等待页面加载完成(`readyState === 'complete'`
3. `take_screenshot``format: "jpeg"` + `quality: 85`,保存为 `.jpg`(不是 `.png`
4. **分终端目录存放**`交付文档/{子系统名}/操作手册/screenshots/web-admin/{页面名}.jpg`,截图命名用中文页面名(如 `个人监测数据.jpg`
5. **压缩比例**JPEG 质量 85%(参考 `项目背景.md` 7.4 节 PRD 截图规范),清晰度优先,不设文件大小上限
### Step 6:交给操作手册 skill
截图完成后,交给 `doc-operation-manual` skill,用真实截图组织成操作手册。
## 输出物
- `交付文档/{子系统名}/操作手册/screenshots/web-admin/*.jpg`(实际系统截图,JPEG 质量 85%)
## 注意事项
- **验证码是每次登录的障碍**:优先建议让开发在测试环境关闭验证码;未关闭则每次登录请用户人工报验证码
- **模块可能未上线**:首页模块卡片标「开发中,敬请期待」的模块不可截图,先和用户确认目标模块是否可用
- **截图以实际系统为准**:操作手册严禁用原型图,必须是测试环境真实页面
- **截图前确保浏览器全屏/正确视口**:窗口未全屏会导致截图不完整、比例失调,截图前先 `resize_page` 设置 1440×900 视口
- **截图只截主界面 + 重要表单/详情页**:弹窗、筛选下拉等轻量交互不单独截图,在功能描述中用文字/表格说明
- **截图规格**:JPEG 质量 85%(清晰度优先,不设文件大小上限),分终端目录 `web-admin/` 存放
- 截图文件名用中文页面名,与操作手册章节一一对应
+2 -1
View File
@@ -12,4 +12,5 @@ _*.png
_*.jpg
_*.jpeg
_*.gif
_*.webp
_*.webp
/交付文档/
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ClaudeCodeTabState">
<option name="tabSessions">
<map>
<entry key="0">
<value>
<TabSessionState>
<option name="provider" value="claude" />
<option name="sessionId" value="1a5b94ba-13db-41c5-99dc-97e92d5c7371" />
<option name="cwd" value="$PROJECT_DIR$" />
<option name="model" value="claude-opus-4-7[1m]" />
<option name="permissionMode" value="bypassPermissions" />
<option name="reasoningEffort" value="high" />
</TabSessionState>
</value>
</entry>
<entry key="1">
<value>
<TabSessionState>
<option name="provider" value="claude" />
<option name="sessionId" value="2b914133-b4ca-4fe5-8982-8bd381b51120" />
<option name="cwd" value="$PROJECT_DIR$" />
<option name="model" value="claude-opus-4-7[1m]" />
<option name="permissionMode" value="bypassPermissions" />
<option name="reasoningEffort" value="high" />
</TabSessionState>
</value>
</entry>
<entry key="2">
<value>
<TabSessionState>
<option name="provider" value="claude" />
<option name="sessionId" value="9db55ee0-b91b-4abe-9d2f-5655a168223d" />
<option name="cwd" value="$PROJECT_DIR$" />
<option name="model" value="claude-opus-4-7[1m]" />
<option name="permissionMode" value="bypassPermissions" />
<option name="reasoningEffort" value="high" />
</TabSessionState>
</value>
</entry>
</map>
</option>
<option name="tabCount" value="3" />
</component>
</project>
+14 -8
View File
@@ -1,4 +1,4 @@
<!-- Last updated: 2026-08-21 | Commit: b62d183 -->
<!-- Last updated: 2026-09-10 | Commit: b5a8b83 -->
# 项目约束
本项目所有工作必须严格遵守 `项目背景.md` 中定义的全部规范,包括:
@@ -54,6 +54,9 @@
- `project_overview.md` — 项目技术栈与三端架构,避免对项目结构产生误解
3. **按需加载**(与当前任务相关时加载):
- `user_profile.md` — 用户角色与偏好(需要个性化回复时)
- `feedback_medical_pad_migration.md` — 医疗点PAD端原型规范:无前缀英文名、无模块导航页、简单弹窗页内实现、1280×800 外壳(涉及 medical-pad 页面时)
- `feedback_shell_sidebar_scope.md` — Web 管理端壳层规范:顶栏=一级分组、侧栏仅当前分组二级菜单、页头不放面包屑(涉及管理端新模块壳层时)
- `project_database_postgresql.md` — 项目数据库统一为 PostgreSQL(生成数据库设计说明书/解析 SQL 时)
- `lint_report.md` — 记忆健康检查报告(发现记忆体系存在待处理问题时)
### 记忆体系的更新与同步
@@ -64,13 +67,16 @@
```
.claude/memory/
├── MEMORY.md # 索引(入口)
├── decisions.md # 关键决策
├── feedback.md # 协作规范
├── project_progress.md # 阶段进度
├── project_overview.md # 技术栈与架构
├── user_profile.md # 用户画像
── lint_report.md # 记忆健康检查报告(按需)
├── MEMORY.md # 索引(入口)
├── decisions.md # 关键决策
├── feedback.md # 协作规范
├── feedback_medical_pad_migration.md # 医疗点PAD端原型规范(按需)
├── feedback_shell_sidebar_scope.md # Web管理端壳层导航规范(按需)
├── project_progress.md # 阶段进度
── project_overview.md # 技术栈与架构
├── project_database_postgresql.md # 数据库选型(按需)
├── user_profile.md # 用户画像
└── lint_report.md # 记忆健康检查报告(按需)
```
> 记忆文件是跨会话持久化的知识,优先级高于 Agent 的默认推断。若记忆内容与当前代码状态冲突,以代码为准并更新记忆。
+205
View File
@@ -0,0 +1,205 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<!--
业务架构图模板(doc-detailed-design skill 使用)
说明:
- 四层自上而下:用户交互层 / 应用与服务层 / 数据整合层 / 数据服务层,层间用横向分隔线(.divider)分隔
- 应用与服务层:按「一级菜单」分组为横排竖版板块(.domain),板块内功能项竖版排列
· .d-item = Web 管理端功能(蓝底标签)
· .d-item--app = 员工端 APP 功能(绿底标签,建议 📱 前缀)
· .domain-model = 智能模型类板块(虚线特殊样式)
· .legend = 底部双端图例
- 板块数量按子系统一级菜单数调整;菜单过多时可精简功能项或分两行
- 标题/副标题/各层内容由 skill 按子系统替换
-->
<style>
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:"Microsoft YaHei","PingFang SC",sans-serif;background:#fff;padding:28px 24px;width:960px;}
.wrap{background:linear-gradient(180deg,#f7fbff,#eef4fb);border:1px solid #e3edf7;border-radius:16px;padding:26px 24px;}
.head{text-align:center;margin-bottom:24px;}
.title{font-size:20px;font-weight:800;color:#17355a;letter-spacing:1px;}
.subtitle{font-size:12px;color:#7b8ba0;margin-top:6px;letter-spacing:2px;}
.layer{display:flex;border-radius:14px;overflow:hidden;box-shadow:0 4px 16px rgba(24,80,140,.08);margin-bottom:0;}
.side{width:34px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff;writing-mode:vertical-rl;letter-spacing:4px;}
.body{flex:1;padding:16px 20px;}
.lname{font-size:14px;font-weight:800;display:flex;align-items:center;gap:8px;margin-bottom:12px;}
.ltag{font-size:10px;font-weight:600;padding:2px 8px;border-radius:8px;background:rgba(255,255,255,.65);}
.items{display:flex;flex-wrap:wrap;gap:8px;}
.item{font-size:12px;padding:7px 13px;border-radius:8px;background:#fff;border:1px solid rgba(0,0,0,.06);box-shadow:0 2px 6px rgba(0,0,0,.04);color:#3a4c66;}
/* 层间横向分隔线 */
.divider{display:flex;align-items:center;height:22px;padding:0 4px;}
.divider-line{flex:1;height:1px;background:linear-gradient(90deg,transparent,#bcd2ec 18%,#bcd2ec 82%,transparent);}
/* 用户交互层 */
.l1{background:linear-gradient(135deg,#e8f4ff,#d8ecff);}
.l1 .side{background:linear-gradient(180deg,#4da3ff,#1d7be0);}
.l1 .lname{color:#0a5cb8;}
.l1 .item{border-color:#c3e2ff;}
/* 应用与服务层(竖版板块横排一行) */
.l2{background:linear-gradient(135deg,#eef3ff,#e4ecff);}
.l2 .side{background:linear-gradient(180deg,#7a9bff,#4a6fe0);}
.l2 .lname{color:#3d57c9;}
.domains{display:flex;gap:8px;align-items:stretch;}
.domain{flex:1;min-width:0;background:#fff;border:1px solid #d4dfff;border-radius:10px;padding:10px 6px;box-shadow:0 2px 6px rgba(0,0,0,.04);display:flex;flex-direction:column;}
.domain-head{font-size:12px;font-weight:800;color:#3d57c9;margin-bottom:8px;text-align:center;display:flex;align-items:center;justify-content:center;gap:4px;line-height:1.2;}
.domain-head::before{content:'';width:4px;height:13px;background:linear-gradient(180deg,#7a9bff,#4a6fe0);border-radius:2px;flex-shrink:0;}
.domain-items{display:flex;flex-direction:column;gap:5px;}
.d-item{font-size:10.5px;padding:5px 4px;border-radius:6px;background:#f3f6ff;border:1px solid #d4dfff;color:#3a4c66;text-align:center;white-space:nowrap;line-height:1.3;}
.d-item--app{background:#e9f9f0;border:1px solid #b7ebc8;color:#1d7a45;}
.domain-model{background:linear-gradient(135deg,#e6f7ff,#d9f0ff);border:1px dashed #69c0ff;}
.domain-model .domain-head{color:#0a5cb8;}
.domain-model .domain-head::before{background:linear-gradient(180deg,#69c0ff,#1890ff);}
.domain-model .d-item{background:#fff;border-color:#91d5ff;color:#0a5cb8;font-weight:600;}
.legend{display:flex;gap:14px;margin-top:10px;font-size:10.5px;color:#7b8ba0;align-items:center;}
.legend .dot{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:4px;vertical-align:-1px;}
.legend .dot--mgmt{background:#f3f6ff;border:1px solid #d4dfff;}
.legend .dot--app{background:#e9f9f0;border:1px solid #b7ebc8;}
/* 数据整合层 */
.l3{background:linear-gradient(135deg,#e9fbf8,#ddf6f1);}
.l3 .side{background:linear-gradient(180deg,#5ad2c4,#2ba8a0);}
.l3 .lname{color:#08979c;}
.l3 .item{border-color:#c0f0e8;}
/* 数据服务层 */
.l4{background:linear-gradient(135deg,#eef4ff,#e4edff);}
.l4 .side{background:linear-gradient(180deg,#6b8cff,#3f5bd8);}
.l4 .lname{color:#2f4abf;}
.l4 .item{background:#f3f6ff;border-color:#d3dcf7;}
</style>
</head>
<body>
<div class="wrap">
<div class="head">
<div class="title">{子系统名}业务架构图</div>
<div class="subtitle">{核心理念副标题,如:数据驱动 · 智能监测 · 闭环管理}</div>
</div>
<!-- 用户交互层:按端替换(员工端 + 管理端) -->
<div class="layer l1">
<div class="side">用户交互层</div>
<div class="body">
<div class="lname">用户交互层(应用层)<span class="ltag">价值呈现端</span></div>
<div class="items">
<div class="item">👤 企业员工 · 员工端 APP</div>
<div class="item">🖥️ 管理专员 · 管理后台(Web 管理端)</div>
</div>
</div>
</div>
<div class="divider"><div class="divider-line"></div></div>
<!-- 应用与服务层:按一级菜单分组为横排竖版板块;d-item=管理端,d-item--app=员工端 -->
<div class="layer l2">
<div class="side">应用与服务层</div>
<div class="body">
<div class="lname">应用与服务层<span class="ltag">业务中枢</span></div>
<div class="domains">
<div class="domain">
<div class="domain-head">健康档案</div>
<div class="domain-items">
<span class="d-item">个人监测数据</span>
<span class="d-item">单位报表数据</span>
<span class="d-item">算法模型</span>
</div>
</div>
<div class="domain">
<div class="domain-head">健康现状</div>
<div class="domain-items">
<span class="d-item">员工评估信息</span>
<span class="d-item">单位评估统计</span>
<span class="d-item d-item--app">📱 现状仪表盘</span>
</div>
</div>
<div class="domain">
<div class="domain-head">健康排查</div>
<div class="domain-items">
<span class="d-item">问卷管理</span>
<span class="d-item">活动管理</span>
<span class="d-item d-item--app">📱 活动参与</span>
<span class="d-item d-item--app">📱 问卷填写</span>
</div>
</div>
<div class="domain">
<div class="domain-head">穿戴监测</div>
<div class="domain-items">
<span class="d-item">设备管理</span>
<span class="d-item">用户数据</span>
<span class="d-item">告警记录</span>
<span class="d-item">数据统计</span>
<span class="d-item">运维中心</span>
<span class="d-item d-item--app">📱 指标详情</span>
<span class="d-item d-item--app">📱 预警历史</span>
</div>
</div>
<div class="domain">
<div class="domain-head">健康环境</div>
<div class="domain-items">
<span class="d-item">设备管理</span>
<span class="d-item">数据监测</span>
<span class="d-item">异常报警</span>
<span class="d-item">数据分析</span>
<span class="d-item d-item--app">📱 地图查看</span>
<span class="d-item d-item--app">📱 设备详情</span>
<span class="d-item d-item--app">📱 报警数据</span>
</div>
</div>
<div class="domain">
<div class="domain-head">异常事件</div>
<div class="domain-items">
<span class="d-item">系统脏数据</span>
<span class="d-item">结果异常</span>
</div>
</div>
<div class="domain domain-model">
<div class="domain-head">智能模型</div>
<div class="domain-items">
<span class="d-item">🧠 健康评估模型</span>
<span class="d-item">🚨 告警预警模型</span>
</div>
</div>
</div>
<div class="legend">
<span><span class="dot dot--mgmt"></span>Web 管理端功能</span>
<span><span class="dot dot--app"></span>员工端 APP 功能</span>
</div>
</div>
</div>
<div class="divider"><div class="divider-line"></div></div>
<!-- 数据整合层:通用,一般无需替换 -->
<div class="layer l3">
<div class="side">数据整合层</div>
<div class="body">
<div class="lname">数据整合层(接入层)<span class="ltag">数据中枢</span></div>
<div class="items">
<div class="item">数据清洗</div>
<div class="item">数据转换</div>
<div class="item">数据加密</div>
<div class="item">数据融合</div>
</div>
</div>
</div>
<div class="divider"><div class="divider-line"></div></div>
<!-- 数据服务层:按子系统对接的硬件/设备替换 -->
<div class="layer l4">
<div class="side">数据服务层</div>
<div class="body">
<div class="lname">数据服务层<span class="ltag">数据感知网络</span></div>
<div class="items">
<div class="item">⌚ 智能穿戴设备(手表)</div>
<div class="item">🌡️ 室内空气监测设备</div>
<div class="item">🌤️ 室外空气监测设备</div>
<div class="item">💧 水质监测设备</div>
<div class="item">🌸 花粉监测设备</div>
</div>
</div>
</div>
</div>
</body>
</html>
+164
View File
@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:"Microsoft YaHei","PingFang SC",sans-serif;background:#fff;padding:28px 24px;width:960px;}
.wrap{background:linear-gradient(180deg,#f7fbff,#eef4fb);border:1px solid #e3edf7;border-radius:16px;padding:26px 24px;}
.head{text-align:center;margin-bottom:22px;}
.title{font-size:20px;font-weight:800;color:#17355a;letter-spacing:1px;}
.layer{display:flex;border-radius:12px;overflow:hidden;box-shadow:0 4px 14px rgba(24,80,140,.08);}
.side{width:32px;display:flex;align-items:center;justify-content:center;font-size:13px;color:#fff;writing-mode:vertical-rl;letter-spacing:3px;font-weight:700;}
.body{flex:1;padding:13px 18px;}
.lname{font-size:13px;font-weight:800;margin-bottom:10px;display:flex;align-items:center;gap:6px;}
.items{display:flex;flex-wrap:wrap;gap:7px;align-items:center;}
.box{font-size:11.5px;padding:6px 12px;border-radius:7px;background:#fff;border:1px solid rgba(0,0,0,.06);box-shadow:0 1px 4px rgba(0,0,0,.04);color:#3a4c66;}
.sep{width:1px;height:20px;background:#d0d9e6;margin:0 4px;}
.arrow{display:flex;justify-content:center;height:24px;}
.arrow-line{width:2px;height:100%;background:linear-gradient(180deg,#4da3ff,#2ba8a0);}
/* 前端层 */
.t1{background:linear-gradient(135deg,#e8f4ff,#dcecff);}
.t1 .side{background:linear-gradient(180deg,#4da3ff,#1d7be0);}
.t1 .lname{color:#0a5cb8;}
.t1 .box{border-color:#c3e2ff;}
/* 接入层 */
.t2{background:#f4f6fa;}
.t2 .side{background:linear-gradient(180deg,#9aa8bc,#6b7b90);}
.t2 .lname{color:#5b6b7b;}
.t2 .box{background:#fff7e6;border-color:#ffd591;}
/* 服务层 */
.t3{background:linear-gradient(135deg,#eef3ff,#e2eaff);}
.t3 .side{background:linear-gradient(180deg,#7a9bff,#4a6fe0);}
.t3 .lname{color:#3d57c9;}
.svc{background:linear-gradient(135deg,#e6f7ff,#d9f0ff);border:1px solid #91d5ff;color:#0a5cb8;font-weight:600;}
.mid{background:#fff7e6;border:1px dashed #ffc069;color:#c56a0a;}
/* 中间件层 */
.t4{background:linear-gradient(135deg,#e9fbf8,#ddf6f1);}
.t4 .side{background:linear-gradient(180deg,#5ad2c4,#2ba8a0);}
.t4 .lname{color:#08979c;}
.t4 .box{border-color:#c0f0e8;}
/* 数据层 */
.t5{background:linear-gradient(135deg,#eef4ff,#e2ebff);}
.t5 .side{background:linear-gradient(180deg,#6b8cff,#3f5bd8);}
.t5 .lname{color:#2f4abf;}
.t5 .box{background:#f3f6ff;border-color:#d3dcf7;font-weight:600;color:#2f4abf;}
/* 监控运维层 */
.t6{background:linear-gradient(135deg,#f5f0ff,#ece4ff);}
.t6 .side{background:linear-gradient(180deg,#a98bff,#7b5ce0);}
.t6 .lname{color:#6b3fd1;}
.t6 .box{background:#faf7ff;border-color:#e0d3ff;}
</style>
</head>
<body>
<div class="wrap">
<div class="head"><div class="title">体重管理系统技术架构图</div></div>
<!-- 前端层 -->
<div class="layer t1">
<div class="side">前端层</div>
<div class="body">
<div class="lname">前端层 · Qiankun 微前端(MonorepoTurbo + npm workspaces</div>
<div class="items">
<div class="box">主应用 main(基座)</div>
<div class="box">体重管理子应用</div>
<div class="box">营养管理子应用</div>
<div class="box">健康监测子应用</div>
<div class="box">专家咨询子应用</div>
<div class="sep"></div>
<div class="box">Vue 3.5</div>
<div class="box">Ant Design Vue 4</div>
<div class="box">Vite 8</div>
<div class="box">Pinia</div>
</div>
</div>
</div>
<div class="arrow"><div class="arrow-line"></div></div>
<!-- 接入层 -->
<div class="layer t2">
<div class="side">接入层</div>
<div class="body">
<div class="lname">接入层 · 统一入口</div>
<div class="items">
<div class="box">Nginx 反向代理 · 负载均衡</div>
<div class="box">Spring Cloud Gateway · 统一鉴权 / 限流 / 路由</div>
<div class="box">Sa-Token 认证授权</div>
</div>
</div>
</div>
<div class="arrow"><div class="arrow-line"></div></div>
<!-- 服务层 -->
<div class="layer t3">
<div class="side">服务层</div>
<div class="body">
<div class="lname">服务层 · Spring Cloud Alibaba 微服务(Java 21 + Spring Boot 3.3</div>
<div class="items">
<div class="box svc">体重管理服务</div>
<div class="box svc">营养管理服务</div>
<div class="box svc">健康监测服务</div>
<div class="box svc">健康评估服务</div>
<div class="box svc">专家咨询服务</div>
<div class="box svc">应急就医服务</div>
<div class="box svc">系统管理服务</div>
<div class="sep"></div>
<div class="box mid">Nacos 注册 / 配置</div>
<div class="box mid">Feign 服务调用 + 降级</div>
<div class="box mid">Sentinel 熔断限流</div>
<div class="box mid">MyBatis-Plus ORM</div>
</div>
</div>
</div>
<div class="arrow"><div class="arrow-line"></div></div>
<!-- 中间件层 -->
<div class="layer t4">
<div class="side">中间件层</div>
<div class="body">
<div class="lname">中间件层 · 公共服务</div>
<div class="items">
<div class="box">Redis 缓存 / 分布式锁 / 幂等</div>
<div class="box">RocketMQ 消息队列</div>
<div class="box">XXL-Job 定时任务</div>
<div class="box">MinIO 文件存储</div>
</div>
</div>
</div>
<div class="arrow"><div class="arrow-line"></div></div>
<!-- 数据层 -->
<div class="layer t5">
<div class="side">数据层</div>
<div class="body">
<div class="lname">数据层 · 持久化存储</div>
<div class="items">
<div class="box">PostgreSQL 15 数据库</div>
</div>
</div>
</div>
<div class="arrow"><div class="arrow-line"></div></div>
<!-- 监控运维层 -->
<div class="layer t6">
<div class="side">运维监控</div>
<div class="body">
<div class="lname">运维监控层 · 可观测性</div>
<div class="items">
<div class="box">Prometheus + Grafana 指标监控</div>
<div class="box">SkyWalking 链路追踪</div>
<div class="box">ELK 日志搜集分析</div>
</div>
</div>
</div>
</div>
</body>
</html>
+232
View File
@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{子系统名} · 产品宣传册</title>
<!--
产品宣传册模板(横版)
用途:doc-brochure skill 的生成骨架
说明:{占位符} 由 skill 填充;横版 A4(297×210mm)分页,便于打印/转 PDF
转 PDF@page 已设 A4 landscape,浏览器打印(Ctrl+P)或 headless 截图即可
约束:宣传册必须通用,不得出现客户名称与项目名称(客户公司名、甲方名称、委托方、油田等一律不写)
分页:每 .page 一页(297×210mm 横版 A4),内容需在该页内铺满且不溢出(overflow:hidden 兜底),
避免同一版块的文字与截图被切到下一页
-->
<style>
:root{
--primary:#1890FF;
--primary-dark:#0a5cb8;
--primary-light:#E6F7FF;
--accent:#13C2C2;
--success:#52C41A;
--warning:#FAAD14;
--purple:#722ED1;
--ink:#1f2d3d;
--ink-2:#5b6b7b;
--line:#e8e8e8;
--bg:#f5f7fa;
}
*{margin:0;padding:0;box-sizing:border-box;}
@page{ size:A4 landscape; margin:0; }
body{
font-family:"Microsoft YaHei","PingFang SC",-apple-system,"Segoe UI",sans-serif;
color:var(--ink);background:#eef1f5;
-webkit-print-color-adjust:exact;print-color-adjust:exact;
}
.page{ width:297mm;height:210mm;margin:0 auto 24px;background:#fff;page-break-after:always;overflow:hidden;position:relative; }
.page:last-child{ page-break-after:auto;margin-bottom:0; }
.inner{ padding:14mm 16mm;height:100%;display:flex;flex-direction:column;justify-content:center; }
/* 通用 */
.kicker{ font-size:12px;letter-spacing:3px;color:var(--primary);font-weight:800; }
h2.sec{ font-size:24px;font-weight:800;margin-top:6px; }
.head{ border-bottom:1px solid #eef2f6; padding-bottom:12px; margin-bottom:18px; }
.secbar{ width:56px;height:4px;background:linear-gradient(90deg,var(--primary),var(--accent));border-radius:2px;margin:10px 0 0; }
p.lead{ font-size:13px;line-height:1.8;color:var(--ink-2); }
/* 图标底 */
.ic{ width:46px;height:46px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0; }
.ic--blue{ background:linear-gradient(135deg,#e6f7ff,#d4eeff); }
.ic--cyan{ background:linear-gradient(135deg,#e6fffb,#d2f5f0); }
.ic--orange{ background:linear-gradient(135deg,#fff7e6,#ffedd4); }
.ic--purple{ background:linear-gradient(135deg,#f9f0ff,#efe0ff); }
.ic--green{ background:linear-gradient(135deg,#f0fff4,#dff5e5); }
/* ===== 封面 ===== */
.cover{ background:linear-gradient(135deg,#0a2540 0%,#0a5cb8 55%,#1890FF 100%);color:#fff; }
.cover .inner{ justify-content:space-between;position:relative; }
.cover__top{ display:flex;justify-content:space-between;align-items:center;font-size:14px;color:rgba(255,255,255,.96); }
.cover__logo{ font-weight:800;font-size:16px;letter-spacing:1px;display:flex;align-items:center;gap:8px; }
.cover__logo .dot{ width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#40c9ff,#1890FF); }
.cover__mid{ display:flex;align-items:center;justify-content:space-between;gap:40px; }
.cover__title{ font-size:44px;font-weight:900;line-height:1.2;margin-bottom:14px;letter-spacing:2px; }
.cover__slogan{ font-size:16px;color:rgba(255,255,255,.96);line-height:1.9; }
.cover__tags{ margin-top:22px;display:flex;gap:10px;flex-wrap:wrap; }
.cover__tag{ padding:7px 16px;border:1px solid rgba(255,255,255,.5);border-radius:20px;font-size:13px;background:rgba(255,255,255,.08); }
.cover__deco{ width:280px;height:280px;position:relative;flex-shrink:0; }
.cover__deco .ring{ position:absolute;border-radius:50%; }
.r1{ width:280px;height:280px;border:2px solid rgba(255,255,255,.18); }
.r2{ width:210px;height:210px;border:2px dashed rgba(255,255,255,.22);left:35px;top:35px; }
.r3{ width:140px;height:140px;background:radial-gradient(circle,rgba(255,255,255,.28),transparent 70%);left:70px;top:70px; }
.cover__bottom{ display:flex;justify-content:space-between;font-size:14px;color:rgba(255,255,255,.96); }
/* ===== 概述 ===== */
.stat-row{ display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0; }
.stat{ background:linear-gradient(135deg,#f7fbff,#eef6ff);border:1px solid #e3eefb;border-radius:14px;padding:18px 14px;display:flex;align-items:center;gap:12px; }
.stat b{ font-size:28px;color:var(--primary);font-weight:800;line-height:1; }
.stat span{ font-size:12px;color:var(--ink-2);line-height:1.4; }
/* ===== 功能 ===== */
.feat-grid{ display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:14px; }
.feat{ border:1px solid var(--line);border-radius:14px;padding:18px;background:linear-gradient(180deg,#fff,#fafcff);position:relative;overflow:hidden; }
.feat::before{ content:"";position:absolute;top:0;left:0;width:100%;height:3px;background:linear-gradient(90deg,var(--primary),var(--accent)); }
.feat__head{ display:flex;align-items:center;gap:12px;margin-bottom:10px; }
.feat h3{ font-size:16px;font-weight:800; }
.feat p{ font-size:12px;line-height:1.7;color:var(--ink-2); }
.feat__points{ margin-top:8px;list-style:none; }
.feat__points li{ font-size:11.5px;color:var(--ink-2);line-height:1.7;padding-left:14px;position:relative; }
.feat__points li::before{ content:"·";position:absolute;left:4px;color:var(--primary);font-weight:900; }
/* ===== 优势 ===== */
.adv-row{ display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:14px; }
.adv{ display:flex;gap:14px;padding:16px;border-radius:14px;background:linear-gradient(135deg,#f7fbff,#eef6ff);border:1px solid #e3eefb;align-items:flex-start; }
.adv__num{ font-size:26px;font-weight:900;color:var(--primary);line-height:1;white-space:nowrap; }
.adv h4{ font-size:14px;margin-bottom:6px; }
.adv p{ font-size:11.5px;color:var(--ink-2);line-height:1.6; }
/* ===== 终端 ===== */
.term-row{ display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:14px; }
.term{ display:flex;gap:14px;align-items:center;padding:18px;border:1px solid var(--line);border-radius:14px;background:#fff; }
.term b{ font-size:14px;display:block;margin-bottom:4px; }
.term span{ font-size:11.5px;color:var(--ink-2);line-height:1.6; }
/* ===== 封底 ===== */
.shots{ display:flex;flex-direction:column;gap:20px;flex:1;justify-content:center; }
.shots__row{ display:flex;gap:26px;justify-content:center;align-items:flex-end; }
.shot{ display:flex;flex-direction:column;align-items:center;gap:8px; }
.shot__frame{ overflow:hidden;box-shadow:0 6px 18px rgba(15,60,120,.14);background:#f5f7fa; }
.shot__frame--phone{ border:4px solid #2d3748;border-radius:16px; }
.shot__frame--phone img{ width:130px;height:282px;object-fit:cover;object-position:top;display:block; }
.shot__frame--web{ border:1px solid #d9e1ec;border-radius:10px; }
.shot__frame--web img{ width:290px;height:181px;object-fit:cover;object-position:top;display:block; }
.shot__label{ font-size:11px;color:var(--ink-2); }
.back{ background:linear-gradient(135deg,#0a2540,#0a5cb8);color:#fff; }
.back .inner{ align-items:center;justify-content:center;text-align:center; }
.back__title{ font-size:32px;font-weight:900;margin-bottom:12px; }
.back__line{ font-size:15px;color:rgba(255,255,255,.96);line-height:2; }
.back__foot{ margin-top:30px;font-size:14px;color:rgba(255,255,255,.92); }
</style>
</head>
<body>
<!-- 1. 封面 -->
<section class="page cover">
<div class="inner">
<div class="cover__top">
<span class="cover__logo"><span class="dot"></span>{平台品牌名}</span>
<span>产品宣传册</span>
</div>
<div class="cover__mid">
<div>
<div class="cover__title">{子系统名}</div>
<p class="cover__slogan">{一句话 Slogan / 价值主张}</p>
<div class="cover__tags">
<span class="cover__tag">{关键词1}</span>
<span class="cover__tag">{关键词2}</span>
<span class="cover__tag">{关键词3}</span>
</div>
</div>
<div class="cover__deco"><div class="ring r1"></div><div class="ring r2"></div><div class="ring r3"></div></div>
</div>
<div class="cover__bottom"><span>{解决方案定位}</span><span>{编制日期}</span></div>
</div>
</section>
<!-- 2. 产品概述 -->
<section class="page">
<div class="inner">
<div class="head"><span class="kicker">产品概述</span><h2 class="sec">{子系统定位一句话}</h2><div class="secbar"></div></div>
<p class="lead">{产品价值说明,2~3 句}</p>
<div class="stat-row">
<div class="stat"><div class="ic ic--blue">📱</div><div><b>{N}</b><span>{指标1}</span></div></div>
<div class="stat"><div class="ic ic--cyan">🧩</div><div><b>{N}</b><span>{指标2}</span></div></div>
<div class="stat"><div class="ic ic--orange">📄</div><div><b>{N}</b><span>{指标3}</span></div></div>
<div class="stat"><div class="ic ic--purple">👥</div><div><b>{N}</b><span>{指标4}</span></div></div>
</div>
<p class="lead">{补充说明}</p>
</div>
</section>
<!-- 3. 核心功能(每页 6 个) -->
<section class="page">
<div class="inner">
<div class="head"><span class="kicker">核心功能</span><h2 class="sec">{功能板块标题}</h2><div class="secbar"></div></div>
<div class="feat-grid">
<div class="feat">
<div class="feat__head"><div class="ic ic--blue">{emoji}</div><h3>{功能名}</h3></div>
<p>{功能简介}</p>
<ul class="feat__points"><li>{亮点1}</li><li>{亮点2}</li></ul>
</div>
<!-- 复制 .feat 卡片,每页 6 个 -->
</div>
</div>
</section>
<!-- 4. 特色优势 -->
<section class="page">
<div class="inner">
<div class="head"><span class="kicker">特色优势</span><h2 class="sec">为什么选择我们</h2><div class="secbar"></div></div>
<div class="adv-row">
<div class="adv"><div class="adv__num">01</div><div><h4>{优势标题}</h4><p>{优势说明}</p></div></div>
<!-- 复制 .adv,每页 6 个 -->
</div>
</div>
</section>
<!-- 5. 终端覆盖 -->
<section class="page">
<div class="inner">
<div class="head"><span class="kicker">终端覆盖</span><h2 class="sec">多端协同,全场景覆盖</h2><div class="secbar"></div></div>
<div class="term-row">
<div class="term"><div class="ic ic--blue">{emoji}</div><div><b>{终端名}</b><span>{终端说明}</span></div></div>
<!-- 复制 .term -->
</div>
</div>
</section>
<!-- 6. 产品界面展示 -->
<section class="page">
<div class="inner">
<div class="head"><span class="kicker">界面展示</span><h2 class="sec">产品界面预览</h2><div class="secbar"></div></div>
<div class="shots">
<div class="shots__row">
<div class="shot">
<div class="shot__frame shot__frame--phone"><img src="{截图相对路径}" alt="{页面名}"></div>
<span class="shot__label">{页面名}</span>
</div>
<!-- 复制 .shot,第一行放 5 张 APP 竖版截图(phone -->
</div>
<div class="shots__row">
<div class="shot">
<div class="shot__frame shot__frame--web"><img src="{截图相对路径}" alt="{页面名}"></div>
<span class="shot__label">{页面名}</span>
</div>
<!-- 复制 .shot,第二行放 3 张 Web 端截图(web) -->
</div>
</div>
</div>
</section>
<!-- 7. 封底 -->
<section class="page back">
<div class="inner">
<div class="back__title">{子系统名}</div>
<div class="back__line">{感谢语 / 联系方式 / 口号}</div>
<div class="back__foot">{编制日期} · 版权所有</div>
</div>
</section>
</body>
</html>
@@ -0,0 +1,57 @@
<!--
公共章节模板
用途:各子系统详细设计说明书 / 操作手册中「公共部分」章节的标准素材
说明:甲方要求公共部分在每个子系统中重复体现;skill 生成文档时,将本文件对应章节
复制/改写后并入目标文档,不另起新文件
-->
# 公共部分
<!-- 以下为跨子系统共用的功能章节,按需并入各子系统的详细设计 / 操作手册 -->
## 一、登录与认证
### 功能说明
平台提供统一的账号密码登录入口,各终端(Web 管理端 / 员工端 APP / 健康应急 APP / 医疗点 PAD / 小程序)共用统一身份认证体系。
### 业务规则
- 用户账号由平台管理员统一创建并分配角色权限
- 登录成功后按角色加载对应菜单与功能权限
- 连续多次密码错误触发账号临时锁定
### 交互说明
1. 打开终端进入登录页
2. 输入账号、密码
3. 点击「登录」进入工作台/首页
## 二、消息与即时通讯
### 功能说明
提供站内消息通知与即时通讯能力,支持系统通知、业务提醒、点对点会话。
### 业务规则
- 系统通知按业务事件自动推送
- 未读消息以角标形式在入口处提示
## 三、AI 助手
### 功能说明
提供智能问答与健康建议能力,支持文本输入、常见问题推送、数据统计。
## 四、个人中心
### 功能说明
提供当前登录用户的基本信息查看与维护、账号设置等能力。
## 五、组织架构与权限
### 功能说明
平台统一维护单位/部门/人员组织架构,各子系统共享组织架构数据,按角色控制功能访问权限。
+109
View File
@@ -0,0 +1,109 @@
<!--
数据库设计说明书模板
用途:doc-database-design skill 的生成骨架,表结构格式沿用「doc/体重管理项目系统设计说明.docx」5.3 节
填写约定:{占位符} 由 skill 依据研发提供的 SQL 文件解析填充
通用约束:全文不得出现客户名称与项目名称,只介绍数据库自身设计,产出文档可供任意客户与人群阅读。
PDF 导出规范(竖版 A4):
- 版式:竖版 A4(210×297mm),最终交付为 PDF;本 .md 为源,导出时按下方分页原则处理
- 分页三原则:① 标题不孤行(标题与其后内容同页)② 图文不割裂 ③ 表行不切割
(字段表单行、数据字典表单行不被水平切成两页)
- 可选 CSS(浏览器 / Puppeteer 系导出工具):
@page { size: A4 portrait; margin: 18mm 16mm; }
h1,h2,h3,h4 { page-break-after: avoid; }
img, pre, blockquote { page-break-inside: avoid; }
tr { page-break-inside: avoid; }
- 无法避免的跨页(超大字段表等)由使用方在导出时自行处理
-->
# {子系统名}数据库设计说明书
| 项 | 内容 |
|----|------|
| 文档类型 | 数据库设计说明书 |
| 数据库 | PostgreSQL |
| Schema | {schema 名,如 platform-weight} |
| 字符集 | UTF-8 |
| 版本号 | V1.0 |
| 编制日期 | {YYYY-MM-DD} |
---
## 目录
<!-- 转 PDF 工具按标题层级自动生成 -->
## 1. 概述
### 1.1 数据库环境
{数据库类型 / 版本 / Schema / 字符集}
### 1.2 命名规范
{按 PostgreSQL 整理,如:}
- 采用 UTF-8 字符集
- 表名统一按业务域前缀命名(如 `wm_` 表示体重管理域),采用小写下划线命名
- 字段名采用小写下划线命名(如 `org_code`
- 每张表均设置主键 `id`(雪花 ID,不表示业务含义),严禁大表使用 UUID/MD5 作主键
- 必须为表、字段添加注释
- 不使用数据库外键,表间关联由程序保证
- 数据量可能很大的表(超 2000 万)采用分库/分表/分区表
### 1.3 设计原则
{沿用 docx 5.1 节:}
- **一致性原则**:统一、系统地分析与设计数据来源,保证数据一致性与有效性
- **数据独立性原则**:数据空间独立、数据结构独立
- **完整性原则**:对输入数据有审核和约束机制
- **安全性原则**:认证与授权机制、数据隔离、实时备份、数据加密
- **可伸缩性与可扩展性原则**:充分考虑发展与移植需要
- **规范化**:遵循规范化理论,减少插入/删除/修改异常
## 2. 数据表清单
| 序号 | 表中文名 | 英文表名 | 说明 |
|------|---------|---------|------|
| 1 | {表中文名} | {table_name} | {一句话说明} |
<!-- 按业务模块分组,如:活动管理 / 报名管理 / 设备管理 ... -->
## 3. 数据表结构说明
<!-- 每张表一个小节,格式如下 -->
### 3.1 {表中文名}({英文表名})
{表用途一句话}
| 序号 | 列名 | 数据类型 | 长度 | 主键 | 允许空值 | 列说明 |
|------|------|---------|------|------|---------|--------|
| 1 | id | bigint(32) | | 是 | NO | ID |
| 2 | {col_name} | {varchar(255)} | {255} | 否 | {YES/NO} | {说明} |
## 4. 表关系与 ER 图
```mermaid
erDiagram
{表A} ||--o{ {表B} : {关系说明}
{表A} {
bigint id PK
varchar col_name "说明"
}
```
## 5. 索引与约束设计
{主键索引 / 唯一索引 / 普通索引 / 外键约束(由程序保证)的说明}
| 表 | 索引名 | 字段 | 类型 | 说明 |
|----|--------|------|------|------|
## 6. 数据字典
{枚举类字段的取值字典,按字段分组}
| 字段 | 取值 | 含义 |
|------|------|------|
| {state} | 0/1/2/3/4 | {各取值含义} |
+245
View File
@@ -0,0 +1,245 @@
<!--
系统详细设计说明书模板
用途:doc-detailed-design skill 的生成骨架,参考「doc/体重管理项目系统设计说明.docx」8 章框架优化
填写约定:{占位符} 由 skill 依据「子系统页面清单 + 项目背景.md + 各终端 PRD.md」自动填充
架构图:业务架构图/技术架构图用 H5 布局生成后截图,以 PNG 引用(见 doc-detailed-design skill
HTML 注释(<!-- -->)仅作填写指引,转 PDF 时不会显示
通用约束:全文不得出现客户名称与项目名称(客户公司名、甲方名称、委托方等一律不写),
只介绍系统自身概况,产出文档可供任意客户与人群阅读。
PDF 导出规范(竖版 A4):
- 版式:竖版 A4(210×297mm),最终交付为 PDF;本 .md 为源,导出时按下方分页原则处理
- 分页三原则:① 标题不孤行(标题与其后内容同页)② 图文不割裂(图片与其说明/标题同页)
③ 表行不切割(表格单行不被水平切成两页)
- 可选 CSS(浏览器 / Puppeteer 系导出工具):
@page { size: A4 portrait; margin: 18mm 16mm; }
h1,h2,h3,h4 { page-break-after: avoid; }
img, pre, blockquote { page-break-inside: avoid; }
tr { page-break-inside: avoid; }
- 无法避免的跨页(超大表格 / 超长截图)由使用方在导出时自行处理
-->
# {子系统名}详细设计说明书
<!-- 文档标题与文件名均含子系统名 -->
| 项 | 内容 |
|----|------|
| 系统名称 | {子系统名} |
| 文档类型 | 系统详细设计说明书 |
| 版本号 | V1.0 |
| 编制日期 | {YYYY-MM-DD} |
---
## 目录
<!-- 转 PDF 工具按标题层级自动生成目录 -->
## 一、项目背景
{一段话:先交代企业健康管理平台整体背景,再落到本子系统定位}
- 素材来源:`项目背景.md` 6.1 节 + 本子系统各终端 PRD 的「模块概述」段
## 二、建设目标
{分条列建设目标,每条「动词 + 宾语 + 效果」结构,参考 docx「建设目标」章节}
- 示例句式:构建……生态体系 / 打造……核心引擎 / 实现……追踪与激励 / 建立……评估与决策支撑
## 三、需求说明与功能设计
### 3.1 需求概述
{按端分列,说明本子系统涉及哪些终端、每端实现哪些需求点}
### 3.2 功能清单
{以列表/表格形式罗列各端菜单名称,后台 + 移动端分开罗列}
**Web 管理端**
- {一级菜单1}:{二级菜单/功能点罗列}
- {一级菜单2}{……}
**员工端 APP**
- {业务线1}{功能点罗列}
- {业务线2}{……}
### 3.3 业务架构设计
{分层描述业务架构,自上而下四层:用户交互层 → 应用与服务层 → 数据整合层 → 数据服务层}
**业务架构图**
![业务架构图]({业务架构图.png 相对路径})
### 3.4 核心业务详细设计
{按「端 → 功能模块(一级菜单)→ 功能点(页面/弹窗)」三级展开。功能模块与功能清单对齐,功能点与页面/弹窗对齐}
#### 3.4.1 {端名,如 Web管理端}
##### 3.4.1.1 {功能模块1}
**功能描述**{一句话说明该模块用途}
**功能点**
- **{功能点名}**{描述}
- **{功能点名}**{描述}
##### 3.4.1.2 {功能模块2}
{同上结构}
#### 3.4.2 {端名2}
{同上结构}
## 四、项目技术架构设计方案
### 4.1 系统技术架构
系统采用前后端分离架构,后端基于 Spring Cloud Alibaba 微服务体系,前端基于 Qiankun 微前端架构。
- **后端**Java 21 + Spring Boot 3.3 + Spring Cloud AlibabaNacos 注册配置中心);MyBatis-Plus 作为 ORM 框架;Sa-Token 统一认证授权;服务间调用采用 FeignFallbackFactory 降级兜底);熔断限流采用 SentinelPostgreSQL 15 数据存储;Redis 缓存与分布式锁;RocketMQ 消息队列;XXL-Job 定时任务;MinIO 文件存储。
- **前端**Qiankun 微前端 + MonorepoTurbo + npm workspaces);Vue 3 + TypeScript + ViteAnt Design Vue 组件库;Pinia 状态管理;secure-lsAES)与 jsencryptRSA)安全存储与传输。
**技术架构图**
![技术架构图]({技术架构图.png 相对路径})
### 4.2 系统技术选型说明
| 技术 | 服务类型 | 选型原因 |
|------|---------|---------|
| Java 21 | 开发语言 | 主流开发语言,LTS 版本,具备分布式、健壮性、安全性等特点 |
| Spring Boot 3.3 | 容器+MVC框架 | 简化配置,提高开发效率 |
| Spring Cloud Alibaba | 微服务框架 | Nacos 注册配置、Sentinel 熔断限流,与国内生态无缝集成 |
| Nacos | 注册配置中心 | 服务注册发现 + 动态配置管理 |
| MyBatis-Plus | ORM框架 | 主流 ORM 映射框架,简化 CRUD |
| Sa-Token | 认证授权 | 轻量级认证授权框架,支持 Token 会话 |
| Feign | 服务调用 | 声明式 HTTP 客户端,支持 FallbackFactory 降级 |
| Sentinel | 熔断限流 | 分布式系统流量防护,熔断、限流、降级 |
| PostgreSQL 15 | 数据库 | 开源关系型数据库,功能丰富、扩展性强 |
| Redis | 缓存 | 高性能缓存、分布式锁、幂等控制 |
| RocketMQ | 消息队列 | 分布式消息流平台,削峰填谷、异步解耦 |
| XXL-Job | 定时任务 | 分布式定时任务调度 |
| MinIO | 文件存储 | 开源对象存储系统 |
| Vue 3 + TypeScript | 前端框架 | 渐进式前端框架,类型安全 |
| Qiankun | 微前端 | 微前端框架,主子应用解耦 |
| Ant Design Vue | UI 组件库 | 企业级 UI 组件库 |
| Vite | 构建工具 | 新一代前端构建工具,快速冷启动 |
| nginx | 反向代理 | 高性能反向代理 Web 服务器,负载均衡 |
| docker | 容器技术 | 高性能、开源、轻量级容器引擎 |
### 4.3 系统接口设计规范
- 接口命名用名词(资源),操作类型由 HTTP 动词表达
- HTTP Method 对应资源 CURD;请求/返回格式优先 JSON
- 保证接口幂等性;URL 加入版本号
- 参数与 URL 采用蛇行命名(如 `updated_time`
- 使用 SSL 安全套接层
### 4.4 系统对接方案
#### 4.4.1 对接目标与原则
{标准与规范性 / 安全与保密性 / 可靠与稳定性 / 松耦合与可扩展性 四原则}
#### 4.4.2 对接系统范围与内容分析
| 对接类别 | 具体对接系统/设备 | 核心对接内容与数据流向 | 对接方式 |
|---------|------------------|----------------------|---------|
| {内部企业系统 / 智能硬件与物联网} | {系统名} | {单向/双向 + 数据内容} | {RESTful API / 消息队列 / 数据库视图 / SFTP} |
#### 4.4.3 技术架构与对接模式
{API 网关中心模式 / 混合对接 RESTful+JSON / 消息队列 / 数据库视图 / SFTP 文件传输}
#### 4.4.4 数据安全与隐私保护方案
{传输安全 / 系统级认证 / 用户级授权 / 数据脱敏 / 数据最小化 / 沙箱测试环境}
## 五、数据库结构设计方案
{概述一句 + 引用独立文档}
> 本系统数据库表结构设计详见《{子系统名}数据库设计说明书》,本章不再赘述。
## 六、系统安全与可用性设计方案
### 6.1 系统安全防护措施
{补全安全措施,分条:}
- **身份认证**:统一身份认证体系,按角色控制功能访问权限
- **权限控制**:基于角色的访问控制(RBAC),最小权限原则
- **数据加密**:敏感数据传输加密(HTTPS/TLS 1.2+),数据库存储加密
- **传输安全**:所有数据传输使用 HTTPS 加密,禁止明文传输敏感信息
- **操作审计**:关键操作日志记录,支持追溯
- **数据脱敏**:对外提供数据时对个人身份信息脱敏
### 6.2 系统可用性设计方案
{补全可用性设计,参考共性需求说明.md 性能要求:}
**性能指标**
| 指标 | 标准 |
|------|------|
| 请求响应时间(RT) | 平均响应 1s 以下,统计查询接口 3s 以下 |
| 系统处理能力 | HPS 10000次/秒、TPS 3000笔/秒、QPS 5000次/秒 |
| 并发用户数 | 正常负载下支持并发 3000+ |
| 错误率 | 低于千分之六(成功率高于 99.6%) |
| 稳定性 | 7×24 小时稳定运行 |
**高可用保障**
- **高可用架构**:微服务集群部署,Nacos 服务注册发现,Nginx 负载均衡
- **集群切换**:故障节点自动剔除,业务无中断,节点恢复无感知切换
- **熔断限流**:Sentinel 熔断、限流、降级;Feign 调用 FallbackFactory 降级兜底
- **幂等与并发控制**:Redis 分布式锁、接口幂等控制
- **容灾备份**:全量 + 增量备份,以小时为单位周期循环
- **监控告警**Prometheus + Grafana 指标监控 + SkyWalking 链路追踪 + ELK 日志分析 + 告警通知
## 七、部署与运维方案
### 7.1 项目部署概述
{补全部署方案:}
- **部署架构**:Docker 容器化部署,前端 Nginx + 后端微服务集群,支持水平扩展
- **环境划分**local(本地)/ dev(开发)/ test(测试)/ uat(预生产)/ prod(生产),通过 Maven Profile 激活,配置由 Nacos 统一管理
- **资源规划**:前端静态资源 Nginx 分发,数据库与缓存独立部署
- **部署步骤**:环境准备 → 数据库初始化 → Nacos 配置 → 服务部署 → 网关配置 → 前端发布
### 7.2 项目运维方案
{补全运维方案,含运维架构与监控指标:}
**运维架构**:系统运维监控体系由指标监控、链路追踪、日志分析三大支柱构成:
- 指标监控:Prometheus + Grafana
- 链路追踪:SkyWalking
- 日志分析:ELKElasticsearch + Logstash/Filebeat + Kibana
**监控核心指标**
| 监控维度 | 监控指标 |
|---------|---------|
| 系统资源 | CPU 使用率、内存使用率、磁盘 IO、网络吞吐 |
| JVM | 堆内存使用、GC 频率与耗时、线程数 |
| 应用接口 | QPS、接口响应时间(RT)、错误率、慢查询 |
| 中间件 | Redis 命中率/连接数、PostgreSQL 连接数/慢查询、RocketMQ 消息积压 |
**告警机制**:接口故障或性能劣化时通过短信、邮件、钉钉即时通知;阈值告警(CPU > 80%、内存 > 80%、接口 RT > 3s、错误率 > 0.6% 等)触发
**升级策略**:灰度发布、版本回滚
**备份策略**:数据库全量 + 增量备份、异地容灾
## 八、售后服务方案(可选章节)
<!-- 商务性质章节,甲方无要求可省略 -->
### 8.1 售后服务概述
### 8.2 售后服务体系
### 8.3 售后服务流程
### 8.4 售后服务承诺
### 8.5 售后服务计划
### 8.6 技术支持响应承诺
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env node
/**
* PRD 合并器:将同一子系统的多个终端 PRD 合并为一份《产品需求文档》
* 用法:node merge-prd.js <子系统名> <输出md路径> <终端配置JSON>
* 终端配置JSON 形如:
* [{"name":"Web管理端","prd":"web-admin/weight-management/doc/PRD.md","shot":"../../web-admin/weight-management/doc/screenshots/"}]
* 说明:只读取各终端 PRD,不改动原始文件;截图路径改写为从合并文档位置指回原模块
*/
const fs = require('fs');
const sysName = process.argv[2] || '体重管理系统';
const outPath = process.argv[3] || `交付文档/${sysName}/${sysName}产品需求文档.md`;
const terms = process.argv[4]
? JSON.parse(process.argv[4])
: [
{ name: 'Web管理端', prd: 'web-admin/weight-management/doc/PRD.md', shot: '../../web-admin/weight-management/doc/screenshots/' },
{ name: '员工端APP', prd: 'employee-app/weight-management/doc/PRD.md', shot: '../../employee-app/weight-management/doc/screenshots/' },
];
// 提取某标记之后、到下一个 ##/### 标题之前的内容
function sectionAfter(content, startMarker) {
const i = content.indexOf(startMarker);
if (i < 0) return '';
const rest = content.slice(i + startMarker.length);
const m = rest.match(/\n##?\s/);
const end = m ? m.index : rest.length;
return rest.slice(0, end).trim();
}
// 提取两个标记之间的内容;end 标记不存在时返回空字符串
function between(content, start, end) {
const i = content.indexOf(start);
if (i < 0) return '';
let j = end ? content.indexOf(end, i + start.length) : content.length;
if (j < 0) return '';
return content.slice(i + start.length, j).trim();
}
// 整合后的子系统概述(各子系统需定制,此处为体重管理示例)
const overview = `体重管理系统覆盖 Web 管理端与员工端 APP 两大终端,构建「目标设定—活动组织—过程监测—效果评估」的员工体重管理闭环。
- **Web 管理端**:为管理人员提供干预活动创建与管理、员工报名跟踪与目标体重审核、过程数据监测、历史活动归档、人员数据查询、个人目标管理、多类型体重测量设备管理及异常事件汇总等能力。
- **员工端 APP**:为员工提供目标设定与审核、每日吃/动/体重打卡、吃动平衡分析、膳食与运动建议、体重记录与报告、排行榜,以及蓝牙体脂秤设备管理等能力。`;
let md = '';
md += `# ${sysName}产品需求文档\n\n`;
md += `> 本文档由以下终端 PRD 合并生成(原始文件未改动):\n`;
terms.forEach(t => (md += `> - ${t.name}\`${t.prd}\`\n`));
md += `\n---\n\n`;
md += `## 1. 子系统概述\n\n${overview}\n\n`;
// 页面导航结构(按端分组)
md += `### 页面导航结构\n\n`;
terms.forEach(t => {
const prd = fs.readFileSync(t.prd, 'utf8');
let nav = between(prd, '### 页面导航结构', '### 状态流转图');
if (!nav) nav = between(prd, '### 页面导航结构', '## 页面清单');
md += `#### ${t.name}\n\n${nav}\n\n`;
});
// 状态流转图(存在才输出)
const firstPrd = fs.readFileSync(terms[0].prd, 'utf8');
let stateDiagram = between(firstPrd, '### 状态流转图', '## 页面清单');
if (!stateDiagram) {
for (let k = 1; k < terms.length; k++) {
const prd = fs.readFileSync(terms[k].prd, 'utf8');
stateDiagram = between(prd, '### 状态流转图', '## 页面清单');
if (stateDiagram) break;
}
}
if (stateDiagram) md += `### 状态流转图\n\n${stateDiagram}\n\n`;
// 页面清单(按端分组)
md += `## 2. 页面清单\n\n`;
terms.forEach(t => {
const prd = fs.readFileSync(t.prd, 'utf8');
const list = between(prd, '## 页面清单', '## 逐页说明');
md += `### ${t.name}\n\n${list}\n\n`;
});
// 逐页说明(按端分组,截图路径改写)
md += `## 3. 逐页说明\n\n`;
terms.forEach(t => {
const prd = fs.readFileSync(t.prd, 'utf8');
let detail = between(prd, '## 逐页说明', null);
detail = detail.replace(/screenshots\//g, t.shot);
md += `### ${t.name}\n\n${detail}\n\n`;
});
fs.writeFileSync(outPath, md, 'utf8');
console.log(`已生成:${outPath}`);
console.log(`合并终端:${terms.map(t => t.name).join('、')}`);
@@ -0,0 +1,115 @@
<!--
操作手册模板
用途:doc-operation-manual skill 的生成骨架
粒度:按「子系统 + 终端」分册,每个终端单独一份操作手册
截图规范(必须遵守):
- 范围:只截「主界面」+「重要表单/详情页」;弹窗、筛选下拉等轻量交互不单独截图,在功能描述/注意事项中用文字或表格说明
- 规格:JPEG 质量 85%(清晰度优先,不设文件大小上限),APP 端宽度 390px、Web 端视口 1440×900(截图前确保浏览器全屏,避免截图不完整/比例失调,参考 项目背景.md 7.4 节 PRD 截图规范)
- 目录:按终端分目录 screenshots/{终端目录}/employee-app / web-admin / medical-pad 等),截图命名用中文页面名
- 引用:相对路径 screenshots/{终端目录}/{页面名}.jpg
通用约束:全文不得出现客户名称与项目名称,只介绍系统自身操作,产出文档可供任意客户与人群阅读。
PDF 导出规范(竖版 A4):
- 版式:竖版 A4(210×297mm),最终交付为 PDF;本 .md 为源,导出时按下方分页原则处理
- 分页三原则:① 标题不孤行(标题与其后内容同页)② 图文不割裂(操作步骤/截图/注意事项作为一个版块,
图片与其标题不跨页)③ 表行不切割(常见问题表格单行不被水平切成两页)
- 可选 CSS(浏览器 / Puppeteer 系导出工具):
@page { size: A4 portrait; margin: 18mm 16mm; }
h1,h2,h3,h4 { page-break-after: avoid; }
img, pre, blockquote { page-break-inside: avoid; }
tr { page-break-inside: avoid; }
- 无法避免的跨页(超长截图等)由使用方在导出时自行处理
-->
# {子系统名} · {终端名}操作手册
| 项 | 内容 |
|----|------|
| 文档类型 | 操作手册 |
| 适用终端 | {终端名} |
| 适用对象 | {用户角色} |
| 版本号 | V1.0 |
| 编制日期 | {YYYY-MM-DD} |
---
## 目录
<!-- 转 PDF 工具按标题层级自动生成 -->
## 1. 概述
### 1.1 系统简介
{一句话说明本终端在子系统中的用途与价值}
### 1.2 适用对象
{本终端面向的用户角色,如:平台管理员 / 企业员工 / 体检医生 / 医疗点医护人员}
### 1.3 运行环境
{设备 / 浏览器 / 分辨率要求,引用 `项目背景.md` 2.x 视觉规范对应终端的尺寸}
- 示例:Web 管理端需 Chrome 浏览器,最小宽度 1280px
### 1.4 登录说明
{登录步骤 + 界面截图 + 账号说明}
**操作步骤**
1. {打开系统,进入登录页}
2. {输入账号密码}
3. {点击登录}
![登录页]({截图相对路径})
## 2. 界面总览
{整体布局与功能区说明 + 首页/工作台截图 + 导航结构说明}
- 顶部导航 / 侧边栏 / 内容区 各功能区职责
![首页]({截图相对路径})
## 3. 功能操作指南
{按「一级菜单/业务线」分组,逐功能描述;每个功能含 操作步骤 + 截图 + 注意事项}
### 3.1 {一级功能1}
#### 3.1.1 {功能点}
**操作步骤**
1. {步骤1}
2. {步骤2}
3. {步骤3}
**界面截图**
![{功能点}](screenshots/{终端目录}/{页面名}.jpg)
**注意事项**
- {注意点1}
- {注意点2}
### 3.2 {一级功能2}
{同上结构}
## 4. 常见问题与故障处理
| 序号 | 问题现象 | 可能原因 | 处理方法 |
|------|---------|---------|---------|
| 1 | {问题} | {原因} | {处理} |
## 5. 附录
### 5.1 术语表
| 术语 | 说明 |
|------|------|
### 5.2 快捷操作
{键盘快捷键 / 常用入口 汇总}
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env node
/**
* SQL 解析器:将 Navicat 导出的 PostgreSQL/MySQL 建表 SQL 解析为数据库设计说明书 Markdown
* 用法:node parse-sql.js <SQL文件路径> <输出md路径> <子系统名> [数据库类型]
* 示例:node parse-sql.js doc/platform-weight.sql 交付文档/体重管理系统/数据库设计说明书.md 体重管理系统 PostgreSQL
*/
const fs = require('fs');
const sqlPath = process.argv[2];
const outPath = process.argv[3];
const systemName = process.argv[4] || '';
const dbType = (process.argv[5] || 'PostgreSQL');
if (!sqlPath || !outPath) {
console.error('用法:node parse-sql.js <SQL文件> <输出md> <子系统名> [数据库类型]');
process.exit(1);
}
const sql = fs.readFileSync(sqlPath, 'utf8');
// 自动识别 schema:取第一个 CREATE TABLE 里的 "schema"."table"
const schemaMatch = sql.match(/CREATE TABLE "([^"]+)"\."(\w+)" \(/);
const schema = schemaMatch ? schemaMatch[1] : '';
// ===== 解析 CREATE TABLE 块 =====
const tableBlocks = [];
const tableRe = /CREATE TABLE "[^"]+"\."(\w+)" \(([\s\S]*?)\)\s*;/g;
let m;
while ((m = tableRe.exec(sql))) {
tableBlocks.push({ name: m[1], body: m[2] });
}
// ===== 解析表注释 =====
const tableComment = {};
const tCommentRe = /COMMENT ON TABLE "[^"]+"\."(\w+)" IS '([^']*)';/g;
while ((m = tCommentRe.exec(sql))) tableComment[m[1]] = m[2];
// ===== 解析字段注释 =====
const colComment = {};
const cCommentRe = /COMMENT ON COLUMN "[^"]+"\."(\w+)"\."(\w+)" IS '([^']*)';/g;
while ((m = cCommentRe.exec(sql))) {
if (!colComment[m[1]]) colComment[m[1]] = {};
colComment[m[1]][m[2]] = m[3];
}
// ===== 解析主键 =====
const pkey = {};
const pkRe = /ALTER TABLE "[^"]+"\."(\w+)" ADD CONSTRAINT "\w+" PRIMARY KEY \(([^)]*)\);/g;
while ((m = pkRe.exec(sql))) {
pkey[m[1]] = m[2].split(',').map(s => s.trim().replace(/"/g, ''));
}
// ===== 解析索引 =====
const indexes = {};
const idxRe = /CREATE (UNIQUE )?INDEX "(\w+)" ON "[^"]+"\."(\w+)" USING \w+ \(([\s\S]*?)\)[^;]*;/g;
while ((m = idxRe.exec(sql))) {
const unique = !!m[1];
const idxName = m[2];
const table = m[3];
const cols = m[4].split(',').map(s => s.trim().replace(/"/g, '').replace(/\s.*$/, ''));
if (!indexes[table]) indexes[table] = [];
indexes[table].push({ name: idxName, cols, unique });
}
// ===== 解析字段 =====
function parseFields(body) {
const fields = [];
const lines = body.split('\n');
for (const line of lines) {
const fm = line.match(/\s*"(\w+)"\s+(\w+)(?:\(([^)]*)\))?/);
if (!fm) continue;
const name = fm[1];
const type = fm[2];
const len = fm[3] || '';
const notNull = /\bNOT NULL\b/.test(line);
fields.push({ name, type, len, notNull });
}
return fields;
}
// ===== 提取表中文名(取注释括号前部分)=====
function cnName(table) {
const c = tableComment[table] || '';
const idx = c.indexOf('');
return idx > 0 ? c.slice(0, idx) : (c || table);
}
// ===== 提取数据字典(枚举字段:注释含 数字- 模式)=====
function extractDict() {
const dict = [];
for (const table of tableBlocks.map(t => t.name)) {
for (const [col, comment] of Object.entries(colComment[table] || {})) {
// 注释含形如 "0-xx 1-xx" 的枚举
if (/\d+\s*-\s*\S+/.test(comment) && !/主键|雪花|时间|日期|id|编号|编码|姓名|电话/.test(comment)) {
dict.push({ table, col, comment });
}
}
}
return dict;
}
// ===== 生成 Markdown =====
const date = new Date().toISOString().slice(0, 10);
let md = '';
md += `# ${systemName}数据库设计说明书\n\n`;
md += `| 项 | 内容 |\n|----|------|\n`;
md += `| 文档类型 | 数据库设计说明书 |\n`;
md += `| 数据库 | ${dbType} |\n`;
md += `| Schema | ${schema} |\n`;
md += `| 数据表数量 | ${tableBlocks.length} |\n`;
md += `| 版本号 | V1.0 |\n`;
md += `| 编制日期 | ${date} |\n\n`;
md += `---\n\n`;
// 1. 概述
md += `## 1. 概述\n\n`;
md += `### 1.1 数据库环境\n\n`;
md += `- 数据库类型:${dbType}\n`;
md += `- Schema\`${schema}\`\n`;
md += `- 字符集:UTF-8\n\n`;
md += `### 1.2 命名规范\n\n`;
md += `- 表名统一以 \`wm_\` 前缀开头,采用小写下划线命名\n`;
md += `- 字段名采用小写下划线命名(如 \`org_code\`\n`;
md += `- 每张表均设置主键 \`id\`(雪花 ID),主键不表示业务含义\n`;
md += `- 必须为表、字段添加注释\n`;
md += `- 不使用数据库外键,表间关联由程序保证\n\n`;
md += `### 1.3 设计原则\n\n`;
md += `- **一致性原则**:统一分析与设计数据来源,保证数据一致性与有效性\n`;
md += `- **数据独立性原则**:数据空间独立、数据结构独立\n`;
md += `- **完整性原则**:对输入数据有审核和约束机制\n`;
md += `- **安全性原则**:认证与授权机制、数据隔离、实时备份、数据加密\n`;
md += `- **可伸缩性与可扩展性原则**:充分考虑发展与移植需要\n`;
md += `- **规范化**:遵循规范化理论,减少插入/删除/修改异常\n\n`;
// 2. 数据表清单
md += `## 2. 数据表清单\n\n`;
md += `| 序号 | 表中文名 | 英文表名 | 说明 |\n|------|---------|---------|------|\n`;
tableBlocks.forEach((t, i) => {
md += `| ${i + 1} | ${cnName(t.name)} | \`${t.name}\` | ${tableComment[t.name] || ''} |\n`;
});
md += `\n`;
// 3. 数据表结构说明
md += `## 3. 数据表结构说明\n\n`;
tableBlocks.forEach((t, i) => {
md += `### 3.${i + 1} ${cnName(t.name)}(\`${t.name}\`)\n\n`;
md += `${tableComment[t.name] || ''}\n\n`;
md += `| 序号 | 列名 | 数据类型 | 长度 | 主键 | 允许空值 | 列说明 |\n`;
md += `|------|------|---------|------|------|---------|--------|\n`;
const fields = parseFields(t.body);
const pkCols = pkey[t.name] || [];
fields.forEach((f, j) => {
const isPk = pkCols.includes(f.name);
md += `| ${j + 1} | ${f.name} | ${f.type} | ${f.len} | ${isPk ? '是' : '否'} | ${f.notNull ? 'NO' : 'YES'} | ${colComment[t.name]?.[f.name] || ''} |\n`;
});
md += `\n`;
});
// 4. 表关系与 ER 图
md += `## 4. 表关系与 ER 图\n\n`;
md += `数据库设计未使用外键约束,表间通过业务字段关联,由程序层保证引用完整性。\n\n`;
const fkMap = {};
tableBlocks.forEach(t => {
parseFields(t.body).forEach(f => {
if (/_(id|code|no|sn)$/.test(f.name) && f.name !== 'id' && f.name !== 'original_id') {
if (!fkMap[f.name]) fkMap[f.name] = [];
fkMap[f.name].push(t.name);
}
});
});
if (Object.keys(fkMap).length) {
md += `**关联字段清单**(据此补全 ER 关系):\n\n`;
Object.entries(fkMap).forEach(([fk, tables]) => {
md += `- \`${fk}\`${tables.map(t => '`' + t + '`').join('、')}\n`;
});
md += `\n`;
}
md += `\`\`\`mermaid\nerDiagram\n`;
md += ` %% TODO: 由人工/技能根据上方关联字段清单补充核心表关系\n`;
md += `\`\`\`\n\n`;
// 5. 索引与约束设计
md += `## 5. 索引与约束设计\n\n`;
md += `| 表 | 索引/约束名 | 字段 | 类型 | 说明 |\n|----|------------|------|------|------|\n`;
tableBlocks.forEach(t => {
const pkCols = pkey[t.name] || [];
if (pkCols.length) {
md += `| ${t.name} | ${t.name}_pkey | ${pkCols.join(', ')} | 主键 | 主键约束 |\n`;
}
(indexes[t.name] || []).forEach(idx => {
md += `| ${t.name} | ${idx.name} | ${idx.cols.join(', ')} | ${idx.unique ? '唯一索引' : '普通索引'} | |\n`;
});
});
md += `\n`;
// 6. 数据字典
md += `## 6. 数据字典\n\n`;
const dict = extractDict();
md += `| 表 | 字段 | 取值说明 |\n|----|------|---------|\n`;
dict.forEach(d => {
md += `| ${d.table} | ${d.col} | ${d.comment} |\n`;
});
md += `\n`;
fs.writeFileSync(outPath, md, 'utf8');
console.log(`已生成:${outPath}`);
console.log(`表数量:${tableBlocks.length},字段注释:${Object.values(colComment).reduce((s, t) => s + Object.keys(t).length, 0)},索引:${Object.values(indexes).reduce((s, a) => s + a.length, 0)},数据字典条目:${dict.length}`);
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env node
/**
* HTML 宣传册转 PDF(横版 A4,保留背景)
* 用法:node pdf-export.js <html文件路径>
* 输出:与 HTML 同目录同名 .pdf
* 依赖:playwrightchromium),无则用系统 Edge 兜底
*/
const { chromium } = require('playwright');
const { pathToFileURL } = require('url');
const path = require('path');
const htmlFile = process.argv[2];
if (!htmlFile) {
console.error('用法:node pdf-export.js <html文件路径>');
process.exit(1);
}
(async () => {
const abs = path.resolve(htmlFile);
const url = pathToFileURL(abs).href;
const pdfFile = process.argv[3] || abs.replace(/\.html$/i, '.pdf');
let browser;
try {
browser = await chromium.launch();
} catch (e) {
// playwright 自带 chromium 不可用时,退回系统 Edge
browser = await chromium.launch({ channel: 'msedge' });
}
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
await page.pdf({
path: pdfFile,
format: 'A4',
landscape: true,
printBackground: true,
margin: { top: 0, bottom: 0, left: 0, right: 0 },
displayHeaderFooter: false,
});
await browser.close();
console.log('PDF 已生成:' + pdfFile);
})();
+86
View File
@@ -0,0 +1,86 @@
<!--
产品需求文档(合并版)模板
用途:doc-prd-merge skill 的生成骨架
核心逻辑:同一子系统的多个终端已各自维护 doc/PRD.md,本模板用于「合成一份新文件」,不改动原有 PRD、沿用现有截图
交付定位:单独交付(不与原型代码一起交付),文档内不得出现页面文件路径与页面文件名
通用约束:全文不得出现客户名称与项目名称,只介绍产品需求本身,产出文档可供任意客户与人群阅读。
PDF 导出规范(竖版 A4):
- 版式:竖版 A4(210×297mm),最终交付为 PDF;本 .md 为源,导出时按下方分页原则处理
- 分页三原则:① 标题不孤行(标题与其后内容同页)② 图文不割裂(页面标题/截图/功能说明作为一个版块,
图片与其标题不跨页)③ 表行不切割(「系统功能清单」HTML 表格含 rowspan 合并单元格,
导出时尤需保证合并单元整体不被水平切成两页)
- 可选 CSS(浏览器 / Puppeteer 系导出工具):
@page { size: A4 portrait; margin: 18mm 16mm; }
h1,h2,h3,h4 { page-break-after: avoid; }
img, pre, blockquote { page-break-inside: avoid; }
tr { page-break-inside: avoid; }
- 无法避免的跨页(超大功能清单表 / 超长截图)由使用方在导出时自行处理
-->
# {子系统名}产品需求文档
## 1. 子系统概述
{整合各终端「模块概述」,改写成统一的一段,覆盖本子系统所有终端的整体功能}
- 素材:各终端 PRD 的「模块概述」段
### 系统功能清单
{按端分组,用 HTML 表格 + rowspan 合并单元格呈现功能层级,三列:功能模块 / 功能子模块 / 功能点}
{合并规则:列1「功能模块」= 一级菜单,rowspan 合并;列2「功能子模块」= 二级菜单,rowspan 合并;列3「功能点」= 叶子功能,每行一个}
{无二级子模块的模块(如健康现状),列2 留空即可}
#### {端1(如 Web管理端)}
<table border="1" cellspacing="0" cellpadding="8" style="border-collapse:collapse; font-size:14px;">
<thead>
<tr>
<th style="width:120px;">功能模块</th>
<th style="width:220px;">功能子模块</th>
<th>功能点</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="2">档案</td>
<td rowspan="2">个人监测数据</td>
<td>个人监测数据列表</td>
</tr>
<tr>
<td>个人监测详情</td>
</tr>
<tr>
<td>健康现状</td>
<td></td>
<td>员工评估信息</td>
</tr>
</tbody>
</table>
#### {端2}
{同上}
## 2. 逐页说明
{按端分组,每页标题仅保留功能名(不含文件名),截图路径改为从本文档位置指回原模块 doc/screenshots/ 的相对路径}
### {端1}
#### {页面功能名}
![{页面功能名}]({指回原截图的相对路径})
**功能说明**{沿用原文}
**业务规则**{沿用原文}
**交互说明**{沿用原文}
### {端2}
{同上}
{表单字段表:仅当各终端原 PRD 存在独立「表单字段表」段时才合并生成,否则本部分省略}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -238,7 +238,7 @@
- 员工信息:姓名(张晓红)、性别、年龄、部门
- 数据对比表:初始数据 / 目标数据 / 当前数据(体重、BMI、体脂率、腰围)
- 变化指标:绿色显示下降数值
- 体重记录列表:按时间倒序,每条显示时间、体重、体脂率、腰围、来源标签(身高秤、蓝牙称、体脂仪、自记)
- 体重记录列表:按时间倒序,每条显示时间、体重、体脂率、腰围、来源标签(体重秤、蓝牙称、体脂仪、自记)
**交互说明:**
- 点击返回:返回体重记录页
@@ -65,7 +65,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="form-group">
<div class="form-row">
<span class="form-label">姓名</span>
<input class="form-input" id="inputName" type="text" value="王建国" placeholder="请输入姓名" />
<input class="form-input" id="inputName" type="text" value="石维国" placeholder="请输入姓名" />
</div>
</div>
</div>
+1 -1
View File
@@ -38,7 +38,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:68px;height:68px;border-radius:50%;background:linear-gradient(135deg,#e6f4ff,#91caff);display:flex;align-items:center;justify-content:center;font-size:34px;border:3px solid rgba(255,255,255,0.5);">👤</div>
<div>
<div style="color:#fff;font-size:20px;font-weight:700;">王建</div>
<div style="color:#fff;font-size:20px;font-weight:700;">石维</div>
<div style="color:rgba(255,255,255,0.8);font-size:13px;margin-top:3px;">健康管理员</div>
<div style="color:rgba(255,255,255,0.78);font-size:12px;margin-top:4px;">安全环保部 · 健康管理科</div>
</div>
@@ -140,24 +140,24 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<script>
/* ===== Mock数据 ===== */
var activeOrders = [
{ id:'EM202606001', method:'video', methodLabel:'应急视频', time:'2026-06-22 14:35', empName:'张小红', empGender:'女', empAge:32, empNo:'CQ001256', empUnit:'公司机关', empDept:'办公室', empPhone:'13812346789', center:'第一应急中心', operator:'王建国', professional:'刘志明' },
{ id:'EM202606001', method:'video', methodLabel:'应急视频', time:'2026-06-22 14:35', empName:'张小红', empGender:'女', empAge:32, empNo:'CQ001256', empUnit:'公司机关', empDept:'办公室', empPhone:'13812346789', center:'第一应急中心', operator:'石维国', professional:'刘志明' },
{ id:'EM202606002', method:'phone', methodLabel:'应急电话', time:'2026-06-22 10:12', empName:'李建国', empGender:'男', empAge:45, empNo:'CQ000832', empUnit:'采油一厂', empDept:'技术部', empPhone:'13923458901', center:'第二应急中心', operator:'赵海龙', professional:'陈晓东' },
{ id:'EM202606003', method:'120', methodLabel:'拨打120', time:'2026-06-21 08:20', empName:'赵志强', empGender:'男', empAge:61, empNo:'CQ000089', empUnit:'公司机关', empDept:'后勤部', empPhone:'13756782345', center:'第一应急中心', operator:'王建国', professional:'周伟' }
{ id:'EM202606003', method:'120', methodLabel:'拨打120', time:'2026-06-21 08:20', empName:'赵志强', empGender:'男', empAge:61, empNo:'CQ000089', empUnit:'公司机关', empDept:'后勤部', empPhone:'13756782345', center:'第一应急中心', operator:'石维国', professional:'周伟' }
];
var doneOrders = [
{ id:'EM202606004', method:'video', methodLabel:'应急视频', time:'2026-06-20 16:05', empName:'王芳', empGender:'女', empAge:28, empNo:'CQ001103', empUnit:'信息技术中心', empDept:'数据部', empPhone:'13634564567', center:'第一应急中心', operator:'王建国', professional:'刘志明' },
{ id:'EM202606004', method:'video', methodLabel:'应急视频', time:'2026-06-20 16:05', empName:'王芳', empGender:'女', empAge:28, empNo:'CQ001103', empUnit:'信息技术中心', empDept:'数据部', empPhone:'13634564567', center:'第一应急中心', operator:'石维国', professional:'刘志明' },
{ id:'EM202606005', method:'phone', methodLabel:'应急电话', time:'2026-06-18 09:45', empName:'陈伟', empGender:'男', empAge:36, empNo:'CQ000567', empUnit:'安全环保部', empDept:'安环部', empPhone:'13545677890', center:'第三应急中心', operator:'孙海涛', professional:'黄志强' },
{ id:'EM202606006', method:'120', methodLabel:'拨打120', time:'2026-06-15 22:10', empName:'孙丽', empGender:'女', empAge:41, empNo:'CQ001340', empUnit:'采油一厂', empDept:'作业区', empPhone:'15867890123', center:'第二应急中心', operator:'赵海龙', professional:'马云飞' },
{ id:'EM202606007', method:'video', methodLabel:'应急视频', time:'2026-06-12 11:30', empName:'周明', empGender:'男', empAge:33, empNo:'CQ000921', empUnit:'信息技术中心', empDept:'技术部', empPhone:'18678903456', center:'第一应急中心', operator:'王建国', professional:'刘志明' },
{ id:'EM202606007', method:'video', methodLabel:'应急视频', time:'2026-06-12 11:30', empName:'周明', empGender:'男', empAge:33, empNo:'CQ000921', empUnit:'信息技术中心', empDept:'技术部', empPhone:'18678903456', center:'第一应急中心', operator:'石维国', professional:'刘志明' },
{ id:'EM202606008', method:'phone', methodLabel:'应急电话', time:'2026-06-10 15:20', empName:'刘海波', empGender:'男', empAge:55, empNo:'CQ000234', empUnit:'公司机关', empDept:'行政部', empPhone:'13389015678', center:'第三应急中心', operator:'孙海涛', professional:'周伟' },
{ id:'EM202606009', method:'120', methodLabel:'拨打120', time:'2026-06-08 07:55', empName:'吴静', empGender:'女', empAge:26, empNo:'CQ001412', empUnit:'公司机关', empDept:'人力部', empPhone:'15289016789', center:'第二应急中心', operator:'赵海龙', professional:'陈晓东' },
{ id:'EM202606010', method:'video', methodLabel:'应急视频', time:'2026-06-05 13:40', empName:'黄晓明', empGender:'男', empAge:42, empNo:'CQ000789', empUnit:'采油一厂', empDept:'工程部', empPhone:'18990120123', center:'第一应急中心', operator:'王建国', professional:'黄志强' },
{ id:'EM202606010', method:'video', methodLabel:'应急视频', time:'2026-06-05 13:40', empName:'黄晓明', empGender:'男', empAge:42, empNo:'CQ000789', empUnit:'采油一厂', empDept:'工程部', empPhone:'18990120123', center:'第一应急中心', operator:'石维国', professional:'黄志强' },
{ id:'EM202606011', method:'phone', methodLabel:'应急电话', time:'2026-06-03 18:15', empName:'杨丽华', empGender:'女', empAge:39, empNo:'CQ001098', empUnit:'安全环保部', empDept:'安环部', empPhone:'13101234567', center:'第三应急中心', operator:'孙海涛', professional:'马云飞' },
{ id:'EM202605001', method:'video', methodLabel:'应急视频', time:'2026-05-28 10:00', empName:'郑国强', empGender:'男', empAge:50, empNo:'CQ000456', empUnit:'公司机关', empDept:'财务部', empPhone:'17712347890', center:'第二应急中心', operator:'赵海龙', professional:'刘志明' },
{ id:'EM202605002', method:'120', methodLabel:'拨打120', time:'2026-05-25 20:30', empName:'马燕', empGender:'女', empAge:35, empNo:'CQ001521', empUnit:'信息技术中心', empDept:'运维部', empPhone:'15023452345', center:'第一应急中心', operator:'王建国', professional:'周伟' },
{ id:'EM202605002', method:'120', methodLabel:'拨打120', time:'2026-05-25 20:30', empName:'马燕', empGender:'女', empAge:35, empNo:'CQ001521', empUnit:'信息技术中心', empDept:'运维部', empPhone:'15023452345', center:'第一应急中心', operator:'石维国', professional:'周伟' },
{ id:'EM202605003', method:'phone', methodLabel:'应急电话', time:'2026-05-20 14:50', empName:'林伟东', empGender:'男', empAge:44, empNo:'CQ000678', empUnit:'采油一厂', empDept:'设备部', empPhone:'19934563456', center:'第三应急中心', operator:'孙海涛', professional:'陈晓东' },
{ id:'EM202604001', method:'video', methodLabel:'应急视频', time:'2026-04-15 09:10', empName:'何秀兰', empGender:'女', empAge:47, empNo:'CQ001623', empUnit:'公司机关', empDept:'后勤部', empPhone:'15545676789', center:'第一应急中心', operator:'王建国', professional:'黄志强' }
{ id:'EM202604001', method:'video', methodLabel:'应急视频', time:'2026-04-15 09:10', empName:'何秀兰', empGender:'女', empAge:47, empNo:'CQ001623', empUnit:'公司机关', empDept:'后勤部', empPhone:'15545676789', center:'第一应急中心', operator:'石维国', professional:'黄志强' }
];
/* ===== 当前筛选状态 ===== */
+1 -1
View File
@@ -51,7 +51,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<!-- [交互] 点击头像可跳转至个人中心 -->
<div style="width:48px;height:48px;border-radius:50%;background:rgba(255,255,255,0.25);display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0;border:2px solid rgba(255,255,255,0.4);cursor:pointer;" onclick="location.href='../profile/index.html'">👤</div>
<div style="flex:1;min-width:0;">
<div style="color:#fff;font-size:18px;font-weight:700;">王建</div>
<div style="color:#fff;font-size:18px;font-weight:700;">石维</div>
<div style="color:rgba(255,255,255,0.85);font-size:13px;margin-top:2px;">健康管理员</div>
<div style="color:rgba(255,255,255,0.65);font-size:11px;margin-top:2px;">安全环保部 · 健康管理科</div>
</div>
@@ -158,7 +158,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="record-item__metric"><div class="record-item__metric-label">体重</div><div class="record-item__metric-val">66.8<span class="record-item__metric-unit">kg</span></div></div>
<div class="record-item__metric"><div class="record-item__metric-label">体脂率</div><div class="record-item__metric-val">29.1<span class="record-item__metric-unit">%</span></div></div>
<div class="record-item__metric"><div class="record-item__metric-label">腰围</div><div class="record-item__metric-val">80.5<span class="record-item__metric-unit">cm</span></div></div>
<div class="record-item__source"><span class="source-badge source-badge--scale">身高</span></div>
<div class="record-item__source"><span class="source-badge source-badge--scale">体重</span></div>
</div>
</div>
+2 -1
View File
@@ -129,7 +129,8 @@ function initTabLinks() {
professional: '../professional',
operator: '../operator',
onsite: '../onsite',
admin: '../admin'
admin: '../admin',
nutritionist: '../nutritionist'
};
var base = roleBaseMap[role] || '../expert';
document.getElementById('link-workbench').href = base + '/workbench/index.html';
+8
View File
@@ -1,5 +1,13 @@
# 咨询小助手端(健康应急APP)变更记录
## 2026-09-07
### fix: 截图脚本失效引用修复与孤儿截图清理
**涉及文件:**
- `doc/screenshot.mjs`(修改):workbench / knowledge / profile 三个任务的 `index-v2.html` 引用已随页面改名失效,恢复为现存 `index.html`;删除 `consult-done.html` 任务(页面已不存在)
- `doc/screenshots/consult-done.jpg`(删除):来源页面已删除且无任何文档引用,孤儿截图
## 2026-04-10
### feat: 新增"我的"个人中心模块
+3 -4
View File
@@ -12,20 +12,19 @@ const screenshotsDir = path.join(__dirname, 'screenshots');
// 小助手端所有页面
const pages = [
// 工作台
{ file: '../../assistant/workbench/index-v2.html', name: 'index' },
{ file: '../../assistant/workbench/index.html', name: 'index' },
{ file: '../../assistant/workbench/text-chat.html', name: 'text-chat' },
{ file: '../../assistant/workbench/text-chat-done.html', name: 'text-chat-done' },
{ file: '../../assistant/workbench/consult-done.html', name: 'consult-done' },
{ file: '../../assistant/workbench/text-consult-done.html', name: 'text-consult-done' },
{ file: '../../assistant/workbench/text-consult-timeout.html', name: 'text-consult-timeout' },
{ file: '../../assistant/workbench/employee-profile.html', name: 'employee-profile' },
{ file: '../../assistant/workbench/end-consult-dialog.html', name: 'end-consult-dialog' },
{ file: '../../assistant/workbench/push-expert.html', name: 'push-expert' },
// 知识库
{ file: '../../assistant/knowledge/index-v2.html', name: 'knowledge-index' },
{ file: '../../assistant/knowledge/index.html', name: 'knowledge-index' },
{ file: '../../assistant/knowledge/detail.html', name: 'knowledge-detail' },
// 个人中心
{ file: '../../assistant/profile/index-v2.html', name: 'profile-index' },
{ file: '../../assistant/profile/index.html', name: 'profile-index' },
{ file: '../../assistant/profile/edit-profile.html', name: 'edit-profile' },
{ file: '../../assistant/profile/change-pwd.html', name: 'change-pwd' },
];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

+8
View File
@@ -1,5 +1,13 @@
# 咨询专家端(健康应急APP)变更记录
## 2026-09-07
### fix: 截图脚本失效引用修复并补拍视频进行中截图
**涉及文件:**
- `doc/screenshot.mjs`(修改):workbench / knowledge / profile / ai-assistant / message 五处 `index-v2.html` 引用已随页面改名失效,恢复为现存 `index.html`;新增 `video-in-progress.html → video-in-progress.jpg` 任务
- `doc/screenshots/video-in-progress.jpg`(新增):PRD 第 8 节一直引用该图但从未生成(脚本漏拍此页),按脚本规范补拍
## 2026-04-10
### fix: 修复共用消息页面和AI助手页面的工作台跳转逻辑
+6 -5
View File
@@ -12,21 +12,22 @@ const screenshotsDir = path.join(__dirname, 'screenshots');
// 专家端所有页面
const pages = [
// 工作台
{ file: '../../expert/workbench/index-v2.html', name: 'index' },
{ file: '../../expert/workbench/index.html', name: 'index' },
{ file: '../../expert/workbench/text-chat.html', name: 'text-chat' },
{ file: '../../expert/workbench/text-chat-done.html', name: 'text-chat-done' },
{ file: '../../expert/workbench/text-consult-done.html', name: 'text-consult-done' },
{ file: '../../expert/workbench/text-consult-timeout.html', name: 'text-consult-timeout' },
{ file: '../../expert/workbench/video-appt.html', name: 'video-appt' },
{ file: '../../expert/workbench/video-ongoing.html', name: 'video-ongoing' },
{ file: '../../expert/workbench/video-in-progress.html', name: 'video-in-progress' },
{ file: '../../expert/workbench/video-done.html', name: 'video-done' },
{ file: '../../expert/workbench/video-cancelled.html', name: 'video-cancelled' },
{ file: '../../expert/workbench/video-rejected.html', name: 'video-rejected' },
// 知识库
{ file: '../../expert/knowledge/index-v2.html', name: 'knowledge-index' },
{ file: '../../expert/knowledge/index.html', name: 'knowledge-index' },
{ file: '../../expert/knowledge/detail.html', name: 'knowledge-detail' },
// 个人中心
{ file: '../../expert/profile/index-v2.html', name: 'profile-index' },
{ file: '../../expert/profile/index.html', name: 'profile-index' },
{ file: '../../expert/profile/edit-profile.html', name: 'edit-profile' },
{ file: '../../expert/profile/edit-diseases.html', name: 'edit-diseases' },
{ file: '../../expert/profile/schedule.html', name: 'schedule' },
@@ -34,8 +35,8 @@ const pages = [
{ file: '../../expert/profile/reviews.html', name: 'reviews' },
{ file: '../../expert/profile/change-pwd.html', name: 'change-pwd' },
// 共享模块
{ file: '../../ai-assistant/index-v2.html', name: 'ai-assistant' },
{ file: '../../message/index-v2.html', name: 'message-index' },
{ file: '../../ai-assistant/index.html', name: 'ai-assistant' },
{ file: '../../message/index.html', name: 'message-index' },
{ file: '../../message/contacts.html', name: 'message-contacts' },
{ file: '../../message/im-chat.html', name: 'message-im-chat' },
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+2 -1
View File
@@ -265,7 +265,8 @@ function initTabLinks() {
professional: '../professional',
operator: '../operator',
onsite: '../onsite',
admin: '../admin'
admin: '../admin',
nutritionist: '../nutritionist'
};
var base = roleBaseMap[role] || '../expert';
document.getElementById('link-workbench').href = base + '/workbench/index.html';
@@ -0,0 +1,159 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.back-btn { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<div class="nav-bar">
<button class="back-btn" onclick="window.location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title" id="kd-nav-title">知识详情</span>
</div>
<div class="scroll-content" style="padding:16px;">
<div id="kd-content" style="background:#fff;border-radius:16px;padding:20px;box-shadow:0 1px 6px rgba(0,0,0,0.06);"></div>
<div style="height:16px;"></div>
</div>
</div>
<script>
/* 营养知识库数据集合 */
var KB_DATA = {
'pyramid': {
icon: '🥗', bg: '#f6ffed', title: '中国居民膳食宝塔(2022', cat: '膳食营养', date: '2026-03-15',
summary: '膳食宝塔是中国居民平衡膳食的指导框架,将食物分为五大类,按推荐摄入量由多到少排列,是制定食谱与营养指导的基础依据。',
items: [
{ color: '#52c41a', name: '谷薯类', desc: '每日 250-400g,其中全谷物和杂豆 50-150g,薯类 50-100g。谷类是膳食能量的主要来源,建议粗细搭配。' },
{ color: '#1677ff', name: '蔬菜水果', desc: '每日蔬菜 300-500g(深色蔬菜占一半),水果 200-350g。保证维生素、矿物质和膳食纤维摄入。' },
{ color: '#fa8c16', name: '畜禽鱼蛋', desc: '每日畜禽肉 40-75g、水产品 40-75g、蛋类 40-50g。优先选择鱼禽,减少肥肉和加工肉制品。' },
{ color: '#722ed1', name: '奶豆坚果', desc: '每日奶及奶制品 300-500g,大豆及坚果 25-35g。是优质蛋白和钙的重要来源。' }
]
},
'macro': {
icon: '🍚', bg: '#e6f4ff', title: '三大营养素摄入指南', cat: '膳食营养', date: '2026-03-10',
summary: '蛋白质、脂肪、碳水化合物是人体必需的三大产能营养素,合理配比是维持健康体重和代谢功能的关键。',
items: [
{ color: '#1677ff', name: '碳水化合物', desc: '供能比 50-65%。优先选择全谷物、杂豆、薯类等复合碳水,限制添加糖摄入(每日<25g)。' },
{ color: '#52c41a', name: '蛋白质', desc: '供能比 15-20%。优质蛋白来源包括鱼、禽、蛋、奶、豆制品。轻体力成人每日约 1.0g/kg 体重。' },
{ color: '#fa8c16', name: '脂肪', desc: '供能比 20-30%。优先不饱和脂肪酸(橄榄油、坚果、鱼类),限制饱和脂肪(<10%)和反式脂肪(<1%)。' }
]
},
'vitamin': {
icon: '🍎', bg: '#fff7e6', title: '维生素与矿物质需求', cat: '膳食营养', date: '2026-03-05',
summary: '维生素和矿物质虽然需求量小,却是维持正常生理功能不可或缺的微量营养素,缺乏时会出现特定症状。',
items: [
{ color: '#fa8c16', name: '维生素 C', desc: '每日 100mg。来源:新鲜果蔬(猕猴桃、橙子、青椒)。缺乏可致牙龈出血、伤口愈合不良。' },
{ color: '#1677ff', name: '钙', desc: '每日 800-1000mg。来源:奶制品、豆制品、绿叶菜。缺乏可致骨质疏松、肌肉痉挛。' },
{ color: '#ff4d4f', name: '铁', desc: '男性每日 12mg、女性 20mg。来源:动物肝脏、瘦肉、动物血。缺乏可致缺铁性贫血。' }
]
},
'fatloss': {
icon: '🏃', bg: '#f6ffed', title: '科学减脂饮食原则', cat: '减脂控糖', date: '2026-02-28',
summary: '减脂的核心是热量缺口,但必须在保证营养均衡的前提下进行,避免极端节食导致肌肉流失和代谢下降。',
items: [
{ color: '#52c41a', name: '热量缺口', desc: '每日热量缺口 300-500kcal,减重速度以每周 0.5-1kg 为宜。避免过度节食(不低于基础代谢)。' },
{ color: '#1677ff', name: '高蛋白饮食', desc: '蛋白质供能比可提高至 20-30%,增加饱腹感并减少肌肉流失。选择鸡胸、鱼虾、蛋白、豆制品。' },
{ color: '#fa8c16', name: '控制精制碳水', desc: '减少白米饭、白面包等精制碳水,替换为全谷物、杂豆,稳定血糖、增强饱腹感。' }
]
},
'lowgi': {
icon: '🍬', bg: '#e6f4ff', title: '低 GI 饮食控糖策略', cat: '减脂控糖', date: '2026-02-20',
summary: '低升糖指数(GI)饮食可延缓血糖上升速度,是糖尿病管理和预防的重要饮食策略。',
items: [
{ color: '#1677ff', name: '食物选择', desc: '优先低 GI 食物:全谷物、豆类、大部分蔬菜、低糖水果。避免精制米面、含糖饮料、糕点。' },
{ color: '#52c41a', name: '进餐顺序', desc: '先吃蔬菜和蛋白质,再吃主食,可显著降低餐后血糖峰值,改善血糖控制。' },
{ color: '#fa8c16', name: '烹饪方式', desc: '主食不要煮得过烂,粗细搭配;避免勾芡、油炸;控制每餐主食总量,少量多餐。' }
]
},
'hyperlipid': {
icon: '🫀', bg: '#fff7e6', title: '高血脂饮食管理', cat: '慢性病营养', date: '2026-03-01',
summary: '高血脂是动脉粥样硬化的重要危险因素,饮食管理重点是限制饱和脂肪和反式脂肪,增加膳食纤维。',
items: [
{ color: '#ff4d4f', name: '限制饱和脂肪', desc: '每日饱和脂肪<总能量10%。减少肥肉、动物内脏、黄油、椰子油,选择瘦肉和低脂奶制品。' },
{ color: '#fa8c16', name: '杜绝反式脂肪', desc: '避免人造黄油、植脂末、油炸食品、起酥点心。反式脂肪显著升高 LDL-C,增加心血管风险。' },
{ color: '#52c41a', name: '增加膳食纤维', desc: '每日 25-30g 膳食纤维。多吃燕麦、豆类、蔬菜、水果,可降低胆固醇吸收。' }
]
},
'uric': {
icon: '🦴', bg: '#fff1f0', title: '高尿酸人群饮食禁忌', cat: '慢性病营养', date: '2026-02-15',
summary: '高尿酸血症是痛风的生化基础,通过限制高嘌呤食物、增加水分摄入可有效降低尿酸水平。',
items: [
{ color: '#ff4d4f', name: '限制高嘌呤食物', desc: '避免动物内脏、浓肉汤、海鲜(沙丁鱼、贝类)、啤酒。嘌呤>150mg/100g 的食物应严格限制。' },
{ color: '#fa8c16', name: '低嘌呤食物选择', desc: '优先选择蛋类、奶类、蔬菜、水果、谷物。肉类焯水后再烹饪可减少嘌呤含量。' },
{ color: '#1677ff', name: '多饮水', desc: '每日饮水 2000ml 以上,促进尿酸排泄。避免含糖饮料和酒精,尤其是啤酒和白酒。' }
]
},
'safety': {
icon: '🧪', bg: '#f6ffed', title: '食品添加剂使用规范', cat: '食品安全', date: '2026-02-25',
summary: '食品添加剂在合规使用范围内是安全的,了解常见添加剂的用途和限量有助于科学评估食品安全。',
items: [
{ color: '#52c41a', name: '防腐剂', desc: '如苯甲酸钠、山梨酸钾。用于抑制微生物生长,延长保质期。在规定限量内使用安全。' },
{ color: '#1677ff', name: '抗氧化剂', desc: '如 BHA、BHT、维生素 E。防止油脂氧化酸败,常用于食用油和坚果制品。' },
{ color: '#fa8c16', name: '着色剂', desc: '如焦糖色、胭脂红。用于改善食品色泽。天然色素(姜黄、红曲)安全性更高。' }
]
},
'trace': {
icon: '🔍', bg: '#e6f4ff', title: '食材溯源与质量安全', cat: '食品安全', date: '2026-02-10',
summary: '食材溯源是通过溯源码记录食材从种植养殖、加工、运输到餐桌的全链路信息,保障食品安全可追溯。',
items: [
{ color: '#1677ff', name: '溯源码体系', desc: '每一批次食材赋予唯一溯源码,关联产地、检测报告、流转节点,实现一物一码、全程可查。' },
{ color: '#52c41a', name: '质量检测节点', desc: '毛菜入库时进行农残/重金属/微生物检测,净菜加工后二次质检,合格品方可进入厨房。' },
{ color: '#fa8c16', name: '异常处置', desc: '检测不合格的食材立即隔离并追溯同批次产品,防止问题食材流入餐桌。' }
]
}
};
/* 页面加载时根据URL参数渲染知识详情 */
function renderDetail() {
var params = new URLSearchParams(window.location.search);
var key = params.get('key') || 'pyramid';
var d = KB_DATA[key];
if (!d) d = KB_DATA['pyramid'];
document.getElementById('kd-nav-title').textContent = d.title;
var itemsHtml = d.items.map(function(it) {
return '<div style="background:#f7f8fa;border-radius:10px;padding:14px;margin-bottom:12px;">' +
'<div style="font-size:13px;font-weight:600;color:' + it.color + ';margin-bottom:6px;">' + it.name + '</div>' +
'<div style="font-size:13px;color:#444;line-height:1.7;">' + it.desc + '</div>' +
'</div>';
}).join('');
document.getElementById('kd-content').innerHTML =
'<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid #f5f5f5;">' +
'<div style="width:48px;height:48px;border-radius:12px;background:' + d.bg + ';display:flex;align-items:center;justify-content:center;font-size:24px;">' + d.icon + '</div>' +
'<div><div style="font-size:17px;font-weight:700;color:#1a1a1a;">' + d.title + '</div><div style="font-size:12px;color:#999;margin-top:3px;">' + d.cat + ' · 更新于 ' + d.date + '</div></div>' +
'</div>' +
'<div style="font-size:15px;font-weight:700;color:#1a1a1a;margin-bottom:8px;">概述</div>' +
'<div style="font-size:14px;color:#444;line-height:1.8;margin-bottom:16px;">' + d.summary + '</div>' +
'<div style="font-size:15px;font-weight:700;color:#1a1a1a;margin-bottom:8px;">主要内容</div>' +
itemsHtml;
}
renderDetail();
</script>
</body>
</html>
@@ -0,0 +1,153 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
.sub-tabs { display: flex; gap: 8px; padding: 10px 16px; background: #fff; flex-shrink: 0; }
.sub-tab { font-size: 12px; padding: 4px 14px; border-radius: 16px; background: #f5f5f5; color: #666; cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.sub-tab.active { background: #e6f4ff; color: #1677ff; font-weight: 600; }
.tab-bar { height: 83px; background: #fff; border-top: 1px solid #e8e8e8; display: flex; align-items: flex-start; padding-top: 8px; flex-shrink: 0; }
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; padding: 4px 0; position: relative; text-decoration: none; }
.tab-item .tab-icon { font-size: 22px; line-height: 1; }
.tab-item .tab-label { font-size: 10px; color: #999; font-weight: 500; }
.tab-item.active .tab-label { color: #1677ff; }
.tab-badge { position: absolute; top: 2px; right: 18px; background: #ff4d4f; color: #fff; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; display: flex; align-items: center; justify-content: center; padding: 0 4px; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 蓝色渐变头部 -->
<div style="background:linear-gradient(135deg,#1677ff,#0958d9);padding:16px 20px 20px;flex-shrink:0;">
<div style="color:rgba(255,255,255,0.75);font-size:12px;margin-bottom:2px;">营养专业参考</div>
<div style="color:#fff;font-size:20px;font-weight:700;">知识库</div>
</div>
<!-- 子分类Tab -->
<div class="sub-tabs">
<div class="sub-tab active" onclick="switchKbTab(this,'kb-diet')">膳食营养</div>
<div class="sub-tab" onclick="switchKbTab(this,'kb-weight')">减脂控糖</div>
<div class="sub-tab" onclick="switchKbTab(this,'kb-chronic')">慢性病营养</div>
<div class="sub-tab" onclick="switchKbTab(this,'kb-safe')">食品安全</div>
</div>
<div class="scroll-content">
<!-- 膳食营养 -->
<div id="kb-diet">
<div style="padding:12px 16px 0;">
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('pyramid')">
<div style="width:44px;height:44px;border-radius:10px;background:#f6ffed;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🥗</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">中国居民膳食宝塔(2022</div><div style="font-size:12px;color:#999;margin-top:3px;">五大类食物推荐摄入量与搭配原则</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('macro')">
<div style="width:44px;height:44px;border-radius:10px;background:#e6f4ff;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🍚</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">三大营养素摄入指南</div><div style="font-size:12px;color:#999;margin-top:3px;">蛋白质、脂肪、碳水化合物的合理配比</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('vitamin')">
<div style="width:44px;height:44px;border-radius:10px;background:#fff7e6;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🍎</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">维生素与矿物质需求</div><div style="font-size:12px;color:#999;margin-top:3px;">常见微量营养素的食物来源与缺乏症状</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 减脂控糖 -->
<div id="kb-weight" style="display:none;">
<div style="padding:12px 16px 0;">
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('fatloss')">
<div style="width:44px;height:44px;border-radius:10px;background:#f6ffed;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🏃</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">科学减脂饮食原则</div><div style="font-size:12px;color:#999;margin-top:3px;">热量缺口与营养均衡的减脂方案</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('lowgi')">
<div style="width:44px;height:44px;border-radius:10px;background:#e6f4ff;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🍬</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">低 GI 饮食控糖策略</div><div style="font-size:12px;color:#999;margin-top:3px;">低升糖指数食物选择与餐序调整</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 慢性病营养 -->
<div id="kb-chronic" style="display:none;">
<div style="padding:12px 16px 0;">
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('hyperlipid')">
<div style="width:44px;height:44px;border-radius:10px;background:#fff7e6;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🫀</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">高血脂饮食管理</div><div style="font-size:12px;color:#999;margin-top:3px;">限制饱和脂肪与反式脂肪的摄入</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('uric')">
<div style="width:44px;height:44px;border-radius:10px;background:#fff1f0;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🦴</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">高尿酸人群饮食禁忌</div><div style="font-size:12px;color:#999;margin-top:3px;">嘌呤含量分级与低嘌呤饮食建议</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 食品安全 -->
<div id="kb-safe" style="display:none;">
<div style="padding:12px 16px 0;">
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('safety')">
<div style="width:44px;height:44px;border-radius:10px;background:#f6ffed;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🧪</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">食品添加剂使用规范</div><div style="font-size:12px;color:#999;margin-top:3px;">常见添加剂的安全性与使用限量</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div style="background:#fff;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06);margin-bottom:10px;cursor:pointer;" onclick="goDetail('trace')">
<div style="width:44px;height:44px;border-radius:10px;background:#e6f4ff;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🔍</div>
<div style="flex:1;"><div style="font-size:14px;font-weight:600;color:#1a1a1a;">食材溯源与质量安全</div><div style="font-size:12px;color:#999;margin-top:3px;">从田间到餐桌的全链路安全把控</div></div>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
</div>
<div style="height:16px;"></div>
</div>
</div>
<!-- 底部Tab栏 -->
<div class="tab-bar">
<a class="tab-item" href="../workbench/index.html"><span class="tab-icon">🏥</span><span class="tab-label">工作台</span><span class="tab-badge">2</span></a>
<a class="tab-item active" href="./index.html"><span class="tab-icon">📚</span><span class="tab-label">知识库</span></a>
<a class="tab-item" href="../../ai-assistant/index.html"><span class="tab-icon">🤖</span><span class="tab-label">AI助手</span></a>
<a class="tab-item" href="../../message/index.html"><span class="tab-icon">💬</span><span class="tab-label">消息</span><span class="tab-badge">2</span></a>
<a class="tab-item" href="../profile/index.html"><span class="tab-icon">👤</span><span class="tab-label">我的</span></a>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* 切换知识库分类Tab */
function switchKbTab(el, panelId) {
el.closest('.sub-tabs').querySelectorAll('.sub-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
['kb-diet','kb-weight','kb-chronic','kb-safe'].forEach(function(id) {
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
});
}
/* 跳转到知识详情页 */
function goDetail(key) {
window.location.href = './detail.html?key=' + key;
}
</script>
</body>
</html>
@@ -0,0 +1,82 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.back-btn { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
.btn-primary { background: #1677ff; color: #fff; border: none; border-radius: 10px; padding: 12px 24px; font-size: 15px; font-weight: 600; cursor: pointer; width: 100%; }
.modal-center { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; align-items: center; justify-content: center; }
.modal-center.active { display: flex; }
.modal-box { width: 280px; background: #fff; border-radius: 16px; overflow: hidden; }
.modal-box .modal-title { padding: 20px 20px 8px; font-size: 17px; font-weight: 700; text-align: center; color: #1a1a1a; }
.modal-box .modal-body { padding: 8px 20px 20px; font-size: 14px; color: #666; text-align: center; line-height: 1.6; }
.modal-box .modal-actions { display: flex; border-top: 1px solid #f0f0f0; }
.modal-box .modal-actions button { flex: 1; padding: 14px; font-size: 16px; cursor: pointer; background: none; border: none; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<div class="nav-bar">
<button class="back-btn" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">修改密码</span>
</div>
<div class="scroll-content" style="padding:16px;">
<div style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:20px;">
<div style="display:flex;align-items:center;padding:14px 16px;border-bottom:1px solid #f5f5f5;">
<span style="font-size:14px;color:#999;width:80px;flex-shrink:0;">当前密码</span>
<input type="password" placeholder="请输入当前密码" style="flex:1;border:none;font-size:14px;color:#1a1a1a;outline:none;text-align:right;" />
</div>
<div style="display:flex;align-items:center;padding:14px 16px;border-bottom:1px solid #f5f5f5;">
<span style="font-size:14px;color:#999;width:80px;flex-shrink:0;">新密码</span>
<input type="password" placeholder="请输入新密码" style="flex:1;border:none;font-size:14px;color:#1a1a1a;outline:none;text-align:right;" />
</div>
<div style="display:flex;align-items:center;padding:14px 16px;">
<span style="font-size:14px;color:#999;width:80px;flex-shrink:0;">确认新密码</span>
<input type="password" placeholder="再次输入新密码" style="flex:1;border:none;font-size:14px;color:#1a1a1a;outline:none;text-align:right;" />
</div>
</div>
<button onclick="openModal('modal-pwd-saved')" class="btn-primary">保存修改</button>
</div>
<div id="modal-pwd-saved" class="modal-center">
<div class="modal-box">
<div class="modal-title">修改成功</div>
<div class="modal-body">密码已更新,下次登录请使用新密码。</div>
<div class="modal-actions">
<button onclick="closeModal('modal-pwd-saved');location.href='./index.html'" style="color:#1677ff;font-weight:600;flex:1;padding:14px;font-size:16px;cursor:pointer;background:none;border:none;">确定</button>
</div>
</div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
function openModal(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
function closeModal(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
</script>
</body>
</html>
@@ -0,0 +1,92 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.back-btn { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.nav-right { margin-left: auto; color: #1677ff; font-size: 14px; cursor: pointer; background: none; border: none; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<div class="nav-bar">
<button class="back-btn" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">个人信息设置</span>
<button class="nav-right">保存</button>
</div>
<div class="scroll-content" style="padding:16px;">
<!-- 头像区 -->
<div style="background:#fff;border-radius:12px;padding:20px;margin-bottom:12px;text-align:center;box-shadow:0 1px 6px rgba(0,0,0,0.06);">
<div style="position:relative;display:inline-block;">
<div style="width:80px;height:80px;border-radius:50%;background:linear-gradient(135deg,#e8f4ff,#bdd7ff);display:flex;align-items:center;justify-content:center;font-size:40px;">🥗</div>
<div style="position:absolute;bottom:0;right:0;width:26px;height:26px;background:#1677ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;border:2px solid #fff;cursor:pointer;">📷</div>
</div>
<div style="font-size:13px;color:#999;margin-top:10px;">点击更换头像</div>
</div>
<!-- 擅长领域 -->
<div style="font-size:13px;font-weight:600;color:#999;margin:0 4px 8px;">擅长领域</div>
<div style="background:#fff;border-radius:12px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:16px;">
<div style="display:flex;flex-wrap:wrap;gap:8px;">
<div onclick="toggleTag(this)" style="padding:6px 14px;border-radius:16px;font-size:13px;background:#1677ff;color:#fff;border:1px solid #1677ff;cursor:pointer;">减脂</div>
<div onclick="toggleTag(this)" style="padding:6px 14px;border-radius:16px;font-size:13px;background:#1677ff;color:#fff;border:1px solid #1677ff;cursor:pointer;">控糖</div>
<div onclick="toggleTag(this)" style="padding:6px 14px;border-radius:16px;font-size:13px;background:#1677ff;color:#fff;border:1px solid #1677ff;cursor:pointer;">增肌</div>
<div onclick="toggleTag(this)" style="padding:6px 14px;border-radius:16px;font-size:13px;background:#f5f5f5;color:#666;border:1px solid #e8e8e8;cursor:pointer;">高血脂</div>
<div onclick="toggleTag(this)" style="padding:6px 14px;border-radius:16px;font-size:13px;background:#f5f5f5;color:#666;border:1px solid #e8e8e8;cursor:pointer;">高尿酸</div>
<div onclick="toggleTag(this)" style="padding:6px 14px;border-radius:16px;font-size:13px;background:#f5f5f5;color:#666;border:1px solid #e8e8e8;cursor:pointer;">术后营养</div>
<div onclick="toggleTag(this)" style="padding:6px 14px;border-radius:16px;font-size:13px;background:#f5f5f5;color:#666;border:1px solid #e8e8e8;cursor:pointer;">孕期营养</div>
</div>
<div style="font-size:11px;color:#bbb;margin-top:10px;">点击标签切换选中状态</div>
</div>
<!-- 个人简介 -->
<div style="font-size:13px;font-weight:600;color:#999;margin:0 4px 8px;">个人简介</div>
<div style="background:#fff;border-radius:12px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:16px;">
<textarea style="width:100%;border:none;font-size:14px;color:#1a1a1a;outline:none;resize:none;line-height:1.7;background:transparent;" rows="4" placeholder="请输入个人简介...">注册营养师,从事营养健康管理工作 8 年,擅长体重管理、慢性病营养干预与个性化食谱制定,累计服务员工 2000 余人次。</textarea>
<div style="text-align:right;font-size:11px;color:#bbb;">已输入 68/300 字</div>
</div>
<!-- 职业经历 -->
<div style="font-size:13px;font-weight:600;color:#999;margin:0 4px 8px;">职业经历</div>
<div style="background:#fff;border-radius:12px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:20px;">
<textarea style="width:100%;border:none;font-size:14px;color:#1a1a1a;outline:none;resize:none;line-height:1.7;background:transparent;" rows="4" placeholder="请描述您的职业经历..."></textarea>
<div style="text-align:right;font-size:11px;color:#bbb;">已输入 62/300 字</div>
</div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* 切换擅长领域标签选中状态 */
function toggleTag(el) {
var isOn = el.style.background === 'rgb(22, 119, 255)' || el.style.background === '#1677ff';
el.style.background = isOn ? '#f5f5f5' : '#1677ff';
el.style.color = isOn ? '#666' : '#fff';
el.style.borderColor = isOn ? '#e8e8e8' : '#1677ff';
}
</script>
</body>
</html>
@@ -0,0 +1,155 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
.modal-center { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; align-items: center; justify-content: center; }
.modal-center.active { display: flex; }
.modal-box { width: 280px; background: #fff; border-radius: 16px; overflow: hidden; }
.modal-box .modal-title { padding: 20px 20px 8px; font-size: 17px; font-weight: 700; text-align: center; color: #1a1a1a; }
.modal-box .modal-body { padding: 8px 20px 20px; font-size: 14px; color: #666; text-align: center; line-height: 1.6; }
.modal-box .modal-actions { display: flex; border-top: 1px solid #f0f0f0; }
.modal-box .modal-actions button { flex: 1; padding: 14px; font-size: 16px; cursor: pointer; background: none; border: none; }
.modal-box .modal-actions button:first-child { color: #666; border-right: 1px solid #f0f0f0; }
.modal-box .modal-actions button:last-child { color: #1677ff; font-weight: 600; }
.tab-bar { height: 83px; background: #fff; border-top: 1px solid #e8e8e8; display: flex; align-items: flex-start; padding-top: 8px; flex-shrink: 0; }
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; padding: 4px 0; position: relative; }
.tab-item .tab-icon { font-size: 22px; line-height: 1; }
.tab-item .tab-label { font-size: 10px; color: #999; font-weight: 500; }
.tab-item.active .tab-label { color: #1677ff; }
.tab-badge { position: absolute; top: 2px; right: 18px; background: #ff4d4f; color: #fff; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; display: flex; align-items: center; justify-content: center; padding: 0 4px; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar" style="background:transparent;position:absolute;top:0;left:0;right:0;z-index:10;">
<span class="time" style="color:#fff;">9:41</span>
<div class="icons" style="color:#fff;"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 蓝色渐变头部 -->
<div style="background:linear-gradient(135deg,#1677ff,#0958d9);padding:30px 20px 24px;flex-shrink:0;padding-top:60px;">
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:68px;height:68px;border-radius:50%;background:linear-gradient(135deg,#e8f4ff,#bdd7ff);display:flex;align-items:center;justify-content:center;font-size:34px;border:3px solid rgba(255,255,255,0.5);">🥗</div>
<div>
<div style="color:#fff;font-size:20px;font-weight:700;">李静</div>
<div style="color:rgba(255,255,255,0.8);font-size:13px;margin-top:3px;">注册营养师</div>
<div style="color:rgba(255,255,255,0.65);font-size:12px;margin-top:2px;">健康管理科</div>
</div>
<!-- 服务中按钮 -->
<button onclick="togglePause()" id="pause-btn-mine" style="margin-left:auto;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,0.2);border:1px solid rgba(255,255,255,0.4);border-radius:20px;padding:6px 12px;color:#fff;font-size:12px;cursor:pointer;">
<span style="width:7px;height:7px;border-radius:50%;background:#52c41a;display:inline-block;"></span><span id="pause-text">咨询已开启</span>
</button>
</div>
</div>
<!-- 功能列表滚动区域 -->
<div class="scroll-content" style="padding:12px 16px;">
<div style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:12px;">
<div onclick="location.href='./stats.html'" style="display:flex;align-items:center;padding:16px;cursor:pointer;border-bottom:1px solid #f5f5f5;">
<span style="font-size:18px;margin-right:12px;">📊</span><span style="font-size:15px;color:#1a1a1a;flex:1;">营养咨询统计</span>
<span style="font-size:13px;color:#1677ff;margin-right:8px;">今日 12 次</span>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div onclick="location.href='./schedule.html'" style="display:flex;align-items:center;padding:16px;cursor:pointer;border-bottom:1px solid #f5f5f5;">
<span style="font-size:18px;margin-right:12px;">📅</span><span style="font-size:15px;color:#1a1a1a;flex:1;">排班设置</span>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div onclick="location.href='./edit-profile.html'" style="display:flex;align-items:center;padding:16px;cursor:pointer;">
<span style="font-size:18px;margin-right:12px;">👤</span><span style="font-size:15px;color:#1a1a1a;flex:1;">个人信息设置</span>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
</div>
<div style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 6px rgba(0,0,0,0.06);margin-bottom:12px;">
<div onclick="location.href='./change-pwd.html'" style="display:flex;align-items:center;padding:16px;border-bottom:1px solid #f5f5f5;cursor:pointer;">
<span style="font-size:18px;margin-right:12px;">🔒</span><span style="font-size:15px;color:#1a1a1a;flex:1;">修改密码</span>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
<div style="display:flex;align-items:center;padding:16px;">
<span style="font-size:18px;margin-right:12px;"></span><span style="font-size:15px;color:#1a1a1a;flex:1;">关于我们</span>
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="#ccc" stroke-width="1.5"><path d="M1 1l5 5-5 5"/></svg>
</div>
</div>
<button onclick="openModal('modal-logout')" style="width:100%;padding:14px;background:#fff;color:#ff4d4f;border:none;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;box-shadow:0 1px 6px rgba(0,0,0,0.06);">退出登录</button>
</div>
<!-- 退出确认弹窗 -->
<div id="modal-logout" class="modal-center">
<div class="modal-box">
<div class="modal-title">退出登录</div>
<div class="modal-body">确认退出当前账号?</div>
<div class="modal-actions">
<button onclick="closeModal('modal-logout')">取消</button>
<button onclick="closeModal('modal-logout')" style="color:#ff4d4f;">确认退出</button>
</div>
</div>
</div>
<!-- 咨询状态切换确认弹窗 -->
<div id="modal-pause" class="modal-center">
<div class="modal-box">
<div class="modal-title" id="pause-modal-title">关闭咨询</div>
<div class="modal-body" id="pause-modal-body">是否要关闭营养咨询?关闭后,员工无法发起营养咨询。</div>
<div class="modal-actions">
<button onclick="closeModal('modal-pause')">取消</button>
<button onclick="confirmTogglePause()">确认</button>
</div>
</div>
</div>
<!-- 底部Tab栏,我的为active状态 -->
<div class="tab-bar">
<div class="tab-item" onclick="location.href='../workbench/index.html'"><span class="tab-icon">🏥</span><span class="tab-label">工作台</span><span class="tab-badge">2</span></div>
<div class="tab-item" onclick="location.href='../knowledge/index.html'"><span class="tab-icon">📚</span><span class="tab-label">知识库</span></div>
<div class="tab-item" onclick="location.href='../../ai-assistant/index.html'"><span class="tab-icon">🤖</span><span class="tab-label">AI助手</span></div>
<div class="tab-item" onclick="location.href='../../message/index.html'"><span class="tab-icon">💬</span><span class="tab-label">消息</span><span class="tab-badge">2</span></div>
<div class="tab-item active" onclick="location.href='./index.html'"><span class="tab-icon">👤</span><span class="tab-label">我的</span></div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
let isPaused = false;
function togglePause() {
var title = isPaused ? '开启咨询' : '关闭咨询';
var body = isPaused ? '是否要开启营养咨询?开启后,员工可发起营养咨询。' : '是否要关闭营养咨询?关闭后,员工无法发起营养咨询。';
document.getElementById('pause-modal-title').textContent = title;
document.getElementById('pause-modal-body').textContent = body;
openModal('modal-pause');
}
function confirmTogglePause() {
isPaused = !isPaused;
var bg = isPaused ? '#ff4d4f' : '#52c41a';
var text = isPaused ? '咨询已关闭' : '咨询已开启';
var mineBtn = document.getElementById('pause-btn-mine');
if (mineBtn) {
var dot = mineBtn.querySelector('span');
if (dot) dot.style.background = bg;
var pauseText = document.getElementById('pause-text');
if (pauseText) pauseText.textContent = text;
}
closeModal('modal-pause');
}
function openModal(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
function closeModal(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
</script>
</body>
</html>
@@ -0,0 +1,183 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.back-btn { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.nav-right { margin-left: auto; color: #1677ff; font-size: 14px; cursor: pointer; background: none; border: none; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
.toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; background: #e0e0e0; cursor: pointer; transition: background .2s; flex-shrink: 0; }
.toggle.on { background: #4cd964; }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2); transition: transform .2s; }
.toggle.on::after { transform: translateX(20px); }
.day-card { background: #fff; border-radius: 12px; margin: 0 16px 10px; box-shadow: 0 1px 4px rgba(0,0,0,0.05); overflow: hidden; }
.day-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.day-header .day-name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.day-header .day-summary { font-size: 12px; color: #999; }
.day-header .arrow { width: 16px; height: 16px; color: #ccc; transition: transform .2s; flex-shrink: 0; }
.day-header .arrow.open { transform: rotate(180deg); }
.day-body { max-height: 0; overflow: hidden; transition: max-height .25s ease; }
.day-body.open { max-height: 400px; }
.day-body-inner { padding: 0 16px 14px; }
.slot-row { display: flex; align-items: center; padding: 10px 0; border-top: 1px solid #f5f5f5; }
.slot-row:first-child { border-top: none; }
.slot-info { flex: 1; min-width: 0; }
.slot-period { font-size: 11px; color: #1677ff; font-weight: 500; margin-bottom: 1px; }
.slot-time { font-size: 13px; color: #1a1a1a; }
.slot-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.slot-toggle { position: relative; width: 40px; height: 22px; border-radius: 11px; background: #e0e0e0; cursor: pointer; transition: background .2s; }
.slot-toggle.on { background: #4cd964; }
.slot-toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.2); transition: transform .2s; }
.slot-toggle.on::after { transform: translateX(18px); }
.slot-count { display: flex; align-items: center; gap: 4px; }
.count-btn { width: 26px; height: 26px; border-radius: 50%; border: 1px solid #e8e8e8; background: #fff; font-size: 15px; color: #666; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1; }
.count-btn:active { background: #f5f5f5; }
.count-val { font-size: 15px; font-weight: 700; color: #1677ff; min-width: 18px; text-align: center; }
.count-unit { font-size: 11px; color: #999; }
.slot-row.disabled .slot-time { color: #ccc; }
.slot-row.disabled .slot-period { color: #b8d4ff; }
.slot-row.disabled .count-val { color: #ccc; }
.slot-row.disabled .count-btn { border-color: #f0f0f0; color: #ccc; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<div class="nav-bar">
<button class="back-btn" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">排班设置</span>
<button class="nav-right">保存</button>
</div>
<div class="scroll-content">
<!-- 开启营养咨询 -->
<div style="background:#fff;padding:14px 16px;display:flex;align-items:center;gap:10px;margin-bottom:10px;">
<span style="font-size:14px;color:#1a1a1a;font-weight:500;">开启营养咨询</span>
<div class="toggle on" onclick="this.classList.toggle('on');var t=this.nextElementSibling;t.textContent=this.classList.contains('on')?'已开启 — 员工可发起营养咨询':'已关闭';t.style.color=this.classList.contains('on')?'#4cd964':'#999';"></div>
<span style="font-size:12px;color:#4cd964;flex:1;">已开启 — 员工可发起营养咨询</span>
</div>
<!-- 标题 -->
<div style="padding:2px 16px 10px;display:flex;align-items:center;gap:6px;">
<span style="width:3px;height:14px;background:#1677ff;border-radius:2px;display:inline-block;"></span>
<span style="font-size:14px;font-weight:600;color:#1a1a1a;">每周排班设置</span>
</div>
<div id="cardList"></div>
<div style="height:20px;"></div>
</div>
</div>
<script>
var DAYS = ['周一','周二','周三','周四','周五','周六','周日'];
var SLOTS = [
{ period: '上午', time: '8:00 10:00' },
{ period: '上午', time: '10:00 12:00' },
{ period: '下午', time: '14:00 16:00' },
{ period: '下午', time: '16:00 18:00' }
];
var DATA = DAYS.map(function(_, i) {
var off = i >= 5;
return { slots: SLOTS.map(function(_, si) {
return { on: !off, n: off ? 0 : (si < 2 ? 5 : 4) };
})};
});
var arrowSvg = '<svg class="arrow" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6l4 4 4-4"/></svg>';
function getSummary(di) {
var d = DATA[di], cnt = 0;
d.slots.forEach(function(s) { if (s.on) cnt++; });
return cnt > 0 ? cnt + '个时段开诊' : '休息';
}
function render() {
var wrap = document.getElementById('cardList');
wrap.innerHTML = '';
DAYS.forEach(function(day, di) {
var card = document.createElement('div');
card.className = 'day-card';
card.innerHTML =
'<div class="day-header" onclick="toggle(' + di + ')">' +
'<div><span class="day-name">' + day + '</span> ' +
'<span class="day-summary">' + getSummary(di) + '</span></div>' +
arrowSvg +
'</div>' +
'<div class="day-body" id="body-' + di + '">' +
'<div class="day-body-inner">' + renderSlots(di) + '</div>' +
'</div>';
wrap.appendChild(card);
});
var first = document.getElementById('body-0');
if (first) { first.classList.add('open'); first.previousElementSibling.querySelector('.arrow').classList.add('open'); }
}
function renderSlots(di) {
var html = '';
DATA[di].slots.forEach(function(s, si) {
var dis = s.on ? '' : ' disabled';
var tCls = s.on ? 'slot-toggle on' : 'slot-toggle';
html += '<div class="slot-row' + dis + '">' +
'<div class="slot-info">' +
'<div class="slot-period">' + SLOTS[si].period + '</div>' +
'<div class="slot-time">' + SLOTS[si].time + '</div>' +
'</div>' +
'<div class="slot-right">' +
'<div class="' + tCls + '" onclick="toggleSlot(' + di + ',' + si + ')"></div>' +
'<div class="slot-count">' +
'<button class="count-btn" ' + (s.on ? '' : 'disabled ') + 'onclick="adj(' + di + ',' + si + ',-1)"></button>' +
'<span class="count-val">' + (s.on ? s.n : '—') + '</span>' +
'<button class="count-btn" ' + (s.on ? '' : 'disabled ') + 'onclick="adj(' + di + ',' + si + ',1)">+</button>' +
'<span class="count-unit">人</span>' +
'</div>' +
'</div></div>';
});
return html;
}
function toggle(di) {
var body = document.getElementById('body-' + di);
var arrow = body.previousElementSibling.querySelector('.arrow');
body.classList.toggle('open');
arrow.classList.toggle('open');
}
function toggleSlot(di, si) {
var s = DATA[di].slots[si];
s.on = !s.on;
if (s.on && s.n === 0) s.n = 3;
refreshCard(di);
}
function adj(di, si, delta) {
var s = DATA[di].slots[si];
if (!s.on) return;
s.n = Math.max(1, Math.min(20, s.n + delta));
refreshCard(di);
}
function refreshCard(di) {
var body = document.getElementById('body-' + di);
var wasOpen = body.classList.contains('open');
body.querySelector('.day-body-inner').innerHTML = renderSlots(di);
var header = body.previousElementSibling;
header.querySelector('.day-summary').textContent = getSummary(di);
if (wasOpen) body.classList.add('open');
}
render();
</script>
</body>
</html>
@@ -0,0 +1,143 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.back-btn { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<div class="nav-bar">
<button class="back-btn" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">营养咨询统计</span>
</div>
<div class="scroll-content" style="padding:16px;">
<!-- 月度统计 -->
<div style="background:#fff;border-radius:12px;padding:20px;margin-bottom:16px;box-shadow:0 1px 6px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;justify-content:center;gap:16px;margin-bottom:20px;">
<button onclick="prevMonth()" style="background:none;border:none;color:#1677ff;font-size:18px;cursor:pointer;padding:4px 8px;">
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 1L1 7l6 6"/></svg>
</button>
<div style="font-size:17px;font-weight:600;color:#1a1a1a;min-width:120px;text-align:center;" id="current-month">2026年9月</div>
<button onclick="nextMonth()" style="background:none;border:none;color:#1677ff;font-size:18px;cursor:pointer;padding:4px 8px;">
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 1l6 6-6 6"/></svg>
</button>
</div>
<!-- 概览(3列) -->
<div style="display:flex;gap:8px;margin-bottom:24px;">
<div style="flex:1;background:#e6f4ff;border-radius:10px;padding:14px 8px;text-align:center;">
<div style="font-size:20px;font-weight:700;color:#1677ff;">213</div>
<div style="font-size:11px;color:#666;margin-top:4px;">营养咨询</div>
</div>
<div style="flex:1;background:#f6ffed;border-radius:10px;padding:14px 8px;text-align:center;">
<div style="font-size:20px;font-weight:700;color:#52c41a;">48</div>
<div style="font-size:11px;color:#666;margin-top:4px;">营养方案</div>
</div>
<div style="flex:1;background:#fff7e6;border-radius:10px;padding:14px 8px;text-align:center;">
<div style="font-size:20px;font-weight:700;color:#fa8c16;">12</div>
<div style="font-size:11px;color:#666;margin-top:4px;">发布食谱</div>
</div>
</div>
<!-- 咨询主题分布(饼状图) -->
<div style="display:flex;align-items:center;gap:24px;margin-bottom:24px;">
<svg width="130" height="130" viewBox="0 0 100 100" style="flex-shrink:0;">
<circle cx="50" cy="50" r="40" fill="none" stroke="#1677ff" stroke-width="20"
stroke-dasharray="126 250" stroke-dashoffset="0" transform="rotate(-90 50 50)"/>
<circle cx="50" cy="50" r="40" fill="none" stroke="#52c41a" stroke-width="20"
stroke-dasharray="75 250" stroke-dashoffset="-126" transform="rotate(-90 50 50)"/>
<circle cx="50" cy="50" r="40" fill="none" stroke="#fa8c16" stroke-width="20"
stroke-dasharray="49 250" stroke-dashoffset="-201" transform="rotate(-90 50 50)"/>
<circle cx="50" cy="50" r="28" fill="#fff"/>
<text x="50" y="48" text-anchor="middle" font-size="18" font-weight="700" fill="#1a1a1a">213</text>
<text x="50" y="60" text-anchor="middle" font-size="10" fill="#999">总咨询</text>
</svg>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<div style="width:12px;height:12px;background:#1677ff;border-radius:3px;flex-shrink:0;"></div>
<div style="flex:1;"><span style="font-size:13px;color:#666;">减脂控糖</span></div>
<span style="font-size:15px;font-weight:700;color:#1a1a1a;">107<span style="font-size:11px;color:#999;"> (50%)</span></span>
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<div style="width:12px;height:12px;background:#52c41a;border-radius:3px;flex-shrink:0;"></div>
<div style="flex:1;"><span style="font-size:13px;color:#666;">慢性病营养</span></div>
<span style="font-size:15px;font-weight:700;color:#1a1a1a;">64<span style="font-size:11px;color:#999;"> (30%)</span></span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:12px;height:12px;background:#fa8c16;border-radius:3px;flex-shrink:0;"></div>
<div style="flex:1;"><span style="font-size:13px;color:#666;">其他</span></div>
<span style="font-size:15px;font-weight:700;color:#1a1a1a;">42<span style="font-size:11px;color:#999;"> (20%)</span></span>
</div>
</div>
</div>
<!-- 月度趋势折线图 -->
<div style="border-top:1px solid #f0f0f0;padding-top:20px;">
<div style="font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:16px;">月度趋势</div>
<svg width="100%" height="180" viewBox="0 0 340 180" style="overflow:visible;">
<line x1="0" y1="150" x2="340" y2="150" stroke="#f0f0f0" stroke-width="1"/>
<line x1="0" y1="120" x2="340" y2="120" stroke="#f0f0f0" stroke-width="1"/>
<line x1="0" y1="90" x2="340" y2="90" stroke="#f0f0f0" stroke-width="1"/>
<line x1="0" y1="60" x2="340" y2="60" stroke="#f0f0f0" stroke-width="1"/>
<line x1="0" y1="30" x2="340" y2="30" stroke="#f0f0f0" stroke-width="1"/>
<polyline points="5,126 27,108 49,90 71,60 93,138 115,48 137,96 159,78 181,114 203,66 225,132 247,90 269,120 291,54 313,144 335,42"
fill="none" stroke="#1677ff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="93" cy="138" r="3.5" fill="#1677ff"/>
<circle cx="159" cy="78" r="3.5" fill="#1677ff"/>
<circle cx="203" cy="66" r="6" fill="#1677ff" stroke="#fff" stroke-width="2.5"/>
<text x="93" y="168" text-anchor="middle" font-size="11" fill="#bbb">6</text>
<text x="159" y="168" text-anchor="middle" font-size="11" fill="#bbb">12</text>
<text x="203" y="168" text-anchor="middle" font-size="11" fill="#1677ff" font-weight="600"></text>
<text x="335" y="168" text-anchor="middle" font-size="11" fill="#bbb">30</text>
<text x="203" y="54" text-anchor="middle" font-size="12" fill="#1677ff" font-weight="600">15</text>
<text x="-5" y="153" text-anchor="end" font-size="10" fill="#bbb">0</text>
<text x="-5" y="123" text-anchor="end" font-size="10" fill="#bbb">5</text>
<text x="-5" y="93" text-anchor="end" font-size="10" fill="#bbb">10</text>
<text x="-5" y="63" text-anchor="end" font-size="10" fill="#bbb">15</text>
<text x="-5" y="33" text-anchor="end" font-size="10" fill="#bbb">20</text>
</svg>
</div>
</div>
<div style="height:20px;"></div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
function prevMonth() { alert('切换到上个月(演示)'); }
function nextMonth() { alert('切换到下个月(演示)'); }
</script>
</body>
</html>
@@ -0,0 +1,80 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.back-btn { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="back-btn" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">赵磊</span>
</div>
<!-- 咨询已结束提示条 -->
<div style="background:#fff7e6;padding:8px 16px;font-size:12px;color:#d46b08;flex-shrink:0;text-align:center;">本次营养咨询已结束,仅可查看历史记录</div>
<!-- 消息区(只读) -->
<div class="scroll-content" style="padding:12px 16px;">
<div style="text-align:center;margin:8px 0;"><span style="font-size:11px;color:#bbb;background:#ebebeb;padding:3px 10px;border-radius:10px;">昨天 08:30</span></div>
<!-- 员工消息 -->
<div style="display:flex;gap:8px;margin-bottom:14px;">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#ffe0e0,#ffb3b3);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">👨</div>
<div><div style="font-size:11px;color:#999;margin-bottom:4px;">赵磊</div>
<div style="background:#fff;border-radius:0 12px 12px 12px;padding:10px 14px;font-size:14px;color:#1a1a1a;max-width:220px;box-shadow:0 1px 4px rgba(0,0,0,0.06);line-height:1.5;">营养师您好,我最近体检尿酸偏高,日常饮食要注意什么?</div>
</div>
</div>
<!-- 营养师消息 -->
<div style="display:flex;gap:8px;margin-bottom:14px;flex-direction:row-reverse;">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#e8f4ff,#bdd7ff);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">🥗</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;">
<div style="font-size:11px;color:#999;margin-bottom:4px;">李营养师(我)</div>
<div style="background:#1677ff;border-radius:12px 0 12px 12px;padding:10px 14px;font-size:14px;color:#fff;max-width:220px;line-height:1.5;">高尿酸人群建议限制高嘌呤食物(动物内脏、海鲜、浓肉汤),避免饮酒,多饮水,每日 2000ml 以上,控制体重。</div>
</div>
</div>
<div style="text-align:center;margin:8px 0;"><span style="font-size:11px;color:#bbb;background:#ebebeb;padding:3px 10px;border-radius:10px;">昨天 08:45</span></div>
<!-- 员工结束消息 -->
<div style="display:flex;gap:8px;margin-bottom:14px;">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#ffe0e0,#ffb3b3);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">👨</div>
<div><div style="font-size:11px;color:#999;margin-bottom:4px;">赵磊</div>
<div style="background:#fff;border-radius:0 12px 12px 12px;padding:10px 14px;font-size:14px;color:#1a1a1a;max-width:220px;box-shadow:0 1px 4px rgba(0,0,0,0.06);line-height:1.5;">谢谢营养师,方案很实用,我会坚持执行。</div>
</div>
</div>
<!-- 结束标记 -->
<div style="text-align:center;margin:16px 0;">
<span style="font-size:12px;color:#bbb;">— 咨询已结束 —</span>
</div>
<div style="height:8px;"></div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
</script>
</body>
</html>
@@ -0,0 +1,328 @@
<!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: #fff; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
/* 导航栏 */
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.back-btn { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.nav-right { margin-left: auto; color: #ff4d4f; font-size: 14px; cursor: pointer; background: none; border: none; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
.modal-center { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; align-items: center; justify-content: center; }
.modal-center.active { display: flex; }
.modal-box { width: 280px; background: #fff; border-radius: 16px; overflow: hidden; }
.modal-box .modal-title { padding: 20px 20px 8px; font-size: 17px; font-weight: 700; text-align: center; color: #1a1a1a; }
.modal-box .modal-body { padding: 8px 20px 20px; font-size: 14px; color: #666; text-align: center; line-height: 1.6; }
.modal-box .modal-actions { display: flex; border-top: 1px solid #f0f0f0; }
.modal-box .modal-actions button { flex: 1; padding: 14px; font-size: 16px; cursor: pointer; background: none; border: none; }
.modal-box .modal-actions button:first-child { color: #666; border-right: 1px solid #f0f0f0; }
.modal-box .modal-actions button:last-child { color: #1677ff; font-weight: 600; }
/* 员工档案卡片 */
.bubble-record { max-width: 260px; border-radius: 0 12px 12px 12px; overflow: hidden; background: #fff; border: 1px solid #e8e8e8; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; }
.bubble-record__header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: linear-gradient(135deg, #e8f4ff, #bae0ff); }
.bubble-record__avatar { width: 32px; height: 32px; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.bubble-record__name { font-size: 13px; font-weight: 600; color: #333; }
.bubble-record__meta { font-size: 11px; color: #666; }
.bubble-record__body { padding: 10px 12px; }
.bubble-record__title { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 4px; }
.bubble-record__desc { font-size: 12px; color: #666; line-height: 1.5; }
.bubble-record__tags { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 6px; }
.bubble-record__tag { font-size: 10px; padding: 2px 6px; border-radius: 4px; }
.bubble-record__tag--green { color: #52c41a; background: #f6ffed; }
.bubble-record__tag--orange { color: #d46b08; background: #fff7e6; }
/* 常用语分类与列表 */
.quick-cat { font-size: 12px; padding: 4px 12px; border-radius: 14px; background: #f5f5f5; color: #666; cursor: pointer; user-select: none; transition: all .2s; }
.quick-cat.active { background: #e6f4ff; color: #1677ff; font-weight: 600; }
.quick-item { font-size: 13px; color: #1a1a1a; padding: 8px 0; border-bottom: 1px solid #f0f0f0; cursor: pointer; }
.quick-item:last-child { border-bottom: none; }
.quick-item:active { color: #1677ff; }
/* 快捷操作栏 */
.action-bar { display: flex; gap: 8px; background: #fff; border-bottom: 1px solid #f0f0f0; padding: 8px 16px; flex-shrink: 0; }
.action-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 0; border-radius: 8px; font-size: 13px; cursor: pointer; }
.action-btn--blue { background: #e6f4ff; color: #1677ff; }
.action-btn--green { background: #f6ffed; color: #52c41a; }
/* 底部弹层 */
.sheet-mask { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 300; align-items: flex-end; justify-content: center; }
.sheet-mask.active { display: flex; }
.sheet-panel { width: 100%; max-height: 80%; background: #fff; border-radius: 16px 16px 0 0; display: flex; flex-direction: column; overflow: hidden; }
.sheet-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 0.5px solid #f0f0f0; flex-shrink: 0; }
.sheet-header .s-cancel { font-size: 14px; color: #999; cursor: pointer; background: none; border: none; }
.sheet-header .s-title { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.sheet-header .s-save { font-size: 14px; color: #1677ff; font-weight: 600; cursor: pointer; background: none; border: none; }
.sheet-body { padding: 12px 16px 20px; overflow-y: auto; flex: 1; }
.sheet-body::-webkit-scrollbar { display: none; }
.form-item { margin-bottom: 14px; }
.form-item__label { font-size: 13px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.form-input { width: 100%; border: 1px solid #e8e8e8; border-radius: 8px; padding: 10px 12px; font-size: 14px; outline: none; color: #1a1a1a; }
.form-input:focus { border-color: #1677ff; }
.ratio-row { display: flex; gap: 8px; }
.ratio-row .form-input { flex: 1; text-align: center; }
.tag-picker { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-picker .tag-opt { font-size: 12px; padding: 5px 12px; border-radius: 14px; background: #f5f5f5; color: #666; cursor: pointer; }
.tag-picker .tag-opt.active { background: #e6f4ff; color: #1677ff; font-weight: 600; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<!-- [交互] 点击返回: ./index.html -->
<button class="back-btn" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">张小红</span>
<!-- [交互] 点击打开结束咨询弹窗 -->
<button class="nav-right" onclick="openModal('modal-end-confirm')">结束咨询</button>
</div>
<!-- 快捷操作栏 -->
<div class="action-bar">
<!-- [交互] 点击跳转至: ./employee-health.html 查看员工健康档案 -->
<div class="action-btn action-btn--blue" onclick="location.href='./employee-health.html'">📋 健康档案</div>
<!-- [交互] 点击打开制定营养方案弹层 -->
<div class="action-btn action-btn--green" onclick="openSheet('sheet-plan')">🎯 制定方案</div>
</div>
<!-- 消息区 -->
<div class="scroll-content" id="chat-scroll" style="padding:12px 16px;">
<!-- 时间标记 -->
<div style="text-align:center;margin:8px 0;"><span style="font-size:11px;color:#bbb;background:#ebebeb;padding:3px 10px;border-radius:10px;">10:20</span></div>
<!-- [交互] 员工营养档案卡片 - 点击查看员工营养详情 -->
<div style="display:flex;gap:8px;margin-bottom:14px;">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#ffe0e0,#ffb3b3);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">👩</div>
<div><div style="font-size:11px;color:#999;margin-bottom:4px;">张小红</div>
<div class="bubble-record" onclick="location.href='./employee-detail.html'">
<div class="bubble-record__header">
<div class="bubble-record__avatar">👩</div>
<div>
<div class="bubble-record__name">张小红 · 女 · 32岁</div>
<div class="bubble-record__meta">162cm · 67kg · BMI 25.6</div>
</div>
</div>
<div class="bubble-record__body">
<div class="bubble-record__title">减脂营养咨询</div>
<div class="bubble-record__desc">食堂饭菜偏油,想请教日常减脂怎么搭配饮食</div>
<div class="bubble-record__tags">
<span class="bubble-record__tag bubble-record__tag--green">目标:减脂 5kg</span>
</div>
</div>
</div>
</div>
</div>
<!-- 员工消息(左侧) -->
<div style="display:flex;gap:8px;margin-bottom:14px;">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#ffe0e0,#ffb3b3);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">👩</div>
<div><div style="font-size:11px;color:#999;margin-bottom:4px;">张小红</div>
<div style="background:#fff;border-radius:0 12px 12px 12px;padding:10px 14px;font-size:14px;color:#1a1a1a;max-width:220px;box-shadow:0 1px 4px rgba(0,0,0,0.06);line-height:1.5;">营养师您好,我最近在减脂,但食堂的红烧肉、油炸类菜品偏多,不知道怎么吃才不会超标。</div>
</div>
</div>
<!-- 时间标记 -->
<div style="text-align:center;margin:8px 0;"><span style="font-size:11px;color:#bbb;background:#ebebeb;padding:3px 10px;border-radius:10px;">10:23</span></div>
<!-- 营养师消息(右侧) -->
<div style="display:flex;gap:8px;margin-bottom:14px;flex-direction:row-reverse;">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#e8f4ff,#bdd7ff);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">🥗</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;">
<div style="font-size:11px;color:#999;margin-bottom:4px;">李营养师(我)</div>
<div style="background:#1677ff;border-radius:12px 0 12px 12px;padding:10px 14px;font-size:14px;color:#fff;max-width:220px;line-height:1.5;">您好,减脂期在食堂就餐建议:主菜优先选清蒸、白灼、凉拌类,肉类选去皮鸡胸、鱼虾;主食粗细搭配,每餐一拳主食、一掌蛋白质、两拳蔬菜。</div>
</div>
</div>
<!-- 营养师消息:推荐食谱卡片 -->
<div style="display:flex;gap:8px;margin-bottom:14px;flex-direction:row-reverse;">
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#e8f4ff,#bdd7ff);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">🥗</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;">
<div style="font-size:11px;color:#999;margin-bottom:4px;">李营养师(我)</div>
<div style="background:#fff;border-radius:12px 0 12px 12px;padding:10px;max-width:220px;box-shadow:0 1px 4px rgba(0,0,0,0.08);cursor:pointer;" onclick="location.href='./plan-detail.html'">
<div style="font-size:12px;color:#999;margin-bottom:6px;">为您推荐个性化营养方案</div>
<div style="font-size:14px;font-weight:600;color:#1a1a1a;">减脂期一周饮食方案</div>
<div style="font-size:12px;color:#666;margin-top:4px;line-height:1.5;">每日热量 1400 kcal · 蛋白质 25% · 脂肪 20% · 碳水 55%</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:8px;padding-top:8px;border-top:1px solid #f0f0f0;">
<span style="font-size:11px;color:#52c41a;">● 已生成</span>
<span style="font-size:12px;color:#1677ff;">查看详情 </span>
</div>
</div>
</div>
</div>
<div style="height:8px;"></div>
</div>
<!-- 常用语面板 -->
<div id="quick-panel" style="display:none;background:#fff;border-top:1px solid #f0f0f0;padding:10px 16px;flex-shrink:0;max-height:200px;overflow-y:auto;">
<div style="display:flex;gap:6px;margin-bottom:10px;">
<span class="quick-cat active" data-cat="all" onclick="filterQuick(this)">全部</span>
<span class="quick-cat" data-cat="greet" onclick="filterQuick(this)">问候语</span>
<span class="quick-cat" data-cat="ask" onclick="filterQuick(this)">问询</span>
<span class="quick-cat" data-cat="advice" onclick="filterQuick(this)">建议</span>
<span class="quick-cat" data-cat="end" onclick="filterQuick(this)">结束语</span>
</div>
<div id="quick-list">
<div class="quick-item" data-cat="greet" onclick="useQuick(this)">您好,我是您的专属营养师</div>
<div class="quick-item" data-cat="greet" onclick="useQuick(this)">您好,请描述您的饮食与健康目标</div>
<div class="quick-item" data-cat="ask" onclick="useQuick(this)">请问您目前的身高体重是多少?</div>
<div class="quick-item" data-cat="ask" onclick="useQuick(this)">是否有高血脂、糖尿病等慢性病史?</div>
<div class="quick-item" data-cat="ask" onclick="useQuick(this)">平时主要在食堂就餐还是自带餐?</div>
<div class="quick-item" data-cat="advice" onclick="useQuick(this)">建议主食粗细搭配,每餐一拳量</div>
<div class="quick-item" data-cat="advice" onclick="useQuick(this)">优先选择清蒸、白灼类菜品</div>
<div class="quick-item" data-cat="advice" onclick="useQuick(this)">控制油脂摄入,每日不超过 25g</div>
<div class="quick-item" data-cat="end" onclick="useQuick(this)">如有不适请及时调整并反馈</div>
<div class="quick-item" data-cat="end" onclick="useQuick(this)">祝您健康,欢迎随时咨询!</div>
</div>
</div>
<!-- 输入区 -->
<div style="background:#fff;border-top:1px solid #f0f0f0;padding:8px 12px 14px;flex-shrink:0;">
<div style="display:flex;align-items:center;gap:6px;">
<button onclick="toggleQuick()" style="width:34px;height:34px;border-radius:50%;background:#f5f5f5;border:none;font-size:16px;cursor:pointer;flex-shrink:0;"></button>
<button id="voice-btn" onclick="toggleVoice()" style="width:34px;height:34px;border-radius:50%;background:#f5f5f5;border:none;font-size:16px;cursor:pointer;flex-shrink:0;">🎤</button>
<input id="chat-input" type="text" placeholder="输入营养建议..." style="flex:1;background:#f5f5f5;border:none;border-radius:20px;padding:8px 14px;font-size:14px;outline:none;color:#1a1a1a;" />
<button id="voice-record-btn" style="display:none;flex:1;height:36px;background:#f5f5f5;border:1.5px solid #d9d9d9;border-radius:20px;font-size:13px;color:#555;cursor:pointer;text-align:center;align-items:center;justify-content:center;letter-spacing:1px;">按住说话</button>
<button onclick="sendMsg()" style="width:34px;height:34px;border-radius:50%;background:#1677ff;border:none;font-size:16px;cursor:pointer;flex-shrink:0;color:#fff;display:flex;align-items:center;justify-content:center;"></button>
</div>
</div>
<!-- 结束咨询确认弹窗 -->
<div id="modal-end-confirm" class="modal-center">
<div class="modal-box">
<div class="modal-title">结束咨询</div>
<div class="modal-body">确认结束与张小红的营养咨询?结束后将无法继续发送消息。</div>
<div class="modal-actions">
<button onclick="closeModal('modal-end-confirm')">取消</button>
<button onclick="closeModal('modal-end-confirm');location.href='./index.html'">确认结束</button>
</div>
</div>
</div>
<!-- 制定营养方案底部弹层 -->
<div id="sheet-plan" class="sheet-mask" onclick="if(event.target===this)closeSheet('sheet-plan')">
<div class="sheet-panel" onclick="event.stopPropagation()">
<div class="sheet-header">
<button class="s-cancel" onclick="closeSheet('sheet-plan')">取消</button>
<span class="s-title">为张小红制定营养方案</span>
<button class="s-save" onclick="savePlan()">保存</button>
</div>
<div class="sheet-body">
<div class="form-item">
<div class="form-item__label">目标热量(kcal/日)</div>
<input class="form-input" type="text" value="1400" />
</div>
<div class="form-item">
<div class="form-item__label">三大营养素占比(%</div>
<div class="ratio-row">
<input class="form-input" type="text" value="25" placeholder="蛋白" />
<input class="form-input" type="text" value="20" placeholder="脂肪" />
<input class="form-input" type="text" value="55" placeholder="碳水" />
</div>
</div>
<div class="form-item">
<div class="form-item__label">饮食禁忌</div>
<div class="tag-picker">
<span class="tag-opt active" onclick="toggleTag(this)">高糖</span>
<span class="tag-opt active" onclick="toggleTag(this)">高脂</span>
<span class="tag-opt" onclick="toggleTag(this)">高盐</span>
<span class="tag-opt active" onclick="toggleTag(this)">高嘌呤</span>
<span class="tag-opt" onclick="toggleTag(this)">乳糖</span>
</div>
</div>
<div class="form-item">
<div class="form-item__label">目标描述</div>
<input class="form-input" type="text" value="减脂 5kg,控制尿酸" />
</div>
</div>
</div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
function openModal(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
function closeModal(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
/* 切换常用语面板 */
function toggleQuick() {
var p = document.getElementById('quick-panel');
p.style.display = p.style.display === 'none' ? 'block' : 'none';
}
/* 语音模式状态 */
var isVoiceMode = false;
function toggleVoice() {
isVoiceMode = !isVoiceMode;
var input = document.getElementById('chat-input');
var voiceBtn = document.getElementById('voice-btn');
var recordBtn = document.getElementById('voice-record-btn');
if (isVoiceMode) {
input.style.display = 'none';
recordBtn.style.display = 'flex';
voiceBtn.textContent = '⌨️';
voiceBtn.style.background = '#e6f4ff';
} else {
input.style.display = '';
recordBtn.style.display = 'none';
voiceBtn.textContent = '🎤';
voiceBtn.style.background = '#f5f5f5';
}
}
/* 使用常用语填充输入框 */
function useQuick(el) {
document.getElementById('chat-input').value = el.textContent;
document.getElementById('quick-panel').style.display = 'none';
}
/* 常用语分类筛选 */
function filterQuick(tab) {
tab.parentElement.querySelectorAll('.quick-cat').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
var cat = tab.dataset.cat;
document.querySelectorAll('#quick-list .quick-item').forEach(function(item) {
item.style.display = (cat === 'all' || item.dataset.cat === cat) ? '' : 'none';
});
}
/* 发送消息,在聊天区追加一条营养师消息气泡 */
function sendMsg() {
var input = document.getElementById('chat-input');
var text = input.value.trim();
if (!text) return;
var scroll = document.getElementById('chat-scroll');
var bubble = document.createElement('div');
bubble.style.cssText = 'display:flex;gap:8px;margin-bottom:14px;flex-direction:row-reverse;';
bubble.innerHTML = '<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#e8f4ff,#bdd7ff);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;">🥗</div><div style="display:flex;flex-direction:column;align-items:flex-end;"><div style="font-size:11px;color:#999;margin-bottom:4px;">李营养师(我)</div><div style="background:#1677ff;border-radius:12px 0 12px 12px;padding:10px 14px;font-size:14px;color:#fff;max-width:220px;line-height:1.5;">' + text + '</div></div>';
scroll.insertBefore(bubble, scroll.lastElementChild);
input.value = '';
scroll.scrollTop = scroll.scrollHeight;
}
/* 打开底部弹层 */
function openSheet(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
/* 关闭底部弹层 */
function closeSheet(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
/* 禁忌标签选择 */
function toggleTag(el) { el.classList.toggle('active'); }
/* 保存方案并关闭弹层 */
function savePlan() {
closeSheet('sheet-plan');
alert('营养方案已保存,系统将自动生成一周专属食谱并推送至员工端(原型演示)');
}
</script>
</body>
</html>
@@ -0,0 +1,249 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 员工信息头 */
.emp-header { background: linear-gradient(135deg,#1677ff,#0958d9); padding: 16px 20px; color: #fff; }
.emp-header__name { font-size: 18px; font-weight: 700; }
.emp-header__meta { font-size: 12px; color: rgba(255,255,255,0.85); margin-top: 4px; }
.emp-header__tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.emp-header__tag { font-size: 11px; padding: 3px 10px; border-radius: 12px; background: rgba(255,255,255,0.2); }
/* 区块卡片 */
.card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.card__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
.card__title-extra { font-size: 12px; color: #1677ff; font-weight: 400; cursor: pointer; }
/* 达标概览 */
.overview-row { display: flex; }
.overview-item { flex: 1; text-align: center; }
.overview-item + .overview-item { border-left: 1px solid #f0f0f0; }
.overview-item__val { font-size: 18px; font-weight: 700; }
.overview-item__val--good { color: #52c41a; }
.overview-item__val--bad { color: #ff4d4f; }
.overview-item__label { font-size: 11px; color: #999; margin-top: 2px; }
/* 膳食宝塔 */
.pyramid { display: flex; flex-direction: column; gap: 6px; }
.pyramid-row { display: flex; align-items: center; gap: 8px; }
.pyramid-row__label { width: 56px; font-size: 12px; color: #666; flex-shrink: 0; }
.pyramid-row__bar { flex: 1; height: 10px; background: #f0f0f0; border-radius: 5px; overflow: hidden; }
.pyramid-row__fill { height: 100%; border-radius: 5px; }
.pyramid-row__val { width: 40px; font-size: 11px; color: #999; text-align: right; flex-shrink: 0; }
/* 摄入明细 */
.meal-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }
.meal-row:last-child { border-bottom: none; }
.meal-row__left { display: flex; align-items: center; gap: 8px; }
.meal-row__icon { font-size: 18px; }
.meal-row__name { font-size: 14px; color: #1a1a1a; font-weight: 500; }
.meal-row__kcal { font-size: 14px; font-weight: 700; color: #fa8c16; }
.meal-row__kcal span { font-size: 11px; font-weight: 400; color: #999; }
/* 营养方案 */
.plan-box { background: #f6ffed; border: 1px solid #b7eb8f; border-radius: 10px; padding: 12px; }
.plan-box__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.plan-box__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.plan-box__status { font-size: 11px; color: #52c41a; font-weight: 600; }
.plan-box__row { display: flex; justify-content: space-between; font-size: 12px; color: #666; padding: 3px 0; }
.plan-box__row .val { color: #333; font-weight: 600; }
/* 方案编辑表单 */
.plan-form { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.form-item { margin-bottom: 14px; }
.form-item__label { font-size: 13px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.form-input { width: 100%; border: 1px solid #e8e8e8; border-radius: 8px; padding: 10px 12px; font-size: 14px; outline: none; color: #1a1a1a; }
.form-input:focus { border-color: #1677ff; }
.ratio-row { display: flex; gap: 8px; }
.ratio-row .form-input { flex: 1; text-align: center; }
.tag-picker { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-picker .tag-opt { font-size: 12px; padding: 5px 12px; border-radius: 14px; background: #f5f5f5; color: #666; cursor: pointer; }
.tag-picker .tag-opt.active { background: #e6f4ff; color: #1677ff; font-weight: 600; }
.btn-primary { width: 100%; padding: 12px; background: #1677ff; color: #fff; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; }
.btn-ghost { width: 100%; padding: 12px; background: #fff; color: #1677ff; border: 1px solid #1677ff; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; }
.modal-center { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; align-items: center; justify-content: center; }
.modal-center.active { display: flex; }
.modal-box { width: 280px; background: #fff; border-radius: 16px; overflow: hidden; }
.modal-box .modal-title { padding: 20px 20px 8px; font-size: 17px; font-weight: 700; text-align: center; color: #1a1a1a; }
.modal-box .modal-body { padding: 8px 20px 20px; font-size: 14px; color: #666; text-align: center; line-height: 1.6; }
.modal-box .modal-actions { display: flex; border-top: 1px solid #f0f0f0; }
.modal-box .modal-actions button { flex: 1; padding: 14px; font-size: 16px; cursor: pointer; background: none; border: none; }
.modal-box .modal-actions button:first-child { color: #666; border-right: 1px solid #f0f0f0; }
.modal-box .modal-actions button:last-child { color: #1677ff; font-weight: 600; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="nav-back" onclick="location.href='./employee-list.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">员工营养详情</span>
</div>
<div class="scroll-content">
<!-- 员工信息头 -->
<div class="emp-header">
<div style="display:flex;justify-content:space-between;align-items:flex-start;">
<div>
<div class="emp-header__name">张小红</div>
<div class="emp-header__meta">女 · 32岁 · 公司机关 · 办公室 · 162cm · 67.2kg · BMI 25.6</div>
</div>
<!-- [交互] 点击跳转至: ./employee-health.html 查看员工健康档案 -->
<button onclick="location.href='./employee-health.html'" style="display:flex;align-items:center;gap:4px;background:rgba(255,255,255,0.2);border:1px solid rgba(255,255,255,0.4);border-radius:16px;padding:6px 12px;color:#fff;font-size:12px;cursor:pointer;flex-shrink:0;">📋 健康档案</button>
</div>
<div class="emp-header__tags">
<span class="emp-header__tag">减脂需求</span>
<span class="emp-header__tag">轻体力活动</span>
</div>
</div>
<!-- 营养达标概览 -->
<div class="card">
<div class="card__title">营养达标概览</div>
<div class="overview-row">
<div class="overview-item"><div class="overview-item__val overview-item__val--bad">68%</div><div class="overview-item__label">综合达标率</div></div>
<div class="overview-item"><div class="overview-item__val">22</div><div class="overview-item__label">本月用餐天数</div></div>
<div class="overview-item"><div class="overview-item__val">1890</div><div class="overview-item__label">日均摄入(kcal)</div></div>
</div>
</div>
<!-- 膳食宝塔评估 -->
<div class="card">
<div class="card__title">膳食宝塔评估</div>
<div class="pyramid">
<div class="pyramid-row"><span class="pyramid-row__label">谷薯类</span><div class="pyramid-row__bar"><div class="pyramid-row__fill" style="width:82%;background:#52c41a;"></div></div><span class="pyramid-row__val">82%</span></div>
<div class="pyramid-row"><span class="pyramid-row__label">蔬菜水果</span><div class="pyramid-row__bar"><div class="pyramid-row__fill" style="width:45%;background:#fa8c16;"></div></div><span class="pyramid-row__val">45%</span></div>
<div class="pyramid-row"><span class="pyramid-row__label">畜禽鱼蛋</span><div class="pyramid-row__bar"><div class="pyramid-row__fill" style="width:110%;background:#ff4d4f;"></div></div><span class="pyramid-row__val">110%</span></div>
<div class="pyramid-row"><span class="pyramid-row__label">奶豆坚果</span><div class="pyramid-row__bar"><div class="pyramid-row__fill" style="width:38%;background:#fa8c16;"></div></div><span class="pyramid-row__val">38%</span></div>
<div class="pyramid-row"><span class="pyramid-row__label">油盐</span><div class="pyramid-row__bar"><div class="pyramid-row__fill" style="width:135%;background:#ff4d4f;"></div></div><span class="pyramid-row__val">135%</span></div>
</div>
</div>
<!-- 今日摄入明细 -->
<div class="card">
<div class="card__title">今日摄入明细<span class="card__title-extra" onclick="location.href='./employee-diet-history.html'">查看历史 </span></div>
<div class="meal-row"><div class="meal-row__left"><span class="meal-row__icon">🥣</span><span class="meal-row__name">早餐</span></div><div class="meal-row__kcal">486 <span>kcal</span></div></div>
<div class="meal-row"><div class="meal-row__left"><span class="meal-row__icon">🍱</span><span class="meal-row__name">午餐</span></div><div class="meal-row__kcal">720 <span>kcal</span></div></div>
<div class="meal-row"><div class="meal-row__left"><span class="meal-row__icon">🍎</span><span class="meal-row__name">加餐</span></div><div class="meal-row__kcal">185 <span>kcal</span></div></div>
<div class="meal-row"><div class="meal-row__left"><span class="meal-row__icon">🍽️</span><span class="meal-row__name">晚餐</span></div><div class="meal-row__kcal">560 <span>kcal</span></div></div>
</div>
<!-- 营养方案 -->
<div class="card">
<div class="card__title">营养方案</div>
<div class="plan-box" id="plan-box">
<div class="plan-box__head">
<span class="plan-box__name">减脂期一周饮食方案</span>
<span class="plan-box__status">执行中</span>
</div>
<div class="plan-box__row"><span>目标热量</span><span class="val">1400 kcal/日</span></div>
<div class="plan-box__row"><span>蛋白质 : 脂肪 : 碳水</span><span class="val">25% : 20% : 55%</span></div>
<div class="plan-box__row"><span>饮食禁忌</span><span class="val">高糖 · 高脂</span></div>
<div class="plan-box__row"><span>执行进度</span><span class="val">第 3 天 / 7 天</span></div>
</div>
<div style="display:flex;gap:10px;margin-top:12px;">
<!-- [交互] 点击跳转至: ./plan-detail.html 查看方案详情 -->
<button onclick="location.href='./plan-detail.html'" style="flex:1;padding:12px;background:#fff;color:#1677ff;border:1px solid #1677ff;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;">查看详情</button>
<!-- [交互] 点击展开方案编辑表单 -->
<button onclick="togglePlanForm()" style="flex:1;padding:12px;background:#1677ff;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;">调整方案</button>
</div>
</div>
<!-- 方案编辑表单 -->
<div class="plan-form" id="plan-form" style="display:none;">
<div class="form-item">
<div class="form-item__label">目标热量(kcal/日)</div>
<input class="form-input" type="text" value="1400" />
</div>
<div class="form-item">
<div class="form-item__label">三大营养素占比(%</div>
<div class="ratio-row">
<input class="form-input" type="text" value="25" placeholder="蛋白" />
<input class="form-input" type="text" value="20" placeholder="脂肪" />
<input class="form-input" type="text" value="55" placeholder="碳水" />
</div>
</div>
<div class="form-item">
<div class="form-item__label">饮食禁忌</div>
<div class="tag-picker">
<span class="tag-opt active" onclick="toggleTag(this)">高糖</span>
<span class="tag-opt active" onclick="toggleTag(this)">高脂</span>
<span class="tag-opt" onclick="toggleTag(this)">高盐</span>
<span class="tag-opt" onclick="toggleTag(this)">高嘌呤</span>
<span class="tag-opt" onclick="toggleTag(this)">乳糖</span>
</div>
</div>
<div class="form-item">
<div class="form-item__label">目标描述</div>
<input class="form-input" type="text" value="减脂 5kg,控制体脂率" />
</div>
<!-- [交互] 点击保存方案 -->
<button class="btn-primary" onclick="savePlan()">保存方案</button>
</div>
<div style="height:16px;"></div>
</div>
<!-- 保存成功弹窗 -->
<div id="modal-saved" class="modal-center">
<div class="modal-box">
<div class="modal-title">保存成功</div>
<div class="modal-body">营养方案已保存,系统将自动生成一周专属食谱并推送至员工端小程序。</div>
<div class="modal-actions">
<button onclick="closeModal('modal-saved')">知道了</button>
</div>
</div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* 展开/收起方案编辑表单 */
function togglePlanForm() {
var form = document.getElementById('plan-form');
var isHidden = form.style.display === 'none';
form.style.display = isHidden ? 'block' : 'none';
if (isHidden) {
/* 展开后自动滚动到表单位置,避免表单在视口外用户看不到 */
setTimeout(function() {
form.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 50);
}
}
/* 禁忌标签选择 */
function toggleTag(el) {
el.classList.toggle('active');
}
/* 保存方案 */
function savePlan() {
document.getElementById('plan-form').style.display = 'none';
openModal('modal-saved');
}
function openModal(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
function closeModal(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
</script>
</body>
</html>
@@ -0,0 +1,284 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 员工信息 */
.emp-info { background: linear-gradient(135deg,#1677ff,#0958d9); padding: 14px 20px; color: #fff; display: flex; align-items: center; gap: 12px; }
.emp-info__avatar { width: 42px; height: 42px; border-radius: 50%; background: rgba(255,255,255,0.25); border: 2px solid rgba(255,255,255,0.4); display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.emp-info__name { font-size: 16px; font-weight: 700; }
.emp-info__meta { font-size: 12px; color: rgba(255,255,255,0.85); margin-top: 2px; }
/* 日期导航 */
.date-nav { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; background: #fff; border-bottom: 1px solid #f0f0f0; position: relative; flex-shrink: 0; }
.date-nav__arrow { width: 32px; height: 32px; border-radius: 50%; background: #f5f5f5; display: flex; align-items: center; justify-content: center; cursor: pointer; border: none; color: #666; font-size: 16px; flex-shrink: 0; }
.date-nav__arrow:active { background: #e6f4ff; }
.date-nav__text { font-size: 15px; font-weight: 600; color: #1a1a1a; min-width: 110px; text-align: center; }
.date-nav__cal { width: 28px; height: 28px; border-radius: 6px; background: none; display: flex; align-items: center; justify-content: center; cursor: pointer; border: none; font-size: 16px; flex-shrink: 0; color: #1677ff; }
/* 日历面板 */
.cal-panel { display: none; position: absolute; top: 42px; left: 16px; right: 16px; background: #fff; border-radius: 14px; box-shadow: 0 8px 30px rgba(0,0,0,0.15); z-index: 100; padding: 16px; }
.cal-panel.active { display: block; }
.cal-panel__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.cal-panel__month { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.cal-panel__nav { width: 28px; height: 28px; border-radius: 50%; border: none; background: #f5f5f5; cursor: pointer; font-size: 14px; color: #666; display: flex; align-items: center; justify-content: center; }
.cal-panel__weekdays { display: flex; margin-bottom: 6px; }
.cal-panel__weekday { flex: 1; text-align: center; font-size: 11px; color: #999; padding: 4px 0; }
.cal-panel__grid { display: flex; flex-wrap: wrap; }
.cal-panel__day { width: calc(100% / 7); text-align: center; padding: 8px 0; font-size: 13px; color: #333; cursor: pointer; border-radius: 8px; }
.cal-panel__day--other { color: #ddd; }
.cal-panel__day:hover { background: #f5f5f5; }
.cal-panel__day--today { font-weight: 700; color: #1677ff; }
.cal-panel__day--selected { background: #1677ff; color: #fff; font-weight: 700; }
/* 摄入汇总 */
.summary-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.summary-card__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; }
.summary-row { display: flex; }
.summary-item { flex: 1; text-align: center; }
.summary-item + .summary-item { border-left: 1px solid #f0f0f0; }
.summary-item__val { font-size: 18px; font-weight: 700; }
.summary-item__val--over { color: #ff4d4f; }
.summary-item__val--ok { color: #52c41a; }
.summary-item__label { font-size: 11px; color: #999; margin-top: 2px; }
/* 三餐明细 */
.section-title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin: 14px 12px 6px; }
.meal-card { background: #fff; border-radius: 12px; margin: 8px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.meal-card__head { display: flex; align-items: center; margin-bottom: 8px; }
.meal-card__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.meal-card__time { font-size: 12px; color: #999; margin-left: 8px; }
.meal-card__kcal { margin-left: auto; font-size: 15px; font-weight: 700; color: #fa8c16; }
.meal-card__kcal span { font-size: 11px; font-weight: 400; color: #999; }
.meal-card__foods { display: flex; flex-wrap: wrap; gap: 6px; }
.food-tag { font-size: 12px; padding: 4px 10px; border-radius: 12px; background: #f0f6ff; color: #1677ff; }
.meal-card__source { margin-top: 8px; font-size: 11px; color: #999; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; }
.tag-blue { background: #e6f4ff; color: #1677ff; }
.tag-purple { background: #f9f0ff; color: #722ed1; }
/* 空状态 */
.empty-state { text-align: center; padding: 40px 0; color: #bbb; font-size: 13px; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="nav-back" onclick="location.href='./employee-detail.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">饮食记录</span>
</div>
<!-- 员工信息 -->
<div class="emp-info">
<div class="emp-info__avatar">👩</div>
<div>
<div class="emp-info__name">张小红</div>
<div class="emp-info__meta">女 · 32岁 · 公司机关 · 办公室</div>
</div>
</div>
<!-- 日期导航 -->
<div class="date-nav">
<button class="date-nav__arrow" onclick="changeDate(-1)"></button>
<button class="date-nav__cal" onclick="toggleCalPanel()">📅</button>
<span class="date-nav__text" id="dateText">2026-06-22</span>
<button class="date-nav__arrow" onclick="changeDate(1)"></button>
<!-- 日历选择面板 -->
<div class="cal-panel" id="calPanel">
<div class="cal-panel__head">
<button class="cal-panel__nav" onclick="calPrevMonth(event)"></button>
<span class="cal-panel__month" id="calMonthLabel">2026年6月</span>
<button class="cal-panel__nav" onclick="calNextMonth(event)"></button>
</div>
<div class="cal-panel__weekdays">
<div class="cal-panel__weekday"></div><div class="cal-panel__weekday"></div><div class="cal-panel__weekday"></div><div class="cal-panel__weekday"></div><div class="cal-panel__weekday"></div><div class="cal-panel__weekday"></div><div class="cal-panel__weekday"></div>
</div>
<div class="cal-panel__grid" id="calGrid"></div>
</div>
</div>
<div class="scroll-content">
<!-- 摄入汇总 -->
<div class="summary-card" id="summary-card">
<div class="summary-card__title">每日摄入汇总</div>
<div class="summary-row" id="summary-row"></div>
</div>
<!-- 当日三餐明细 -->
<div class="section-title">当日三餐明细</div>
<div id="meal-list"></div>
<div style="height:16px;"></div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* ===== 饮食记录历史数据 ===== */
var DIET_HISTORY = {
'2026-06-22': {
target: 1400,
meals: [
{ icon: '🥣', name: '早餐', time: '07:30', kcal: 486, source: '智记', cls: 'tag-blue', foods: ['全麦面包 80g', '水煮蛋 60g', '牛奶 250ml'] },
{ icon: '🍱', name: '午餐', time: '12:15', kcal: 720, source: '智记', cls: 'tag-blue', foods: ['清蒸鲈鱼 150g', '杂粮饭 200g', '蒜蓉西兰花 150g'] },
{ icon: '🍎', name: '加餐', time: '15:40', kcal: 185, source: '自记', cls: 'tag-purple', foods: ['苹果 1个', '坚果 30g'] },
{ icon: '🍽️', name: '晚餐', time: '18:30', kcal: 560, source: '智记', cls: 'tag-blue', foods: ['香煎鸡胸 120g', '蔬菜沙拉 200g', '小米粥 200g'] }
]
},
'2026-06-21': {
target: 1400,
meals: [
{ icon: '🥣', name: '早餐', time: '07:42', kcal: 450, source: '智记', cls: 'tag-blue', foods: ['燕麦牛奶 300ml', '水煮蛋 60g'] },
{ icon: '🍱', name: '午餐', time: '12:08', kcal: 680, source: '智记', cls: 'tag-blue', foods: ['白灼虾 150g', '糙米饭 200g', '清炒时蔬 150g'] },
{ icon: '🍎', name: '加餐', time: '15:30', kcal: 120, source: '自记', cls: 'tag-purple', foods: ['酸奶 1杯'] },
{ icon: '🍽️', name: '晚餐', time: '18:45', kcal: 520, source: '智记', cls: 'tag-blue', foods: ['蔬菜豆腐汤 300g', '杂粮馒头 100g'] }
]
},
'2026-06-20': {
target: 1400,
meals: [
{ icon: '🥣', name: '早餐', time: '07:35', kcal: 470, source: '智记', cls: 'tag-blue', foods: ['水煮蛋 60g', '豆浆 250ml', '蒸红薯 100g'] },
{ icon: '🍱', name: '午餐', time: '12:20', kcal: 660, source: '智记', cls: 'tag-blue', foods: ['卤牛肉 100g', '荞麦面 200g', '凉拌黄瓜 150g'] },
{ icon: '🍽️', name: '晚餐', time: '18:20', kcal: 540, source: '智记', cls: 'tag-blue', foods: ['清炒时蔬 200g', '玉米 150g', '菌菇汤 200g'] }
]
},
'2026-06-19': {
target: 1400,
meals: [
{ icon: '🥣', name: '早餐', time: '07:50', kcal: 430, source: '智记', cls: 'tag-blue', foods: ['杂粮粥 300g', '水煮蛋 60g'] },
{ icon: '🍱', name: '午餐', time: '12:10', kcal: 700, source: '智记', cls: 'tag-blue', foods: ['香煎鸡胸 150g', '藜麦饭 150g', '西兰花 150g'] },
{ icon: '🍎', name: '加餐', time: '16:00', kcal: 150, source: '自记', cls: 'tag-purple', foods: ['虾仁沙拉 200g'] },
{ icon: '🍽️', name: '晚餐', time: '18:40', kcal: 480, source: '智记', cls: 'tag-blue', foods: ['蔬菜沙拉 200g', '全麦面包 50g'] }
]
}
};
var currentDate = new Date(2026, 5, 22);
var calViewYear = currentDate.getFullYear();
var calViewMonth = currentDate.getMonth();
/* 计算某天总摄入 */
function dayTotal(data) {
return data.meals.reduce(function(s, m) { return s + m.kcal; }, 0);
}
/* 格式化日期为 YYYY-MM-DD */
function fmtDate(d) {
var y = d.getFullYear();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
/* 渲染当前日期的饮食记录 */
function render() {
var key = fmtDate(currentDate);
document.getElementById('dateText').textContent = key;
var data = DIET_HISTORY[key];
if (!data) {
document.getElementById('summary-card').style.display = 'none';
document.getElementById('meal-list').innerHTML = '<div class="empty-state">当日暂无饮食记录</div>';
return;
}
document.getElementById('summary-card').style.display = 'block';
var total = dayTotal(data);
var diff = total - data.target;
var diffText = diff > 0 ? '+' + diff : diff;
var diffCls = diff > 0 ? 'summary-item__val--over' : 'summary-item__val--ok';
document.getElementById('summary-row').innerHTML =
'<div class="summary-item"><div class="summary-item__val" style="color:#1677ff;">' + total + '</div><div class="summary-item__label">总摄入 kcal</div></div>' +
'<div class="summary-item"><div class="summary-item__val" style="color:#333;">' + data.target + '</div><div class="summary-item__label">目标 kcal</div></div>' +
'<div class="summary-item"><div class="summary-item__val ' + diffCls + '">' + diffText + '</div><div class="summary-item__label">与目标差值</div></div>';
var mealsHtml = data.meals.map(function(m) {
var foodsHtml = m.foods.map(function(f) {
return '<span class="food-tag">' + f + '</span>';
}).join('');
return '<div class="meal-card">' +
'<div class="meal-card__head"><span class="meal-card__name">' + m.icon + ' ' + m.name + '</span><span class="meal-card__time">' + m.time + '</span><span class="meal-card__kcal">' + m.kcal + ' <span>kcal</span></span></div>' +
'<div class="meal-card__foods">' + foodsHtml + '</div>' +
'<div class="meal-card__source"><span class="tag ' + m.cls + '">' + m.source + '</span></div>' +
'</div>';
}).join('');
document.getElementById('meal-list').innerHTML = mealsHtml;
}
/* 切换日期 */
function changeDate(delta) {
currentDate.setDate(currentDate.getDate() + delta);
calViewYear = currentDate.getFullYear();
calViewMonth = currentDate.getMonth();
render();
}
/* 日历面板 */
function toggleCalPanel() {
var panel = document.getElementById('calPanel');
if (panel.classList.contains('active')) { panel.classList.remove('active'); return; }
calViewYear = currentDate.getFullYear();
calViewMonth = currentDate.getMonth();
renderCal();
panel.classList.add('active');
}
function calPrevMonth(e) { e.stopPropagation(); calViewMonth--; if (calViewMonth < 0) { calViewMonth = 11; calViewYear--; } renderCal(); }
function calNextMonth(e) { e.stopPropagation(); calViewMonth++; if (calViewMonth > 11) { calViewMonth = 0; calViewYear++; } renderCal(); }
function renderCal() {
document.getElementById('calMonthLabel').textContent = calViewYear + '年' + (calViewMonth + 1) + '月';
var firstDay = new Date(calViewYear, calViewMonth, 1).getDay();
var daysInMonth = new Date(calViewYear, calViewMonth + 1, 0).getDate();
var today = new Date(2026, 5, 22);
var html = '';
for (var i = 0; i < firstDay; i++) { html += '<div class="cal-panel__day cal-panel__day--other"></div>'; }
for (var d = 1; d <= daysInMonth; d++) {
var cls = 'cal-panel__day';
if (calViewYear === today.getFullYear() && calViewMonth === today.getMonth() && d === today.getDate()) cls += ' cal-panel__day--today';
if (calViewYear === currentDate.getFullYear() && calViewMonth === currentDate.getMonth() && d === currentDate.getDate()) cls += ' cal-panel__day--selected';
html += '<div class="' + cls + '" onclick="selectCalDay(event,' + d + ')">' + d + '</div>';
}
document.getElementById('calGrid').innerHTML = html;
}
function selectCalDay(e, day) {
e.stopPropagation();
currentDate = new Date(calViewYear, calViewMonth, day);
calViewYear = currentDate.getFullYear();
calViewMonth = currentDate.getMonth();
render();
document.getElementById('calPanel').classList.remove('active');
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.date-nav') && !e.target.closest('.cal-panel')) {
document.getElementById('calPanel').classList.remove('active');
}
});
/* 初始化渲染 */
render();
</script>
</body>
</html>
@@ -0,0 +1,269 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 员工卡片 */
.emp-card { background: linear-gradient(135deg,#1677ff,#0958d9); padding: 16px 20px; color: #fff; }
.emp-card__row { display: flex; align-items: center; gap: 12px; }
.emp-card__avatar { width: 48px; height: 48px; border-radius: 50%; background: rgba(255,255,255,0.25); border: 2px solid rgba(255,255,255,0.4); display: flex; align-items: center; justify-content: center; font-size: 24px; flex-shrink: 0; }
.emp-card__name { font-size: 18px; font-weight: 700; }
.emp-card__meta { font-size: 12px; color: rgba(255,255,255,0.85); margin-top: 2px; }
.emp-card__tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.emp-card__tag { font-size: 11px; padding: 3px 10px; border-radius: 12px; background: rgba(255,255,255,0.2); }
/* 卡片区块 */
.card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.card__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; }
.card__title .readonly { font-size: 10px; color: #999; font-weight: 400; background: #f5f5f5; padding: 1px 6px; border-radius: 8px; }
/* 关键摘要 */
.alert-item { display: flex; align-items: flex-start; gap: 8px; padding: 8px 0; border-bottom: 1px solid #f5f5f5; }
.alert-item:last-child { border-bottom: none; }
.alert-item__label { font-size: 12px; color: #999; width: 64px; flex-shrink: 0; margin-top: 2px; }
.alert-item__tags { flex: 1; display: flex; flex-wrap: wrap; gap: 6px; }
.atag { font-size: 12px; padding: 3px 10px; border-radius: 12px; }
.atag--danger { background: #fff1f0; color: #f5222d; }
.atag--warn { background: #fff7e6; color: #fa8c16; }
.atag--blue { background: #f0f7ff; color: #1677ff; }
.atag--normal { background: #f6ffed; color: #52c41a; }
/* 信息行 */
.info-row { display: flex; align-items: center; justify-content: space-between; padding: 9px 0; border-bottom: 0.5px solid #f5f5f5; }
.info-row:last-child { border-bottom: none; }
.info-row .i-label { font-size: 13px; color: #666; }
.info-row .i-value { font-size: 13px; color: #333; font-weight: 500; }
.info-row .i-value .i-unit { font-size: 11px; color: #999; margin-left: 2px; }
/* 标签组 */
.tag-group { display: flex; flex-wrap: wrap; gap: 6px; }
.tag-group .tg { font-size: 12px; padding: 4px 10px; border-radius: 12px; background: #f0f7ff; color: #1677ff; }
.tag-group .tg.warn { background: #fff7e6; color: #fa8c16; }
.tag-group .tg.danger { background: #fff1f0; color: #f5222d; }
.tag-group .tg.green { background: #f6ffed; color: #52c41a; }
/* 子标签 */
.sub-label { font-size: 12px; color: #999; margin-bottom: 6px; }
.sub-block { padding: 8px 0; border-bottom: 0.5px solid #f5f5f5; }
.sub-block:last-child { border-bottom: none; }
/* 餐次配比 */
.ratio-bar { display: flex; height: 10px; border-radius: 5px; overflow: hidden; background: #f0f0f0; margin-bottom: 10px; }
.ratio-bar .ratio-seg { height: 100%; }
.ratio-list { display: flex; }
.ratio-list .ratio-item { flex: 1; display: flex; align-items: center; gap: 4px; font-size: 12px; }
.ratio-list .ratio-item .r-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.ratio-list .ratio-item .r-name { color: #999; }
.ratio-list .ratio-item .r-val { color: #333; font-weight: 600; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<!-- [交互] 点击返回上一页 -->
<button class="nav-back" onclick="history.back()">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">员工健康档案</span>
</div>
<div class="scroll-content">
<!-- 员工卡片 -->
<div class="emp-card">
<div class="emp-card__row">
<div class="emp-card__avatar">👩</div>
<div>
<div class="emp-card__name">张小红</div>
<div class="emp-card__meta">女 · 32岁 · 公司机关 · 办公室 · 162cm · 67.2kg · BMI 25.6</div>
</div>
</div>
<div class="emp-card__tags">
<span class="emp-card__tag">减脂需求</span>
<span class="emp-card__tag">中度活动</span>
</div>
</div>
<!-- 关键摘要(营养师定制方案重点参考) -->
<div class="card" style="border-left:3px solid #1677ff;">
<div class="card__title">🎯 方案定制关键信息</div>
<div class="alert-item">
<span class="alert-item__label">体检异常</span>
<div class="alert-item__tags">
<span class="atag atag--warn">尿酸 380 偏高</span>
<span class="atag atag--normal">血压/血糖正常</span>
</div>
</div>
<div class="alert-item">
<span class="alert-item__label">疾病史</span>
<div class="alert-item__tags">
<span class="atag atag--danger">胃炎</span>
<span class="atag atag--danger">高尿酸</span>
</div>
</div>
<div class="alert-item">
<span class="alert-item__label">用药情况</span>
<div class="alert-item__tags">
<span class="atag atag--blue">别嘌醇片</span>
<span class="atag atag--blue">依那普利</span>
</div>
</div>
<div class="alert-item">
<span class="alert-item__label">过敏原</span>
<div class="alert-item__tags">
<span class="atag atag--danger">🥜 花生</span>
<span class="atag atag--danger">🦐 海鲜</span>
</div>
</div>
<div class="alert-item">
<span class="alert-item__label">饮食禁忌</span>
<div class="alert-item__tags">
<span class="atag atag--warn">🌶️ 少辣</span>
<span class="atag atag--warn">🧂 低盐</span>
<span class="atag atag--warn">🍬 控糖</span>
</div>
</div>
</div>
<!-- 1. 身体指标 -->
<div class="card">
<div class="card__title">📏 身体指标</div>
<div class="info-row"><span class="i-label">身高</span><span class="i-value">172<span class="i-unit">cm</span></span></div>
<div class="info-row"><span class="i-label">体重</span><span class="i-value">68<span class="i-unit">kg</span></span></div>
<div class="info-row"><span class="i-label">BMI</span><span class="i-value">23.0 <span style="color:#52c41a;font-size:11px;">正常</span></span></div>
<div class="info-row"><span class="i-label">体脂率</span><span class="i-value">18.5% <span style="color:#52c41a;font-size:11px;">标准</span></span></div>
<div class="info-row"><span class="i-label">活动水平</span><span class="i-value">中度活动<span class="i-unit">每周3-5次</span></span></div>
</div>
<!-- 2. 体检信息 -->
<div class="card">
<div class="card__title">🩺 体检信息<span class="readonly">只读</span></div>
<div class="info-row"><span class="i-label">体检日期</span><span class="i-value">2026-03-15</span></div>
<div class="info-row"><span class="i-label">血压</span><span class="i-value">125/80<span class="i-unit">mmHg</span></span></div>
<div class="info-row"><span class="i-label">空腹血糖</span><span class="i-value">5.2<span class="i-unit">mmol/L</span></span></div>
<div class="info-row"><span class="i-label">血脂</span><span class="i-value">正常</span></div>
<div class="info-row"><span class="i-label">尿酸</span><span class="i-value" style="color:#fa8c16;">380<span class="i-unit">μmol/L</span> <span style="color:#fa8c16;font-size:11px;">偏高</span></span></div>
<div class="info-row"><span class="i-label">肝功能</span><span class="i-value">正常</span></div>
<div class="info-row"><span class="i-label">肾功能</span><span class="i-value">正常</span></div>
</div>
<!-- 3. 疾病信息 -->
<div class="card">
<div class="card__title">🏥 疾病信息</div>
<div class="sub-block">
<div class="sub-label">疾病史</div>
<div class="tag-group"><span class="tg danger">胃炎</span><span class="tg danger">高尿酸</span></div>
</div>
<div class="sub-block">
<div class="sub-label">用药情况</div>
<div class="tag-group"><span class="tg">别嘌醇片</span><span class="tg">依那普利</span></div>
</div>
<div class="sub-block">
<div class="sub-label">家族病史</div>
<div class="tag-group"><span class="tg warn">高血压</span><span class="tg warn">糖尿病</span></div>
</div>
</div>
<!-- 4. 口味偏好 -->
<div class="card">
<div class="card__title">👅 口味偏好</div>
<div class="sub-block">
<div class="sub-label">口味偏好</div>
<div class="tag-group"><span class="tg">偏辣</span></div>
</div>
<div class="sub-block">
<div class="sub-label">地域菜系</div>
<div class="tag-group"><span class="tg">川菜</span><span class="tg">湘菜</span></div>
</div>
</div>
<!-- 5. 禁忌关联 -->
<div class="card">
<div class="card__title">🚫 禁忌关联</div>
<div class="sub-block">
<div class="sub-label">饮食禁忌</div>
<div class="tag-group"><span class="tg warn">🌶️ 少辣</span><span class="tg warn">🧂 低盐</span><span class="tg warn">🍬 控糖</span></div>
</div>
<div class="sub-block">
<div class="sub-label">过敏原</div>
<div class="tag-group"><span class="tg danger">🥜 花生</span><span class="tg danger">🦐 海鲜</span></div>
</div>
</div>
<!-- 6. 动态生理 -->
<div class="card">
<div class="card__title">📊 动态生理<span class="readonly">只读</span></div>
<div class="info-row"><span class="i-label">日均运动量</span><span class="i-value">6000步/日</span></div>
<div class="info-row"><span class="i-label">静息心率</span><span class="i-value">68<span class="i-unit">bpm</span></span></div>
<div class="info-row"><span class="i-label">平均睡眠</span><span class="i-value">7.5h</span></div>
<div class="info-row"><span class="i-label">压力指数</span><span class="i-value"></span></div>
<div class="info-row"><span class="i-label">数据来源</span><span class="i-value">智能手环</span></div>
</div>
<!-- 7. 菜品偏好 -->
<div class="card">
<div class="card__title">🍲 菜品偏好</div>
<div class="sub-block">
<div class="sub-label">常点菜品</div>
<div class="tag-group"><span class="tg green">宫保鸡丁</span><span class="tg green">清炒西兰花</span><span class="tg green">杂粮饭</span></div>
</div>
<div class="sub-block">
<div class="sub-label">忌口菜品</div>
<div class="tag-group"><span class="tg warn">油炸花生</span></div>
</div>
</div>
<!-- 8. 妈妈味道 -->
<div class="card">
<div class="card__title">🏡 妈妈味道</div>
<div class="info-row"><span class="i-label">家乡</span><span class="i-value">四川成都</span></div>
<div class="sub-block">
<div class="sub-label">口味标签</div>
<div class="tag-group"><span class="tg warn">麻辣</span><span class="tg warn">红油</span><span class="tg warn">鲜香</span></div>
</div>
<div class="sub-block">
<div class="sub-label">思乡菜清单</div>
<div class="tag-group"><span class="tg warn">回锅肉</span><span class="tg warn">麻婆豆腐</span><span class="tg warn">水煮鱼</span></div>
</div>
</div>
<!-- 9. 餐次配比 -->
<div class="card">
<div class="card__title">⚖️ 餐次配比</div>
<div class="ratio-bar">
<span class="ratio-seg" style="width:30%; background:#FFB84C;"></span>
<span class="ratio-seg" style="width:40%; background:#52C41A;"></span>
<span class="ratio-seg" style="width:30%; background:#1677FF;"></span>
</div>
<div class="ratio-list">
<div class="ratio-item"><span class="r-dot" style="background:#FFB84C;"></span><span class="r-name">早餐</span><span class="r-val">30%</span></div>
<div class="ratio-item"><span class="r-dot" style="background:#52C41A;"></span><span class="r-name">午餐</span><span class="r-val">40%</span></div>
<div class="ratio-item"><span class="r-dot" style="background:#1677FF;"></span><span class="r-name">晚餐</span><span class="r-val">30%</span></div>
</div>
</div>
<div style="height:16px;"></div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
</script>
</body>
</html>
@@ -0,0 +1,192 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 搜索与筛选 */
.search-bar { background: #fff; padding: 10px 16px; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
.search-input { display: flex; align-items: center; gap: 8px; background: #f5f5f5; border-radius: 20px; padding: 8px 14px; }
.search-input input { flex: 1; border: none; background: none; font-size: 14px; outline: none; color: #1a1a1a; }
/* Tab */
.seg-tabs { display: flex; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
.seg-tab { flex: 1; text-align: center; padding: 12px 0; font-size: 14px; color: #666; cursor: pointer; border-bottom: 2px solid transparent; }
.seg-tab.active { color: #1677ff; border-bottom-color: #1677ff; font-weight: 600; }
.seg-tab .badge { display: inline-block; background: #e8e8e8; color: #666; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; padding: 0 4px; margin-left: 4px; vertical-align: middle; line-height: 16px; }
.seg-tab.active .badge { background: #e6f4ff; color: #1677ff; }
/* 员工卡片 */
.employee-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); display: flex; align-items: center; gap: 10px; cursor: pointer; }
.employee-card:active { transform: scale(0.98); }
.employee-card__avatar { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.employee-card__info { flex: 1; min-width: 0; }
.employee-card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.employee-card__meta { font-size: 12px; color: #999; margin-top: 3px; display: flex; gap: 8px; flex-wrap: wrap; }
.employee-card__rate { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.employee-card__rate-num { font-size: 15px; font-weight: 700; }
.employee-card__rate-num--good { color: #52c41a; }
.employee-card__rate-num--bad { color: #ff4d4f; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; }
.tag-green { background: #f6ffed; color: #52c41a; }
.tag-orange { background: #fff7e6; color: #fa8c16; }
.tag-red { background: #fff1f0; color: #ff4d4f; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="nav-back" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">员工营养管理</span>
</div>
<!-- 搜索与筛选 -->
<div class="search-bar">
<div class="search-input">
<span style="font-size:14px;">🔍</span>
<input type="text" placeholder="搜索员工姓名 / 工号" />
</div>
</div>
<!-- Tab:全部 / 达标 / 未达标 -->
<div class="seg-tabs">
<div class="seg-tab active" onclick="switchTab(this,'panel-all')">全部<span class="badge">86</span></div>
<div class="seg-tab" onclick="switchTab(this,'panel-good')">达标<span class="badge">62</span></div>
<div class="seg-tab" onclick="switchTab(this,'panel-bad')">未达标<span class="badge">24</span></div>
</div>
<div class="scroll-content">
<!-- 全部列表 -->
<div id="panel-all">
<!-- [交互] 点击跳转至: ./employee-detail.html -->
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#ffe0e0,#ffb3b3);">👩</div>
<div class="employee-card__info">
<div class="employee-card__name">张小红</div>
<div class="employee-card__meta"><span>女 · 32岁</span><span>公司机关 · 办公室</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--bad">68%</span><span class="tag tag-red">未达标</span></div>
</div>
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#e8f4ff,#bdd7ff);">👨</div>
<div class="employee-card__info">
<div class="employee-card__name">李建国</div>
<div class="employee-card__meta"><span>男 · 45岁</span><span>采油一厂 · 技术部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--bad">71%</span><span class="tag tag-orange">待干预</span></div>
</div>
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#f6ffed,#b7eb8f);">👩</div>
<div class="employee-card__info">
<div class="employee-card__name">王芳</div>
<div class="employee-card__meta"><span>女 · 28岁</span><span>信息技术中心 · 数据部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--good">92%</span><span class="tag tag-green">达标</span></div>
</div>
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#e8f4ff,#bdd7ff);">👨</div>
<div class="employee-card__info">
<div class="employee-card__name">陈伟</div>
<div class="employee-card__meta"><span>男 · 36岁</span><span>安全环保部 · 安环部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--good">88%</span><span class="tag tag-green">达标</span></div>
</div>
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#fff7e6,#ffd591);">👨</div>
<div class="employee-card__info">
<div class="employee-card__name">赵志强</div>
<div class="employee-card__meta"><span>男 · 61岁</span><span>公司机关 · 后勤部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--bad">59%</span><span class="tag tag-red">未达标</span></div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 达标列表 -->
<div id="panel-good" style="display:none;">
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#f6ffed,#b7eb8f);">👩</div>
<div class="employee-card__info">
<div class="employee-card__name">王芳</div>
<div class="employee-card__meta"><span>女 · 28岁</span><span>信息技术中心 · 数据部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--good">92%</span><span class="tag tag-green">达标</span></div>
</div>
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#e8f4ff,#bdd7ff);">👨</div>
<div class="employee-card__info">
<div class="employee-card__name">陈伟</div>
<div class="employee-card__meta"><span>男 · 36岁</span><span>安全环保部 · 安环部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--good">88%</span><span class="tag tag-green">达标</span></div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 未达标列表 -->
<div id="panel-bad" style="display:none;">
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#ffe0e0,#ffb3b3);">👩</div>
<div class="employee-card__info">
<div class="employee-card__name">张小红</div>
<div class="employee-card__meta"><span>女 · 32岁</span><span>公司机关 · 办公室</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--bad">68%</span><span class="tag tag-red">未达标</span></div>
</div>
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#e8f4ff,#bdd7ff);">👨</div>
<div class="employee-card__info">
<div class="employee-card__name">李建国</div>
<div class="employee-card__meta"><span>男 · 45岁</span><span>采油一厂 · 技术部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--bad">71%</span><span class="tag tag-orange">待干预</span></div>
</div>
<div class="employee-card" onclick="location.href='./employee-detail.html'">
<div class="employee-card__avatar" style="background:linear-gradient(135deg,#fff7e6,#ffd591);">👨</div>
<div class="employee-card__info">
<div class="employee-card__name">赵志强</div>
<div class="employee-card__meta"><span>男 · 61岁</span><span>公司机关 · 后勤部</span></div>
</div>
<div class="employee-card__rate"><span class="employee-card__rate-num employee-card__rate-num--bad">59%</span><span class="tag tag-red">未达标</span></div>
</div>
<div style="height:16px;"></div>
</div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* Tab 切换 */
function switchTab(el, panelId) {
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
['panel-all','panel-good','panel-bad'].forEach(function(id) {
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
});
}
</script>
</body>
</html>
@@ -0,0 +1,229 @@
<!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: #fff; 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; flex-shrink: 0; }
/* 状态栏 */
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
/* 滚动内容区 */
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 底部Tab栏 */
.tab-bar { height: 83px; background: #fff; border-top: 1px solid #e8e8e8; display: flex; align-items: flex-start; padding-top: 8px; flex-shrink: 0; }
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; padding: 4px 0; position: relative; }
.tab-item .tab-icon { font-size: 22px; line-height: 1; }
.tab-item .tab-label { font-size: 10px; color: #999; font-weight: 500; }
.tab-item.active .tab-label { color: #1677ff; }
.tab-badge { position: absolute; top: 2px; right: 18px; background: #ff4d4f; color: #fff; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; display: flex; align-items: center; justify-content: center; padding: 0 4px; }
/* 分段Tab */
.seg-tabs { display: flex; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
.seg-tab { flex: 1; text-align: center; padding: 12px 0; font-size: 14px; color: #666; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s; }
.seg-tab.active { color: #1677ff; border-bottom-color: #1677ff; font-weight: 600; }
.seg-tab .badge { display: inline-block; background: #e8e8e8; color: #666; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; padding: 0 4px; margin-left: 4px; vertical-align: middle; line-height: 16px; }
.seg-tab.active .badge { background: #e6f4ff; color: #1677ff; }
/* 标签 */
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.tag-blue { background: #e6f4ff; color: #1677ff; }
.tag-green { background: #f6ffed; color: #52c41a; }
.tag-orange { background: #fff7e6; color: #fa8c16; }
.tag-red { background: #fff1f0; color: #ff4d4f; }
.tag-gray { background: #f5f5f5; color: #999; }
/* 快捷入口网格 */
.quick-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 10px 16px 0; }
.quick-card { background: #fff; border-radius: 12px; padding: 14px; cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.06); display: flex; align-items: center; gap: 10px; transition: transform 0.15s; }
.quick-card:active { transform: scale(0.98); }
.quick-card__icon { width: 42px; height: 42px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.quick-card__info { flex: 1; min-width: 0; }
.quick-card__title { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.quick-card__desc { font-size: 11px; color: #999; margin-top: 2px; }
/* 咨询卡片 */
.consult-card { background: #fff; border-radius: 12px; margin: 10px 16px 0; padding: 14px; cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.06); transition: transform 0.15s; }
.consult-card:active { transform: scale(0.98); }
.cc-top { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.cc-avatar { width: 42px; height: 42px; border-radius: 50%; background: linear-gradient(135deg, #e8f4ff, #bdd7ff); display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.cc-info { flex: 1; min-width: 0; }
.cc-name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.cc-meta { font-size: 12px; color: #999; margin-top: 2px; }
.cc-bottom { display: flex; align-items: center; justify-content: space-between; }
.cc-preview { font-size: 13px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 210px; }
.cc-time { font-size: 12px; color: #bbb; flex-shrink: 0; }
.unread-dot { width: 8px; height: 8px; border-radius: 50%; background: #ff4d4f; flex-shrink: 0; }
/* 弹窗样式 */
.modal-center { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; align-items: center; justify-content: center; }
.modal-center.active { display: flex; }
.modal-box { width: 280px; background: #fff; border-radius: 16px; overflow: hidden; }
.modal-box .modal-title { padding: 20px 20px 8px; font-size: 17px; font-weight: 700; text-align: center; color: #1a1a1a; }
.modal-box .modal-body { padding: 8px 20px 20px; font-size: 14px; color: #666; text-align: center; line-height: 1.6; }
.modal-box .modal-actions { display: flex; border-top: 1px solid #f0f0f0; }
.modal-box .modal-actions button { flex: 1; padding: 14px; font-size: 16px; cursor: pointer; background: none; border: none; }
.modal-box .modal-actions button:first-child { color: #666; border-right: 1px solid #f0f0f0; }
.modal-box .modal-actions button:last-child { color: #1677ff; font-weight: 600; }
</style>
</head>
<body>
<div class="phone-shell">
<!-- 状态栏 -->
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 蓝色渐变头部区域 -->
<div style="background:linear-gradient(135deg,#1677ff,#0958d9);padding:16px 20px 20px;flex-shrink:0;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
<div>
<div style="color:rgba(255,255,255,0.75);font-size:12px;margin-bottom:2px;">你好,李营养师</div>
<div style="color:#fff;font-size:20px;font-weight:700;">工作台</div>
</div>
<!-- [交互] 点击切换营养咨询服务状态 -->
<button id="wb-pause-btn" onclick="togglePause()" style="display:flex;align-items:center;gap:6px;background:rgba(255,255,255,0.2);border:1px solid rgba(255,255,255,0.4);border-radius:20px;padding:6px 14px;color:#fff;font-size:13px;cursor:pointer;">
<span id="pause-dot" style="width:8px;height:8px;border-radius:50%;background:#52c41a;display:inline-block;flex-shrink:0;"></span>
<span id="pause-label">咨询已开启</span>
</button>
</div>
<!-- 统计数字 -->
<div style="display:flex;">
<div style="flex:1;text-align:center;"><div style="color:#fff;font-size:22px;font-weight:700;">2</div><div style="color:rgba(255,255,255,0.75);font-size:11px;margin-top:2px;">营养咨询待回复</div></div>
<div style="width:1px;background:rgba(255,255,255,0.25);margin:4px 0;"></div>
<div style="flex:1;text-align:center;"><div style="color:#fff;font-size:22px;font-weight:700;">3</div><div style="color:rgba(255,255,255,0.75);font-size:11px;margin-top:2px;">方案待跟进</div></div>
<div style="width:1px;background:rgba(255,255,255,0.25);margin:4px 0;"></div>
<div style="flex:1;text-align:center;"><div style="color:#fff;font-size:22px;font-weight:700;">5</div><div style="color:rgba(255,255,255,0.75);font-size:11px;margin-top:2px;">食谱被引用</div></div>
</div>
</div>
<div class="scroll-content">
<!-- 快捷功能入口(2 个大 tab -->
<div style="display:flex;gap:10px;margin:10px 16px 0;">
<!-- [交互] 点击跳转至: ./employee-list.html -->
<div onclick="location.href='./employee-list.html'" style="flex:1;background:#fff;border-radius:12px;padding:16px 14px;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,0.06);text-align:center;transition:transform 0.15s;">
<div style="width:48px;height:48px;border-radius:14px;background:linear-gradient(135deg,#e6f4ff,#b7d8ff);display:flex;align-items:center;justify-content:center;font-size:24px;margin:0 auto 8px;">👥</div>
<div style="font-size:15px;font-weight:600;color:#1a1a1a;">员工营养管理</div>
<div style="font-size:11px;color:#999;margin-top:4px;">达标监控 · 一对一指导</div>
</div>
<!-- [交互] 点击跳转至: ./recipe-list.html -->
<div onclick="location.href='./recipe-list.html'" style="flex:1;background:#fff;border-radius:12px;padding:16px 14px;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,0.06);text-align:center;transition:transform 0.15s;">
<div style="width:48px;height:48px;border-radius:14px;background:linear-gradient(135deg,#fff7e6,#ffd591);display:flex;align-items:center;justify-content:center;font-size:24px;margin:0 auto 8px;">🥗</div>
<div style="font-size:15px;font-weight:600;color:#1a1a1a;">我的食谱</div>
<div style="font-size:11px;color:#999;margin-top:4px;">按周发布 · 引用查看</div>
</div>
</div>
<!-- 营养咨询列表 -->
<div id="consult-list" style="margin-top:4px;">
<div class="seg-tabs">
<div class="seg-tab active" onclick="switchWorkTab(this,'wb-text-pending')">待回复<span class="badge">2</span></div>
<div class="seg-tab" onclick="switchWorkTab(this,'wb-text-ongoing')">进行中<span class="badge" style="background:#fff7e6;color:#fa8c16;">1</span></div>
<div class="seg-tab" onclick="switchWorkTab(this,'wb-text-done')">已结束</div>
</div>
<!-- 待回复列表 -->
<div id="wb-text-pending">
<!-- [交互] 点击跳转至: ./consult-chat.html -->
<div class="consult-card" onclick="location.href='./consult-chat.html'">
<div class="cc-top"><div class="cc-avatar">👩</div><div class="cc-info"><div class="cc-name">张小红</div><div class="cc-meta">女 · 32岁 · 减脂需求</div></div><span class="tag tag-red">待回复</span></div>
<div class="cc-bottom"><div class="cc-preview">食堂太油了,想请教怎么吃才能减脂...</div><div style="display:flex;align-items:center;gap:6px;"><span class="cc-time">10:23</span><span class="unread-dot"></span></div></div>
</div>
<!-- [交互] 点击跳转至: ./consult-chat.html -->
<div class="consult-card" onclick="location.href='./consult-chat.html'">
<div class="cc-top"><div class="cc-avatar">👨</div><div class="cc-info"><div class="cc-name">李建国</div><div class="cc-meta">男 · 45岁 · 高血脂</div></div><span class="tag tag-red">待回复</span></div>
<div class="cc-bottom"><div class="cc-preview">血脂偏高,日常饮食有什么忌口...</div><div style="display:flex;align-items:center;gap:6px;"><span class="cc-time">09:55</span><span class="unread-dot"></span></div></div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 进行中列表 -->
<div id="wb-text-ongoing" style="display:none;">
<!-- [交互] 点击跳转至: ./consult-chat.html -->
<div class="consult-card" onclick="location.href='./consult-chat.html'">
<div class="cc-top"><div class="cc-avatar">👨</div><div class="cc-info"><div class="cc-name">陈伟</div><div class="cc-meta">男 · 36岁 · 增肌需求</div></div><span class="tag tag-blue">进行中</span></div>
<div class="cc-bottom"><div class="cc-preview">好的,我按您给的食谱吃一周看看</div><span class="cc-time">昨天</span></div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 已结束列表 -->
<div id="wb-text-done" style="display:none;">
<!-- [交互] 点击跳转至: ./consult-chat-done.html -->
<div class="consult-card" onclick="location.href='./consult-chat-done.html'">
<div class="cc-top"><div class="cc-avatar">👨</div><div class="cc-info"><div class="cc-name">赵磊</div><div class="cc-meta">男 · 35岁</div></div><span class="tag tag-green">已完成</span></div>
<div class="cc-bottom"><div class="cc-preview">谢谢营养师,方案很实用</div><span class="cc-time">08:40</span></div>
</div>
<div style="height:16px;"></div>
</div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 服务状态切换确认弹窗 -->
<div id="modal-pause" class="modal-center">
<div class="modal-box">
<div class="modal-title" id="pause-modal-title">关闭咨询</div>
<div class="modal-body" id="pause-modal-body">是否要关闭营养咨询?关闭后,员工无法发起营养咨询。</div>
<div class="modal-actions">
<button onclick="closeModal('modal-pause')">取消</button>
<button onclick="confirmTogglePause()">确认</button>
</div>
</div>
</div>
<!-- 底部5Tab导航栏 -->
<div class="tab-bar">
<div class="tab-item active" onclick="location.href='./index.html'"><span class="tab-icon">🏥</span><span class="tab-label">工作台</span><span class="tab-badge">2</span></div>
<div class="tab-item" onclick="location.href='../knowledge/index.html'"><span class="tab-icon">📚</span><span class="tab-label">知识库</span></div>
<div class="tab-item" onclick="location.href='../../ai-assistant/index.html'"><span class="tab-icon">🤖</span><span class="tab-label">AI助手</span></div>
<div class="tab-item" onclick="location.href='../../message/index.html'"><span class="tab-icon">💬</span><span class="tab-label">消息</span><span class="tab-badge">2</span></div>
<div class="tab-item" onclick="location.href='../profile/index.html'"><span class="tab-icon">👤</span><span class="tab-label">我的</span></div>
</div>
</div>
<script>
/* 标记当前角色为营养师,供共用页面(消息、AI助手)判断返回目标 */
sessionStorage.setItem('appRole', 'nutritionist');
/**
* 切换二级Tab(待回复/进行中/已结束)
* @param {HTMLElement} el - 被点击的Tab元素
* @param {string} panelId - 目标面板ID
*/
function switchWorkTab(el, panelId) {
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
var scrollContent = el.closest('.seg-tabs').nextElementSibling;
Array.from(scrollContent.children).forEach(function(p) {
if (p.id) p.style.display = p.id === panelId ? 'block' : 'none';
});
}
/* 服务状态切换 */
var isPaused = false;
function togglePause() {
var title = isPaused ? '开启咨询' : '关闭咨询';
var body = isPaused ? '是否要开启营养咨询?开启后,员工可发起营养咨询。' : '是否要关闭营养咨询?关闭后,员工无法发起营养咨询。';
document.getElementById('pause-modal-title').textContent = title;
document.getElementById('pause-modal-body').textContent = body;
openModal('modal-pause');
}
function confirmTogglePause() {
isPaused = !isPaused;
var bg = isPaused ? '#ff4d4f' : '#52c41a';
var text = isPaused ? '咨询已关闭' : '咨询已开启';
document.getElementById('pause-dot').style.background = bg;
document.getElementById('pause-label').textContent = text;
closeModal('modal-pause');
}
function openModal(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
function closeModal(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
</script>
</body>
</html>
@@ -0,0 +1,407 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 头部 */
.plan-header { background: linear-gradient(135deg,#1677ff,#0958d9); padding: 16px 20px; color: #fff; }
.plan-header__top { display: flex; align-items: center; gap: 12px; }
.plan-header__avatar { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; flex-shrink: 0; background: rgba(255,255,255,0.25); border: 2px solid rgba(255,255,255,0.4); }
.plan-header__name { font-size: 17px; font-weight: 700; }
.plan-header__meta { font-size: 12px; color: rgba(255,255,255,0.85); margin-top: 2px; }
.plan-header__tag { margin-left: auto; font-size: 12px; padding: 4px 12px; border-radius: 14px; background: rgba(255,255,255,0.2); }
/* 卡片 */
.card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.card__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; display: flex; align-items: center; }
/* 营养目标 */
.goal-row { display: flex; }
.goal-item { flex: 1; text-align: center; }
.goal-item + .goal-item { border-left: 1px solid #f0f0f0; }
.goal-item__val { font-size: 16px; font-weight: 700; color: #1677ff; }
.goal-item__label { font-size: 11px; color: #999; margin-top: 2px; }
/* 三大营养素 */
.macro-bar { display: flex; height: 24px; border-radius: 6px; overflow: hidden; margin-bottom: 8px; }
.macro-bar__seg { display: flex; align-items: center; justify-content: center; font-size: 11px; color: #fff; font-weight: 600; }
.macro-legend { display: flex; gap: 12px; }
.macro-legend__item { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #666; }
.macro-legend__dot { width: 8px; height: 8px; border-radius: 50%; }
/* 一周饮食安排(按天折叠,每天三餐,食物含分量与热量) */
.day-card { background: #fff; border: 1px solid #e8e8e8; border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.day-card:last-child { margin-bottom: 0; }
.day-header { display: flex; align-items: center; justify-content: space-between; padding: 12px; cursor: pointer; }
.day-header__day { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.day-header__summary { font-size: 12px; color: #999; }
.day-header__kcal { font-size: 12px; color: #fa8c16; font-weight: 600; }
.day-header .arrow { transition: transform .2s; flex-shrink: 0; }
.day-header .arrow.open { transform: rotate(180deg); }
.day-body { display: none; padding: 0 12px 12px; }
.day-body.open { display: block; }
.meal-block { background: #fafafa; border-radius: 8px; padding: 10px; margin-bottom: 8px; }
.meal-block:last-child { margin-bottom: 0; }
.meal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.meal-head__name { font-size: 13px; font-weight: 600; color: #1a1a1a; }
.meal-head__subtotal { font-size: 12px; color: #fa8c16; font-weight: 600; }
.food-row { display: flex; align-items: center; gap: 6px; padding: 6px 0; border-bottom: 1px dashed #f0f0f0; }
.food-row:last-child { border-bottom: none; }
.food-row__name { flex: 1; font-size: 13px; color: #333; }
.food-row__amount { font-size: 12px; color: #666; width: 44px; text-align: right; flex-shrink: 0; }
.food-row__kcal { font-size: 12px; color: #fa8c16; font-weight: 600; width: 56px; text-align: right; flex-shrink: 0; }
/* 执行进度 */
.checkin-row { display: flex; gap: 6px; }
.checkin-day { flex: 1; text-align: center; }
.checkin-day__dot { width: 26px; height: 26px; border-radius: 50%; margin: 0 auto; display: flex; align-items: center; justify-content: center; font-size: 12px; }
.checkin-day__dot--done { background: #52c41a; color: #fff; }
.checkin-day__dot--today { background: #e6f4ff; color: #1677ff; border: 1px solid #1677ff; }
.checkin-day__dot--pending { background: #f5f5f5; color: #bbb; }
.checkin-day__label { font-size: 10px; color: #999; margin-top: 4px; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.tag-red { background: #fff1f0; color: #ff4d4f; }
.tag-orange { background: #fff7e6; color: #fa8c16; }
/* 底部弹层 */
.sheet-mask { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 300; align-items: flex-end; justify-content: center; }
.sheet-mask.active { display: flex; }
.sheet-panel { width: 100%; max-height: 80%; background: #fff; border-radius: 16px 16px 0 0; display: flex; flex-direction: column; overflow: hidden; }
.sheet-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 0.5px solid #f0f0f0; flex-shrink: 0; }
.sheet-header .s-cancel { font-size: 14px; color: #999; cursor: pointer; background: none; border: none; }
.sheet-header .s-title { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.sheet-header .s-save { font-size: 14px; color: #1677ff; font-weight: 600; cursor: pointer; background: none; border: none; }
.sheet-body { padding: 12px 16px 20px; overflow-y: auto; flex: 1; }
.sheet-body::-webkit-scrollbar { display: none; }
.sheet-tip { font-size: 12px; color: #999; background: #fafafa; border-radius: 8px; padding: 10px 12px; line-height: 1.6; margin-bottom: 12px; }
.adj-meal { margin-bottom: 14px; }
.adj-meal__label { font-size: 13px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.adj-dish { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; padding: 5px 10px; border-radius: 14px; background: #f0f6ff; color: #1677ff; margin: 0 6px 6px 0; cursor: pointer; }
.adj-dish .x { color: #999; font-size: 12px; line-height: 1; }
.adj-add { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; padding: 5px 12px; border-radius: 14px; border: 1px dashed #1677ff; color: #1677ff; margin-bottom: 6px; cursor: pointer; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="nav-back" onclick="location.href='./employee-detail.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">营养方案详情</span>
</div>
<div class="scroll-content">
<!-- 头部 -->
<div class="plan-header">
<div class="plan-header__top">
<div class="plan-header__avatar">👩</div>
<div>
<div class="plan-header__name">张小红</div>
<div class="plan-header__meta">减脂期一周饮食方案 · 女 32岁</div>
</div>
<span class="plan-header__tag">执行中</span>
</div>
</div>
<!-- 方案依据(健康档案摘要) -->
<div class="card" style="border-left:3px solid #1677ff;">
<div class="card__title">🎯 方案依据<span style="font-size:12px;color:#1677ff;font-weight:400;margin-left:auto;cursor:pointer;" onclick="location.href='./employee-health.html'">查看完整健康档案 </span></div>
<div style="display:flex;flex-wrap:wrap;gap:6px;">
<span class="tag tag-orange">尿酸 380 偏高</span>
<span class="tag tag-red">胃炎</span>
<span class="tag tag-red">高尿酸</span>
<span class="tag tag-red">过敏原:花生</span>
<span class="tag tag-red">过敏原:海鲜</span>
<span class="tag tag-orange">少辣 · 低盐 · 控糖</span>
</div>
</div>
<!-- 营养目标 -->
<div class="card">
<div class="card__title">营养目标</div>
<div class="goal-row">
<div class="goal-item"><div class="goal-item__val">1400</div><div class="goal-item__label">热量 kcal/日</div></div>
<div class="goal-item"><div class="goal-item__val">75</div><div class="goal-item__label">蛋白质 g</div></div>
<div class="goal-item"><div class="goal-item__val">30</div><div class="goal-item__label">脂肪 g</div></div>
<div class="goal-item"><div class="goal-item__val">190</div><div class="goal-item__label">碳水 g</div></div>
</div>
</div>
<!-- 三大营养素占比 -->
<div class="card">
<div class="card__title">三大营养素占比</div>
<div class="macro-bar">
<div class="macro-bar__seg" style="width:25%;background:#1677ff;">蛋白25%</div>
<div class="macro-bar__seg" style="width:20%;background:#fa8c16;">脂肪20%</div>
<div class="macro-bar__seg" style="width:55%;background:#52c41a;">碳水55%</div>
</div>
<div class="macro-legend">
<span class="macro-legend__item"><span class="macro-legend__dot" style="background:#1677ff;"></span>蛋白质</span>
<span class="macro-legend__item"><span class="macro-legend__dot" style="background:#fa8c16;"></span>脂肪</span>
<span class="macro-legend__item"><span class="macro-legend__dot" style="background:#52c41a;"></span>碳水化合物</span>
</div>
</div>
<!-- 饮食禁忌 -->
<div class="card">
<div class="card__title">饮食禁忌</div>
<div style="display:flex;gap:6px;">
<span class="tag tag-red">高糖</span>
<span class="tag tag-red">高脂</span>
<span class="tag tag-orange">高盐</span>
</div>
</div>
<!-- 一周饮食安排 -->
<div class="card">
<div class="card__title">一周饮食安排<span style="font-size:10px;color:#52c41a;font-weight:400;background:#f6ffed;padding:2px 8px;border-radius:8px;margin-left:8px;">系统生成</span><span style="font-size:12px;color:#1677ff;font-weight:400;margin-left:auto;cursor:pointer;" onclick="openSheet('sheet-adjust')">微调食谱 </span></div>
<div id="day-list"></div>
</div>
<!-- 执行进度 -->
<div class="card">
<div class="card__title">执行进度<span style="font-size:12px;color:#999;font-weight:400;margin-left:8px;">第 3 天 / 7 天</span></div>
<div class="checkin-row">
<div class="checkin-day"><div class="checkin-day__dot checkin-day__dot--done"></div><div class="checkin-day__label">周一</div></div>
<div class="checkin-day"><div class="checkin-day__dot checkin-day__dot--done"></div><div class="checkin-day__label">周二</div></div>
<div class="checkin-day"><div class="checkin-day__dot checkin-day__dot--today">3</div><div class="checkin-day__label">周三</div></div>
<div class="checkin-day"><div class="checkin-day__dot checkin-day__dot--pending">4</div><div class="checkin-day__label">周四</div></div>
<div class="checkin-day"><div class="checkin-day__dot checkin-day__dot--pending">5</div><div class="checkin-day__label">周五</div></div>
<div class="checkin-day"><div class="checkin-day__dot checkin-day__dot--pending">6</div><div class="checkin-day__label">周六</div></div>
<div class="checkin-day"><div class="checkin-day__dot checkin-day__dot--pending">7</div><div class="checkin-day__label">周日</div></div>
</div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 微调食谱底部弹层 -->
<div id="sheet-adjust" class="sheet-mask" onclick="if(event.target===this)closeSheet('sheet-adjust')">
<div class="sheet-panel" onclick="event.stopPropagation()">
<div class="sheet-header">
<button class="s-cancel" onclick="closeSheet('sheet-adjust')">取消</button>
<span class="s-title">微调一周食谱</span>
<button class="s-save" onclick="closeSheet('sheet-adjust');alert('食谱已微调(原型演示)')">保存</button>
</div>
<div class="sheet-body">
<div class="sheet-tip">系统已根据方案参数(1400kcal/日 · 蛋白25% · 脂肪20% · 碳水55% · 禁忌:高糖、高脂、高嘌呤)生成一周专属食谱,可替换任意餐品。</div>
<div id="adjust-list"></div>
</div>
</div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* ===== 一周专属食谱数据(每天三餐,每餐食物含分量与热量) ===== */
var WEEK = [
{ day: '周一', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '全麦面包', amount: 80, unit: 'g', kcal: 200 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '清蒸鲈鱼', amount: 150, unit: 'g', kcal: 160 },
{ name: '杂粮饭', amount: 200, unit: 'g', kcal: 240 },
{ name: '蒜蓉西兰花', amount: 150, unit: 'g', kcal: 60 },
{ name: '紫菜蛋花汤', amount: 200, unit: 'ml', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '香煎鸡胸', amount: 120, unit: 'g', kcal: 200 },
{ name: '蔬菜沙拉', amount: 200, unit: 'g', kcal: 80 },
{ name: '小米粥', amount: 200, unit: 'g', kcal: 120 }
]}
]},
{ day: '周二', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '燕麦牛奶', amount: 300, unit: 'ml', kcal: 220 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '白灼虾', amount: 150, unit: 'g', kcal: 140 },
{ name: '糙米饭', amount: 200, unit: 'g', kcal: 230 },
{ name: '清炒时蔬', amount: 150, unit: 'g', kcal: 55 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '蔬菜豆腐汤', amount: 300, unit: 'g', kcal: 150 },
{ name: '杂粮馒头', amount: 100, unit: 'g', kcal: 220 }
]}
]},
{ day: '周三', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '豆浆', amount: 250, unit: 'ml', kcal: 80 },
{ name: '蒸红薯', amount: 100, unit: 'g', kcal: 90 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '卤牛肉', amount: 100, unit: 'g', kcal: 160 },
{ name: '荞麦面', amount: 200, unit: 'g', kcal: 230 },
{ name: '凉拌黄瓜', amount: 150, unit: 'g', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '清炒时蔬', amount: 200, unit: 'g', kcal: 60 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '菌菇汤', amount: 200, unit: 'g', kcal: 50 }
]}
]},
{ day: '周四', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '杂粮粥', amount: 300, unit: 'g', kcal: 200 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '香煎鸡胸', amount: 150, unit: 'g', kcal: 250 },
{ name: '藜麦饭', amount: 150, unit: 'g', kcal: 180 },
{ name: '西兰花', amount: 150, unit: 'g', kcal: 60 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '虾仁沙拉', amount: 200, unit: 'g', kcal: 150 },
{ name: '全麦面包', amount: 50, unit: 'g', kcal: 120 }
]}
]},
{ day: '周五', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '全麦面包', amount: 80, unit: 'g', kcal: 200 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 },
{ name: '香蕉', amount: 100, unit: 'g', kcal: 90 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '清蒸鱼', amount: 150, unit: 'g', kcal: 160 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '凉拌木耳', amount: 100, unit: 'g', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '菌菇汤', amount: 300, unit: 'g', kcal: 120 },
{ name: '杂粮饭', amount: 150, unit: 'g', kcal: 180 }
]}
]},
{ day: '周六', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '燕麦', amount: 50, unit: 'g', kcal: 190 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '白灼虾', amount: 150, unit: 'g', kcal: 140 },
{ name: '杂粮饭', amount: 200, unit: 'g', kcal: 240 },
{ name: '蔬菜沙拉', amount: 150, unit: 'g', kcal: 60 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '蔬菜沙拉', amount: 200, unit: 'g', kcal: 80 },
{ name: '鸡胸肉', amount: 100, unit: 'g', kcal: 170 },
{ name: '玉米', amount: 100, unit: 'g', kcal: 110 }
]}
]},
{ day: '周日', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '豆浆', amount: 250, unit: 'ml', kcal: 80 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '卤牛肉', amount: 100, unit: 'g', kcal: 160 },
{ name: '糙米饭', amount: 200, unit: 'g', kcal: 230 },
{ name: '清炒时蔬', amount: 150, unit: 'g', kcal: 55 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '鸡胸沙拉', amount: 250, unit: 'g', kcal: 200 },
{ name: '小米粥', amount: 200, unit: 'g', kcal: 120 }
]}
]}
];
/* 计算某餐总热量 */
function mealSubtotal(di, mi) {
return WEEK[di].meals[mi].foods.reduce(function(s, f) { return s + f.kcal; }, 0);
}
/* 计算某天总热量 */
function dayTotal(di) {
return WEEK[di].meals.reduce(function(s, m, mi) { return s + mealSubtotal(di, mi); }, 0);
}
/* 某天三餐小计摘要 */
function daySummary(di) {
return WEEK[di].meals.map(function(m, mi) { return m.name.charAt(0) + mealSubtotal(di, mi); }).join(' · ');
}
/* 渲染一周饮食安排(折叠卡片,只读) */
function renderDayList() {
var html = '';
WEEK.forEach(function(day, di) {
html += '<div class="day-card">' +
'<div class="day-header" onclick="toggleDay(' + di + ')">' +
'<span class="day-header__day">' + day.day + '</span>' +
'<span class="day-header__summary">' + daySummary(di) + '</span>' +
'<span class="day-header__kcal">' + dayTotal(di) + ' kcal</span>' +
'<svg class="arrow" id="arrow-' + di + '" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#999" stroke-width="2"><path d="M4 6l4 4 4-4"/></svg>' +
'</div>' +
'<div class="day-body" id="day-body-' + di + '">' +
day.meals.map(function(m, mi) {
return '<div class="meal-block">' +
'<div class="meal-head"><span class="meal-head__name">' + m.icon + ' ' + m.name + '</span><span class="meal-head__subtotal">小计 ' + mealSubtotal(di, mi) + ' kcal</span></div>' +
m.foods.map(function(f) {
return '<div class="food-row"><span class="food-row__name">' + f.name + '</span><span class="food-row__amount">' + f.amount + f.unit + '</span><span class="food-row__kcal">' + f.kcal + ' kcal</span></div>';
}).join('') +
'</div>';
}).join('') +
'</div>' +
'</div>';
});
document.getElementById('day-list').innerHTML = html;
/* 默认展开周一 */
document.getElementById('day-body-0').classList.add('open');
document.getElementById('arrow-0').classList.add('open');
}
/* 渲染微调弹层(食物带分量与热量) */
function renderAdjustList() {
var html = '';
WEEK.forEach(function(day) {
html += '<div class="adj-meal"><div class="adj-meal__label">' + day.day + '</div>';
day.meals.forEach(function(m) {
m.foods.forEach(function(f) {
html += '<span class="adj-dish">' + m.name.charAt(0) + '·' + f.name + ' ' + f.amount + f.unit + '·' + f.kcal + 'kcal<span class="x">×</span></span>';
});
});
html += '<span class="adj-add"> 添加餐品</span></div>';
});
document.getElementById('adjust-list').innerHTML = html;
}
/* 折叠/展开某天 */
function toggleDay(di) {
var body = document.getElementById('day-body-' + di);
var arrow = document.getElementById('arrow-' + di);
body.classList.toggle('open');
arrow.classList.toggle('open');
}
/* 打开底部弹层 */
function openSheet(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
/* 关闭底部弹层 */
function closeSheet(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
/* 初始化渲染 */
renderDayList();
renderAdjustList();
</script>
</body>
</html>
@@ -0,0 +1,408 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 表单 */
.form-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.form-card__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 12px; }
.form-item { margin-bottom: 14px; }
.form-item:last-child { margin-bottom: 0; }
.form-item__label { font-size: 13px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.form-item__label .req { color: #ff4d4f; margin-left: 2px; }
.form-input { width: 100%; border: 1px solid #e8e8e8; border-radius: 8px; padding: 10px 12px; font-size: 14px; outline: none; color: #1a1a1a; }
.form-input:focus { border-color: #1677ff; }
/* 分段选择 */
.seg-picker { display: flex; background: #f5f5f5; border-radius: 8px; padding: 3px; }
.seg-picker__opt { flex: 1; text-align: center; padding: 7px 0; font-size: 13px; color: #666; border-radius: 6px; cursor: pointer; }
.seg-picker__opt.active { background: #fff; color: #1677ff; font-weight: 600; box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
/* 日期范围 */
.date-range { display: flex; align-items: center; gap: 8px; }
.date-range .form-input { flex: 1; text-align: center; }
.date-range .sep { color: #999; flex-shrink: 0; }
/* 餐次配置 */
.meal-item { border: 1px solid #e8e8e8; border-radius: 10px; padding: 12px; margin-bottom: 10px; }
.meal-item:last-child { margin-bottom: 0; }
.meal-item__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.meal-item__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.meal-item__add { font-size: 12px; color: #1677ff; cursor: pointer; }
.meal-item__dishes { display: flex; flex-wrap: wrap; gap: 6px; }
.dish-chip { font-size: 11px; padding: 4px 10px; border-radius: 12px; background: #f0f6ff; color: #1677ff; }
/* 营养汇总 */
.nutri-summary { display: flex; gap: 8px; }
.nutri-summary__item { flex: 1; text-align: center; background: #fafafa; border-radius: 8px; padding: 8px 0; }
.nutri-summary__val { font-size: 14px; font-weight: 700; color: #1677ff; }
.nutri-summary__label { font-size: 10px; color: #999; margin-top: 2px; }
/* 底部操作 */
.bottom-bar { background: #fff; border-top: 1px solid #f0f0f0; padding: 10px 16px 14px; flex-shrink: 0; display: flex; gap: 10px; }
.btn-draft { flex: 1; padding: 12px; background: #fff; color: #1677ff; border: 1px solid #1677ff; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; }
.btn-pub { flex: 1; padding: 12px; background: #1677ff; color: #fff; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; }
.modal-center { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; align-items: center; justify-content: center; }
.modal-center.active { display: flex; }
.modal-box { width: 280px; background: #fff; border-radius: 16px; overflow: hidden; }
.modal-box .modal-title { padding: 20px 20px 8px; font-size: 17px; font-weight: 700; text-align: center; color: #1a1a1a; }
.modal-box .modal-body { padding: 8px 20px 20px; font-size: 14px; color: #666; text-align: center; line-height: 1.6; }
.modal-box .modal-actions { display: flex; border-top: 1px solid #f0f0f0; }
.modal-box .modal-actions button { flex: 1; padding: 14px; font-size: 16px; cursor: pointer; background: none; border: none; }
.modal-box .modal-actions button:first-child { color: #666; border-right: 1px solid #f0f0f0; }
.modal-box .modal-actions button:last-child { color: #1677ff; font-weight: 600; }
/* 按周折叠卡片 */
.day-card { background: #fff; border: 1px solid #e8e8e8; border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.day-card:last-child { margin-bottom: 0; }
.day-header { display: flex; align-items: center; justify-content: space-between; padding: 12px; cursor: pointer; }
.day-header__day { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.day-header__summary { font-size: 12px; color: #999; }
.day-header__kcal { font-size: 12px; color: #fa8c16; font-weight: 600; }
.day-header .arrow { transition: transform .2s; flex-shrink: 0; }
.day-header .arrow.open { transform: rotate(180deg); }
.day-body { display: none; padding: 0 12px 12px; }
.day-body.open { display: block; }
/* 每餐块 */
.meal-block { background: #fafafa; border-radius: 8px; padding: 10px; margin-bottom: 8px; }
.meal-block:last-child { margin-bottom: 0; }
.meal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.meal-head__name { font-size: 13px; font-weight: 600; color: #1a1a1a; }
.meal-head__subtotal { font-size: 12px; color: #fa8c16; font-weight: 600; }
/* 食物行 */
.food-row { display: flex; align-items: center; gap: 6px; padding: 6px 0; border-bottom: 1px dashed #f0f0f0; }
.food-row:last-child { border-bottom: none; }
.food-row__name { flex: 1; font-size: 13px; color: #333; }
.food-row__input { width: 52px; border: 1px solid #e8e8e8; border-radius: 6px; padding: 4px 6px; font-size: 12px; text-align: center; outline: none; color: #333; background: #fff; }
.food-row__input:focus { border-color: #1677ff; }
.food-row__unit { font-size: 11px; color: #999; }
.food-row__del { width: 20px; height: 20px; border-radius: 50%; border: none; background: #f0f0f0; color: #999; font-size: 12px; cursor: pointer; flex-shrink: 0; line-height: 1; }
/* 添加食物 */
.add-food { display: flex; align-items: center; justify-content: center; gap: 4px; padding: 6px 0; font-size: 12px; color: #1677ff; border: 1px dashed #1677ff; border-radius: 8px; cursor: pointer; margin-top: 8px; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="nav-back" onclick="location.href='./recipe-list.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">制定食谱</span>
</div>
<div class="scroll-content">
<!-- 基本信息 -->
<div class="form-card">
<div class="form-card__title">基本信息</div>
<div class="form-item">
<div class="form-item__label">食谱名称<span class="req">*</span></div>
<input class="form-input" type="text" placeholder="如:机关减脂周菜单(第 38 周)" />
</div>
<div class="form-item">
<div class="form-item__label">食谱类型<span class="req">*</span></div>
<div class="seg-picker">
<div class="seg-picker__opt active" onclick="segPick(this)">周菜单</div>
<div class="seg-picker__opt" onclick="segPick(this)">季节菜单</div>
</div>
</div>
<div class="form-item">
<div class="form-item__label">发布周期<span class="req">*</span></div>
<div class="date-range">
<input class="form-input" type="text" value="2026-09-21" />
<span class="sep"></span>
<input class="form-input" type="text" value="2026-09-27" />
</div>
</div>
<div class="form-item">
<div class="form-item__label">适用食堂</div>
<div class="seg-picker">
<div class="seg-picker__opt active" onclick="segPick(this)">全部食堂</div>
<div class="seg-picker__opt" onclick="segPick(this)">指定食堂</div>
</div>
</div>
</div>
<!-- 餐次配置(按周 · 每天三餐) -->
<div class="form-card">
<div class="form-card__title">餐次配置(按周 · 每天三餐)</div>
<div id="day-list"></div>
</div>
<!-- 营养汇总 -->
<div class="form-card">
<div class="form-card__title">营养汇总(日均估算)</div>
<div class="nutri-summary">
<div class="nutri-summary__item"><div class="nutri-summary__val">1380</div><div class="nutri-summary__label">热量 kcal</div></div>
<div class="nutri-summary__item"><div class="nutri-summary__val">78</div><div class="nutri-summary__label">蛋白质 g</div></div>
<div class="nutri-summary__item"><div class="nutri-summary__val">32</div><div class="nutri-summary__label">脂肪 g</div></div>
<div class="nutri-summary__item"><div class="nutri-summary__val">188</div><div class="nutri-summary__label">碳水 g</div></div>
</div>
</div>
<div style="height:16px;"></div>
</div>
<!-- 底部操作 -->
<div class="bottom-bar">
<button class="btn-draft" onclick="location.href='./recipe-list.html'">存为草稿</button>
<button class="btn-pub" onclick="openModal('modal-pub')">发布食谱</button>
</div>
<!-- 发布确认弹窗 -->
<div id="modal-pub" class="modal-center">
<div class="modal-box">
<div class="modal-title">发布食谱</div>
<div class="modal-body">发布后该食谱将推送给适用食堂,食堂可引用至食谱排期。确认发布?</div>
<div class="modal-actions">
<button onclick="closeModal('modal-pub')">取消</button>
<button onclick="closeModal('modal-pub');location.href='./recipe-list.html'">确认发布</button>
</div>
</div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* 分段选择 */
function segPick(el) {
el.parentElement.querySelectorAll('.seg-picker__opt').forEach(function(o) { o.classList.remove('active'); });
el.classList.add('active');
}
/* ===== 一周食谱数据(每天三餐,每餐食物含摄入量与热量) ===== */
var WEEK = [
{ day: '周一', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '全麦面包', amount: 80, unit: 'g', kcal: 200 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '清蒸鲈鱼', amount: 150, unit: 'g', kcal: 160 },
{ name: '杂粮饭', amount: 200, unit: 'g', kcal: 240 },
{ name: '蒜蓉西兰花', amount: 150, unit: 'g', kcal: 60 },
{ name: '紫菜蛋花汤', amount: 200, unit: 'ml', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '香煎鸡胸', amount: 120, unit: 'g', kcal: 200 },
{ name: '蔬菜沙拉', amount: 200, unit: 'g', kcal: 80 },
{ name: '小米粥', amount: 200, unit: 'g', kcal: 120 }
]}
]},
{ day: '周二', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '燕麦牛奶', amount: 300, unit: 'ml', kcal: 220 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '白灼虾', amount: 150, unit: 'g', kcal: 140 },
{ name: '糙米饭', amount: 200, unit: 'g', kcal: 230 },
{ name: '清炒时蔬', amount: 150, unit: 'g', kcal: 55 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '蔬菜豆腐汤', amount: 300, unit: 'g', kcal: 150 },
{ name: '杂粮馒头', amount: 100, unit: 'g', kcal: 220 }
]}
]},
{ day: '周三', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '豆浆', amount: 250, unit: 'ml', kcal: 80 },
{ name: '蒸红薯', amount: 100, unit: 'g', kcal: 90 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '卤牛肉', amount: 100, unit: 'g', kcal: 160 },
{ name: '荞麦面', amount: 200, unit: 'g', kcal: 230 },
{ name: '凉拌黄瓜', amount: 150, unit: 'g', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '清炒时蔬', amount: 200, unit: 'g', kcal: 60 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '菌菇汤', amount: 200, unit: 'g', kcal: 50 }
]}
]},
{ day: '周四', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '杂粮粥', amount: 300, unit: 'g', kcal: 200 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '香煎鸡胸', amount: 150, unit: 'g', kcal: 250 },
{ name: '藜麦饭', amount: 150, unit: 'g', kcal: 180 },
{ name: '西兰花', amount: 150, unit: 'g', kcal: 60 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '虾仁沙拉', amount: 200, unit: 'g', kcal: 150 },
{ name: '全麦面包', amount: 50, unit: 'g', kcal: 120 }
]}
]},
{ day: '周五', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '全麦面包', amount: 80, unit: 'g', kcal: 200 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 },
{ name: '香蕉', amount: 100, unit: 'g', kcal: 90 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '清蒸鱼', amount: 150, unit: 'g', kcal: 160 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '凉拌木耳', amount: 100, unit: 'g', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '菌菇汤', amount: 300, unit: 'g', kcal: 120 },
{ name: '杂粮饭', amount: 150, unit: 'g', kcal: 180 }
]}
]},
{ day: '周六', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '燕麦', amount: 50, unit: 'g', kcal: 190 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '白灼虾', amount: 150, unit: 'g', kcal: 140 },
{ name: '杂粮饭', amount: 200, unit: 'g', kcal: 240 },
{ name: '蔬菜沙拉', amount: 150, unit: 'g', kcal: 60 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '蔬菜沙拉', amount: 200, unit: 'g', kcal: 80 },
{ name: '鸡胸肉', amount: 100, unit: 'g', kcal: 170 },
{ name: '玉米', amount: 100, unit: 'g', kcal: 110 }
]}
]},
{ day: '周日', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '豆浆', amount: 250, unit: 'ml', kcal: 80 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '卤牛肉', amount: 100, unit: 'g', kcal: 160 },
{ name: '糙米饭', amount: 200, unit: 'g', kcal: 230 },
{ name: '清炒时蔬', amount: 150, unit: 'g', kcal: 55 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '鸡胸沙拉', amount: 250, unit: 'g', kcal: 200 },
{ name: '小米粥', amount: 200, unit: 'g', kcal: 120 }
]}
]}
];
/* 计算某餐总热量 */
function mealSubtotal(di, mi) {
return WEEK[di].meals[mi].foods.reduce(function(s, f) { return s + (parseInt(f.kcal) || 0); }, 0);
}
/* 计算某天总热量 */
function dayTotal(di) {
return WEEK[di].meals.reduce(function(s, m, mi) { return s + mealSubtotal(di, mi); }, 0);
}
/* 某天三餐小计摘要 */
function daySummary(di) {
return WEEK[di].meals.map(function(m, mi) { return m.name.charAt(0) + mealSubtotal(di, mi); }).join(' · ');
}
/* 渲染单个食物行 */
function renderFood(di, mi, fi) {
var f = WEEK[di].meals[mi].foods[fi];
return '<div class="food-row">' +
'<span class="food-row__name">' + f.name + '</span>' +
'<input class="food-row__input" type="text" value="' + f.amount + '" oninput="updateAmount(' + di + ',' + mi + ',' + fi + ',this)" />' +
'<span class="food-row__unit">' + f.unit + '</span>' +
'<input class="food-row__input" type="text" value="' + f.kcal + '" oninput="updateKcal(' + di + ',' + mi + ',' + fi + ',this)" />' +
'<span class="food-row__unit">kcal</span>' +
'<button class="food-row__del" onclick="removeFood(' + di + ',' + mi + ',' + fi + ')">×</button>' +
'</div>';
}
/* 渲染某餐块 */
function renderMeal(di, mi) {
var m = WEEK[di].meals[mi];
var html = '<div class="meal-block">' +
'<div class="meal-head"><span class="meal-head__name">' + m.icon + ' ' + m.name + '</span><span class="meal-head__subtotal" id="meal-sub-' + di + '-' + mi + '">小计 ' + mealSubtotal(di, mi) + ' kcal</span></div>';
m.foods.forEach(function(f, fi) { html += renderFood(di, mi, fi); });
html += '<div class="add-food" onclick="addFood(' + di + ',' + mi + ')"> 添加食物</div></div>';
return html;
}
/* 渲染整周折叠卡片 */
function renderDayList() {
var html = '';
WEEK.forEach(function(day, di) {
html += '<div class="day-card">' +
'<div class="day-header" onclick="toggleDay(' + di + ')">' +
'<span class="day-header__day">' + day.day + '</span>' +
'<span class="day-header__summary">' + daySummary(di) + '</span>' +
'<span class="day-header__kcal" id="day-kcal-' + di + '">' + dayTotal(di) + ' kcal</span>' +
'<svg class="arrow" id="arrow-' + di + '" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#999" stroke-width="2"><path d="M4 6l4 4 4-4"/></svg>' +
'</div>' +
'<div class="day-body" id="day-body-' + di + '">' +
day.meals.map(function(m, mi) { return renderMeal(di, mi); }).join('') +
'</div>' +
'</div>';
});
document.getElementById('day-list').innerHTML = html;
/* 默认展开周一 */
document.getElementById('day-body-0').classList.add('open');
document.getElementById('arrow-0').classList.add('open');
}
/* 折叠/展开某天 */
function toggleDay(di) {
var body = document.getElementById('day-body-' + di);
var arrow = document.getElementById('arrow-' + di);
body.classList.toggle('open');
arrow.classList.toggle('open');
}
/* 更新摄入量 */
function updateAmount(di, mi, fi, el) {
WEEK[di].meals[mi].foods[fi].amount = el.value;
}
/* 更新热量并重算小计与总计 */
function updateKcal(di, mi, fi, el) {
WEEK[di].meals[mi].foods[fi].kcal = parseInt(el.value) || 0;
document.getElementById('meal-sub-' + di + '-' + mi).textContent = '小计 ' + mealSubtotal(di, mi) + ' kcal';
document.getElementById('day-kcal-' + di).textContent = dayTotal(di) + ' kcal';
}
/* 添加食物 */
function addFood(di, mi) {
WEEK[di].meals[mi].foods.push({ name: '新食物', amount: 100, unit: 'g', kcal: 100 });
var body = document.getElementById('day-body-' + di);
body.innerHTML = WEEK[di].meals.map(function(m, mi2) { return renderMeal(di, mi2); }).join('');
}
/* 删除食物 */
function removeFood(di, mi, fi) {
WEEK[di].meals[mi].foods.splice(fi, 1);
var body = document.getElementById('day-body-' + di);
body.innerHTML = WEEK[di].meals.map(function(m, mi2) { return renderMeal(di, mi2); }).join('');
document.getElementById('day-kcal-' + di).textContent = dayTotal(di) + ' kcal';
}
/* 初始化渲染 */
renderDayList();
function openModal(id) { var m = document.getElementById(id); if (m) m.classList.add('active'); }
function closeModal(id) { var m = document.getElementById(id); if (m) m.classList.remove('active'); }
</script>
</body>
</html>
@@ -0,0 +1,309 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 头部 */
.recipe-header { background: linear-gradient(135deg,#1677ff,#0958d9); padding: 16px 20px; color: #fff; }
.recipe-header__name { font-size: 18px; font-weight: 700; }
.recipe-header__meta { font-size: 12px; color: rgba(255,255,255,0.85); margin-top: 6px; }
.recipe-header__tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.recipe-header__tag { font-size: 11px; padding: 3px 10px; border-radius: 12px; background: rgba(255,255,255,0.2); }
/* 卡片 */
.card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.card__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; }
/* 引用统计 */
.cite-stat { display: flex; gap: 8px; }
.cite-stat__item { flex: 1; text-align: center; background: #fafafa; border-radius: 8px; padding: 10px 0; }
.cite-stat__val { font-size: 16px; font-weight: 700; color: #1677ff; }
.cite-stat__label { font-size: 11px; color: #999; margin-top: 2px; }
/* 食堂引用列表 */
.canteen-item { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid #f5f5f5; }
.canteen-item:last-child { border-bottom: none; }
.canteen-item__icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.canteen-item__info { flex: 1; min-width: 0; }
.canteen-item__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.canteen-item__meta { font-size: 12px; color: #999; margin-top: 2px; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.tag-green { background: #f6ffed; color: #52c41a; }
.tag-orange { background: #fff7e6; color: #fa8c16; }
/* 一周餐次安排(按天折叠,每天三餐,食物含摄入量与热量) */
.day-card { background: #fff; border: 1px solid #e8e8e8; border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.day-card:last-child { margin-bottom: 0; }
.day-header { display: flex; align-items: center; justify-content: space-between; padding: 12px; cursor: pointer; }
.day-header__day { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.day-header__summary { font-size: 12px; color: #999; }
.day-header__kcal { font-size: 12px; color: #fa8c16; font-weight: 600; }
.day-header .arrow { transition: transform .2s; flex-shrink: 0; }
.day-header .arrow.open { transform: rotate(180deg); }
.day-body { display: none; padding: 0 12px 12px; }
.day-body.open { display: block; }
.meal-block { background: #fafafa; border-radius: 8px; padding: 10px; margin-bottom: 8px; }
.meal-block:last-child { margin-bottom: 0; }
.meal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.meal-head__name { font-size: 13px; font-weight: 600; color: #1a1a1a; }
.meal-head__subtotal { font-size: 12px; color: #fa8c16; font-weight: 600; }
.food-row { display: flex; align-items: center; gap: 6px; padding: 6px 0; border-bottom: 1px dashed #f0f0f0; }
.food-row:last-child { border-bottom: none; }
.food-row__name { flex: 1; font-size: 13px; color: #333; }
.food-row__amount { font-size: 12px; color: #666; width: 44px; text-align: right; flex-shrink: 0; }
.food-row__kcal { font-size: 12px; color: #fa8c16; font-weight: 600; width: 56px; text-align: right; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="nav-back" onclick="location.href='./recipe-list.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">食谱详情</span>
</div>
<div class="scroll-content">
<!-- 头部 -->
<div class="recipe-header">
<div class="recipe-header__name">机关减脂周菜单(第 37 周)</div>
<div class="recipe-header__meta">周菜单 · 2026-09-14 至 09-20 · 李营养师</div>
<div class="recipe-header__tags">
<span class="recipe-header__tag">低脂</span>
<span class="recipe-header__tag">均衡营养</span>
<span class="recipe-header__tag">已发布</span>
</div>
</div>
<!-- 引用情况 -->
<div class="card">
<div class="card__title">引用情况</div>
<div class="cite-stat">
<div class="cite-stat__item"><div class="cite-stat__val">5</div><div class="cite-stat__label">已引用食堂</div></div>
<div class="cite-stat__item"><div class="cite-stat__val">3</div><div class="cite-stat__label">待确认</div></div>
<div class="cite-stat__item"><div class="cite-stat__val">0</div><div class="cite-stat__label">已拒绝</div></div>
</div>
<div style="margin-top:12px;">
<div class="canteen-item">
<div class="canteen-item__icon" style="background:linear-gradient(135deg,#f6ffed,#b7eb8f);">🏢</div>
<div class="canteen-item__info"><div class="canteen-item__name">机关食堂</div><div class="canteen-item__meta">公司机关 · 已排期</div></div>
<span class="tag tag-green">已引用</span>
</div>
<div class="canteen-item">
<div class="canteen-item__icon" style="background:linear-gradient(135deg,#e6f4ff,#bdd7ff);">🏭</div>
<div class="canteen-item__info"><div class="canteen-item__name">采油一厂食堂</div><div class="canteen-item__meta">采油一厂 · 已排期</div></div>
<span class="tag tag-green">已引用</span>
</div>
<div class="canteen-item">
<div class="canteen-item__icon" style="background:linear-gradient(135deg,#fff7e6,#ffd591);">🏪</div>
<div class="canteen-item__info"><div class="canteen-item__name">信息中心食堂</div><div class="canteen-item__meta">信息技术中心 · 待确认</div></div>
<span class="tag tag-orange">待确认</span>
</div>
</div>
</div>
<!-- 一周餐次安排 -->
<div class="card">
<div class="card__title">一周餐次安排</div>
<div id="day-list"></div>
</div>
<div style="height:16px;"></div>
</div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
/* ===== 一周食谱数据(每天三餐,每餐食物含摄入量与热量) ===== */
var WEEK = [
{ day: '周一', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '全麦面包', amount: 80, unit: 'g', kcal: 200 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '清蒸鲈鱼', amount: 150, unit: 'g', kcal: 160 },
{ name: '杂粮饭', amount: 200, unit: 'g', kcal: 240 },
{ name: '蒜蓉西兰花', amount: 150, unit: 'g', kcal: 60 },
{ name: '紫菜蛋花汤', amount: 200, unit: 'ml', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '香煎鸡胸', amount: 120, unit: 'g', kcal: 200 },
{ name: '蔬菜沙拉', amount: 200, unit: 'g', kcal: 80 },
{ name: '小米粥', amount: 200, unit: 'g', kcal: 120 }
]}
]},
{ day: '周二', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '燕麦牛奶', amount: 300, unit: 'ml', kcal: 220 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '白灼虾', amount: 150, unit: 'g', kcal: 140 },
{ name: '糙米饭', amount: 200, unit: 'g', kcal: 230 },
{ name: '清炒时蔬', amount: 150, unit: 'g', kcal: 55 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '蔬菜豆腐汤', amount: 300, unit: 'g', kcal: 150 },
{ name: '杂粮馒头', amount: 100, unit: 'g', kcal: 220 }
]}
]},
{ day: '周三', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '豆浆', amount: 250, unit: 'ml', kcal: 80 },
{ name: '蒸红薯', amount: 100, unit: 'g', kcal: 90 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '卤牛肉', amount: 100, unit: 'g', kcal: 160 },
{ name: '荞麦面', amount: 200, unit: 'g', kcal: 230 },
{ name: '凉拌黄瓜', amount: 150, unit: 'g', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '清炒时蔬', amount: 200, unit: 'g', kcal: 60 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '菌菇汤', amount: 200, unit: 'g', kcal: 50 }
]}
]},
{ day: '周四', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '杂粮粥', amount: 300, unit: 'g', kcal: 200 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '香煎鸡胸', amount: 150, unit: 'g', kcal: 250 },
{ name: '藜麦饭', amount: 150, unit: 'g', kcal: 180 },
{ name: '西兰花', amount: 150, unit: 'g', kcal: 60 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '虾仁沙拉', amount: 200, unit: 'g', kcal: 150 },
{ name: '全麦面包', amount: 50, unit: 'g', kcal: 120 }
]}
]},
{ day: '周五', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '全麦面包', amount: 80, unit: 'g', kcal: 200 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 },
{ name: '香蕉', amount: 100, unit: 'g', kcal: 90 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '清蒸鱼', amount: 150, unit: 'g', kcal: 160 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '凉拌木耳', amount: 100, unit: 'g', kcal: 40 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '菌菇汤', amount: 300, unit: 'g', kcal: 120 },
{ name: '杂粮饭', amount: 150, unit: 'g', kcal: 180 }
]}
]},
{ day: '周六', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 },
{ name: '燕麦', amount: 50, unit: 'g', kcal: 190 },
{ name: '牛奶', amount: 250, unit: 'ml', kcal: 65 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '白灼虾', amount: 150, unit: 'g', kcal: 140 },
{ name: '杂粮饭', amount: 200, unit: 'g', kcal: 240 },
{ name: '蔬菜沙拉', amount: 150, unit: 'g', kcal: 60 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '蔬菜沙拉', amount: 200, unit: 'g', kcal: 80 },
{ name: '鸡胸肉', amount: 100, unit: 'g', kcal: 170 },
{ name: '玉米', amount: 100, unit: 'g', kcal: 110 }
]}
]},
{ day: '周日', meals: [
{ icon: '🥣', name: '早餐', foods: [
{ name: '豆浆', amount: 250, unit: 'ml', kcal: 80 },
{ name: '玉米', amount: 150, unit: 'g', kcal: 160 },
{ name: '水煮蛋', amount: 60, unit: 'g', kcal: 70 }
]},
{ icon: '🍱', name: '午餐', foods: [
{ name: '卤牛肉', amount: 100, unit: 'g', kcal: 160 },
{ name: '糙米饭', amount: 200, unit: 'g', kcal: 230 },
{ name: '清炒时蔬', amount: 150, unit: 'g', kcal: 55 }
]},
{ icon: '🍽️', name: '晚餐', foods: [
{ name: '鸡胸沙拉', amount: 250, unit: 'g', kcal: 200 },
{ name: '小米粥', amount: 200, unit: 'g', kcal: 120 }
]}
]}
];
/* 计算某餐总热量 */
function mealSubtotal(di, mi) {
return WEEK[di].meals[mi].foods.reduce(function(s, f) { return s + f.kcal; }, 0);
}
/* 计算某天总热量 */
function dayTotal(di) {
return WEEK[di].meals.reduce(function(s, m, mi) { return s + mealSubtotal(di, mi); }, 0);
}
/* 某天三餐小计摘要 */
function daySummary(di) {
return WEEK[di].meals.map(function(m, mi) { return m.name.charAt(0) + mealSubtotal(di, mi); }).join(' · ');
}
/* 渲染整周折叠卡片(只读) */
function renderDayList() {
var html = '';
WEEK.forEach(function(day, di) {
html += '<div class="day-card">' +
'<div class="day-header" onclick="toggleDay(' + di + ')">' +
'<span class="day-header__day">' + day.day + '</span>' +
'<span class="day-header__summary">' + daySummary(di) + '</span>' +
'<span class="day-header__kcal">' + dayTotal(di) + ' kcal</span>' +
'<svg class="arrow" id="arrow-' + di + '" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#999" stroke-width="2"><path d="M4 6l4 4 4-4"/></svg>' +
'</div>' +
'<div class="day-body" id="day-body-' + di + '">' +
day.meals.map(function(m, mi) {
return '<div class="meal-block">' +
'<div class="meal-head"><span class="meal-head__name">' + m.icon + ' ' + m.name + '</span><span class="meal-head__subtotal">小计 ' + mealSubtotal(di, mi) + ' kcal</span></div>' +
m.foods.map(function(f) {
return '<div class="food-row"><span class="food-row__name">' + f.name + '</span><span class="food-row__amount">' + f.amount + f.unit + '</span><span class="food-row__kcal">' + f.kcal + ' kcal</span></div>';
}).join('') +
'</div>';
}).join('') +
'</div>' +
'</div>';
});
document.getElementById('day-list').innerHTML = html;
/* 默认展开周一 */
document.getElementById('day-body-0').classList.add('open');
document.getElementById('arrow-0').classList.add('open');
}
/* 折叠/展开某天 */
function toggleDay(di) {
var body = document.getElementById('day-body-' + di);
var arrow = document.getElementById('arrow-' + di);
body.classList.toggle('open');
arrow.classList.toggle('open');
}
/* 初始化渲染 */
renderDayList();
</script>
</body>
</html>
@@ -0,0 +1,175 @@
<!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: #f7f8fa; 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; flex-shrink: 0; }
.status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; z-index: 10; }
.status-bar .time { font-size: 15px; font-weight: 700; color: #1a1a1a; }
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #1a1a1a; }
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { display: flex; align-items: center; gap: 4px; color: #1677ff; font-size: 15px; cursor: pointer; background: none; border: none; padding: 4px 0; }
.nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; white-space: nowrap; }
.nav-add { margin-left: auto; color: #1677ff; font-size: 14px; cursor: pointer; background: none; border: none; font-weight: 600; }
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
.scroll-content::-webkit-scrollbar { display: none; }
.seg-tabs { display: flex; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
.seg-tab { flex: 1; text-align: center; padding: 12px 0; font-size: 14px; color: #666; cursor: pointer; border-bottom: 2px solid transparent; }
.seg-tab.active { color: #1677ff; border-bottom-color: #1677ff; font-weight: 600; }
.seg-tab .badge { display: inline-block; background: #e8e8e8; color: #666; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; padding: 0 4px; margin-left: 4px; vertical-align: middle; line-height: 16px; }
.seg-tab.active .badge { background: #e6f4ff; color: #1677ff; }
/* 食谱卡片 */
.recipe-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; }
.recipe-card:active { transform: scale(0.98); }
.recipe-card__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.recipe-card__icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; flex-shrink: 0; }
.recipe-card__info { flex: 1; min-width: 0; }
.recipe-card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.recipe-card__meta { font-size: 12px; color: #999; margin-top: 2px; }
.recipe-card__footer { display: flex; align-items: center; justify-content: space-between; padding-top: 10px; border-top: 1px solid #f5f5f5; }
.recipe-card__cite { font-size: 12px; color: #666; }
.recipe-card__cite .num { font-weight: 700; color: #1677ff; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.tag-green { background: #f6ffed; color: #52c41a; }
.tag-gray { background: #f5f5f5; color: #999; }
.tag-blue { background: #e6f4ff; color: #1677ff; }
/* 制定食谱按钮 */
.fab { position: absolute; right: 20px; bottom: 100px; width: 56px; height: 56px; border-radius: 50%; background: #1677ff; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28px; cursor: pointer; box-shadow: 0 4px 12px rgba(22,119,255,0.4); z-index: 50; }
.fab:active { transform: scale(0.94); }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="time">9:41</span>
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
</div>
<!-- 导航栏 -->
<div class="nav-bar">
<button class="nav-back" onclick="location.href='./index.html'">
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
</button>
<span class="nav-title">我的食谱</span>
<!-- [交互] 点击跳转至: ./recipe-create.html -->
<button class="nav-add" onclick="location.href='./recipe-create.html'">制定食谱</button>
</div>
<!-- Tab -->
<div class="seg-tabs">
<div class="seg-tab active" onclick="switchTab(this,'panel-all')">全部<span class="badge">6</span></div>
<div class="seg-tab" onclick="switchTab(this,'panel-pub')">已发布<span class="badge">4</span></div>
<div class="seg-tab" onclick="switchTab(this,'panel-draft')">草稿<span class="badge">2</span></div>
</div>
<div class="scroll-content">
<div id="panel-all">
<!-- [交互] 点击跳转至: ./recipe-detail.html -->
<div class="recipe-card" onclick="location.href='./recipe-detail.html'">
<div class="recipe-card__head">
<div class="recipe-card__icon" style="background:linear-gradient(135deg,#f6ffed,#b7eb8f);">🥗</div>
<div class="recipe-card__info">
<div class="recipe-card__name">机关减脂周菜单(第 37 周)</div>
<div class="recipe-card__meta">周菜单 · 早/中/晚 · 2026-09-14 ~ 09-20</div>
</div>
<span class="tag tag-green">已发布</span>
</div>
<div class="recipe-card__footer">
<span class="recipe-card__cite"><span class="num">5</span> 个食堂引用</span>
<span style="font-size:12px;color:#1677ff;">查看详情 </span>
</div>
</div>
<div class="recipe-card" onclick="location.href='./recipe-detail.html'">
<div class="recipe-card__head">
<div class="recipe-card__icon" style="background:linear-gradient(135deg,#fff7e6,#ffd591);">🍚</div>
<div class="recipe-card__info">
<div class="recipe-card__name">一线高能量周菜单(第 37 周)</div>
<div class="recipe-card__meta">周菜单 · 早/中/晚 · 2026-09-14 ~ 09-20</div>
</div>
<span class="tag tag-green">已发布</span>
</div>
<div class="recipe-card__footer">
<span class="recipe-card__cite"><span class="num">3</span> 个食堂引用</span>
<span style="font-size:12px;color:#1677ff;">查看详情 </span>
</div>
</div>
<div class="recipe-card" onclick="location.href='./recipe-detail.html'">
<div class="recipe-card__head">
<div class="recipe-card__icon" style="background:linear-gradient(135deg,#e6f4ff,#bdd7ff);">🍂</div>
<div class="recipe-card__info">
<div class="recipe-card__name">秋季养生季节菜单</div>
<div class="recipe-card__meta">季节菜单 · 早/中/晚 · 2026-09 ~ 11</div>
</div>
<span class="tag tag-green">已发布</span>
</div>
<div class="recipe-card__footer">
<span class="recipe-card__cite"><span class="num">8</span> 个食堂引用</span>
<span style="font-size:12px;color:#1677ff;">查看详情 </span>
</div>
</div>
<div class="recipe-card" onclick="location.href='./recipe-detail.html'">
<div class="recipe-card__head">
<div class="recipe-card__icon" style="background:linear-gradient(135deg,#f9f0ff,#d3adf7);">🥩</div>
<div class="recipe-card__info">
<div class="recipe-card__name">控糖低 GI 周菜单(第 38 周)</div>
<div class="recipe-card__meta">周菜单 · 早/中/晚 · 2026-09-21 ~ 09-27</div>
</div>
<span class="tag tag-gray">草稿</span>
</div>
<div class="recipe-card__footer">
<span class="recipe-card__cite">尚未发布</span>
<span style="font-size:12px;color:#1677ff;">继续编辑 </span>
</div>
</div>
<div style="height:90px;"></div>
</div>
<div id="panel-pub" style="display:none;">
<div class="recipe-card" onclick="location.href='./recipe-detail.html'">
<div class="recipe-card__head">
<div class="recipe-card__icon" style="background:linear-gradient(135deg,#f6ffed,#b7eb8f);">🥗</div>
<div class="recipe-card__info"><div class="recipe-card__name">机关减脂周菜单(第 37 周)</div><div class="recipe-card__meta">周菜单 · 2026-09-14 ~ 09-20</div></div>
<span class="tag tag-green">已发布</span>
</div>
<div class="recipe-card__footer"><span class="recipe-card__cite"><span class="num">5</span> 个食堂引用</span><span style="font-size:12px;color:#1677ff;">查看详情 </span></div>
</div>
<div style="height:90px;"></div>
</div>
<div id="panel-draft" style="display:none;">
<div class="recipe-card" onclick="location.href='./recipe-detail.html'">
<div class="recipe-card__head">
<div class="recipe-card__icon" style="background:linear-gradient(135deg,#f9f0ff,#d3adf7);">🥩</div>
<div class="recipe-card__info"><div class="recipe-card__name">控糖低 GI 周菜单(第 38 周)</div><div class="recipe-card__meta">周菜单 · 2026-09-21 ~ 09-27</div></div>
<span class="tag tag-gray">草稿</span>
</div>
<div class="recipe-card__footer"><span class="recipe-card__cite">尚未发布</span><span style="font-size:12px;color:#1677ff;">继续编辑 </span></div>
</div>
<div style="height:90px;"></div>
</div>
</div>
<!-- 制定食谱悬浮按钮 -->
<div class="fab" onclick="location.href='./recipe-create.html'"></div>
</div>
<script>
/* 标记当前角色为营养师 */
sessionStorage.setItem('appRole', 'nutritionist');
function switchTab(el, panelId) {
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
['panel-all','panel-pub','panel-draft'].forEach(function(id) {
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
});
}
</script>
</body>
</html>
+1 -1
View File
@@ -122,7 +122,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="case-avatar case-avatar-emergency">🚑</div>
<div class="case-main">
<div class="case-title-row">
<div class="case-name">王建<span style="font-size:13px;font-weight:400;color:#666;margin-left:6px;">38岁</span></div>
<div class="case-name">石维<span style="font-size:13px;font-weight:400;color:#666;margin-left:6px;">38岁</span></div>
<span class="tag tag-red">进行中</span>
</div>
<div class="case-meta">求助方式:<span class="tag tag-orange">拨打120</span></div>
@@ -110,7 +110,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
<div class="ec-top">
<div class="ec-avatar">🚑</div>
<div class="ec-info">
<div class="ec-name">王建<span class="age">38岁</span></div>
<div class="ec-name">石维<span class="age">38岁</span></div>
<div class="ec-time">求助方式:<span class="tag tag-orange">拨打120</span></div>
<div class="ec-time">求助时间:2026-04-23 08:15</div>
</div>
+1 -1
View File
@@ -160,7 +160,7 @@ stateDiagram-v2
![医疗点地图](screenshots/resource-map-clinic.jpg)
![急救包地图](screenshots/resource-map-kit.jpg)
以上四个地图页面与能源医院地图共享统一的页面结构,仅资源类型不同(注:救护车不显示地图导航按钮)。
以上四个地图页面与能源医院地图共享统一的页面结构,仅资源类型不同(注:救护车不显示地图导航按钮)。AED弹窗卡片名称展示为“AED(设备编号)”,急救包弹窗卡片名称展示为“急救包(编号)”。
**交互说明:** 同能源医院地图页面,底部Tab栏切换时跳转到对应资源地图页面,点击标记弹出该类型资源的概要卡片。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -98,7 +98,7 @@
<div class="popup-card" onclick="window.location.href='aed-detail.html'">
<div class="popup-card__img"><svg width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" rx="4" fill="#fff8e8"/><text x="20" y="25" text-anchor="middle" fill="#faad14" font-size="14" font-weight="700">AED</text></svg></div>
<div class="popup-card__info">
<div class="popup-card__name">半自动体外除颤仪</div>
<div class="popup-card__name">AEDA596852322</div>
<div class="popup-card__dist">距离3.5km 驾车约12分钟</div>
<div class="popup-card__addr">详细地址:生产保障大队办公楼楼道</div>
<div class="popup-card__unit">第三采油厂-虎狼峁采油作业区</div>
@@ -98,7 +98,7 @@
<div class="popup-card" onclick="window.location.href='first-aid-kit-detail.html'">
<div class="popup-card__img"><svg width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" rx="4" fill="#e8ffe8"/><rect x="8" y="12" width="24" height="18" rx="3" fill="#52c41a"/><rect x="16" y="8" width="8" height="6" rx="1" fill="#52c41a"/><rect x="17" y="17" width="6" height="6" rx="1" fill="#fff"/><rect x="19" y="15" width="2" height="10" rx="0.5" fill="#fff"/></svg></div>
<div class="popup-card__info">
<div class="popup-card__name">急救包</div>
<div class="popup-card__name">急救包JJBA0001</div>
<div class="popup-card__dist">距离3.5km 驾车约12分钟</div>
<div class="popup-card__addr">详细地址:生产保障大队办公楼楼道</div>
<div class="popup-card__unit">第三采油厂-虎狼峁采油作业区</div>
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>申请记录</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card__date { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.status { font-size: 12px; padding: 2px 10px; border-radius: 10px; font-weight: 500; }
.status--pending { background: #fff3e6; color: #fa8c16; }
.status--done { background: #e6f9ef; color: #52c41a; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.empty { text-align: center; color: #999; padding: 60px 0; font-size: 14px; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="follow-list.html"></a><span class="nav-title">申请记录</span></div>
<div class="scroll-content">
<div class="card">
<div class="card__head">
<span class="card__date">申请随访日期:2026-09-16</span>
<span class="status status--pending">待处理</span>
</div>
<div class="card__info">
<div><b>症状描述:</b>偶有头晕,血压波动,服药后未见明显缓解</div>
<div><b>申请时间:</b>2026-09-10 10:20</div>
</div>
</div>
<div class="card">
<div class="card__head">
<span class="card__date">申请随访日期:2026-08-12</span>
<span class="status status--done">已处理</span>
</div>
<div class="card__info">
<div><b>症状描述:</b>血糖控制不佳,口渴多饮,近期体重下降</div>
<div><b>申请时间:</b>2026-08-05 09:15</div>
</div>
</div>
<div class="card">
<div class="card__head">
<span class="card__date">申请随访日期:2026-06-18</span>
<span class="status status--done">已处理</span>
</div>
<div class="card__info">
<div><b>症状描述:</b>晨起头晕,血压偏高,请求医生评估用药方案</div>
<div><b>申请时间:</b>2026-06-10 14:30</div>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随访详情</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.sec { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.sec__title { font-size: 15px; font-weight: 700; color: #1a1a1a; border-left: 3px solid #1677ff; padding-left: 8px; margin-bottom: 12px; }
.namecard { display: flex; gap: 14px; align-items: center; }
.namecard__name { font-size: 17px; font-weight: 700; color: #1a1a1a; margin-bottom: 4px; }
.namecard__meta { font-size: 12px; color: #666; line-height: 1.7; }
.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px; font-size: 13px; color: #666; line-height: 1.7; }
.kv b { color: #333; font-weight: 500; }
.kv__full { grid-column: 1 / -1; }
.pill { display: inline-block; padding: 1px 10px; border-radius: 10px; font-size: 11px; background: rgba(49,156,255,.1); color: #319cff; margin-left: 6px; }
.tag { display: inline-block; padding: 1px 8px; border-radius: 4px; font-size: 11px; margin-right: 4px; }
.tag--ht { background: rgba(24,144,255,.12); color: #1890ff; }
.tag--dm { background: rgba(42,193,139,.12); color: #2ac18b; }
.med-card { background: #f2f6ff; border-radius: 10px; padding: 12px 14px; font-size: 13px; color: #4a5a7f; line-height: 1.9; margin-bottom: 10px; }
.med-card b { color: #333; font-size: 14px; }
.sign-box { height: 90px; border: 1px solid #e5e8ef; border-radius: 8px; background: #fbfcfe; display: flex; align-items: center; justify-content: center; }
.sign-box span { font-family: KaiTi, serif; font-size: 30px; color: #333; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="follow-list.html"></a><span class="nav-title">随访详情</span></div>
<div class="scroll-content">
<div class="sec">
<div class="namecard">
<div class="namecard__info">
<div class="namecard__name">岳春生<span class="pill">第3次</span></div>
<div class="namecard__meta">男 · 54岁 · 员工编号 1035678</div>
<div class="namecard__meta">联系电话:13956282214</div>
<div class="namecard__meta">单位部门:陕E作业区-管理四中心</div>
<div class="namecard__meta">慢病干预:<span class="tag tag--ht">高血压</span><span class="tag tag--dm">糖尿病</span></div>
</div>
</div>
</div>
<div class="sec">
<div class="sec__title">随访信息</div>
<div class="kv" style="grid-template-columns:1fr">
<div>随访医生:<b>石维国</b></div>
<div>随访日期:<b>2026-08-20</b></div>
<div>随访形式:<b>门诊随访</b></div>
<div>效果评价:<b>良好</b></div>
<div>下次随访:<b>2026-09-20</b></div>
<div>症状:<b>偶有头晕,血压波动</b></div>
</div>
</div>
<div class="sec">
<div class="sec__title">体征</div>
<div class="kv">
<div>血压:<b>158/98 mmHg</b></div>
<div>心率:<b>76 次/分</b></div>
<div>血糖:<b>8.2 mmol/L</b></div>
<div>身高:<b>172 cm</b></div>
<div>体重:<b>78.5 kg</b></div>
<div>BMI<b>26.5(自动)</b></div>
<div>腰围:<b>94 cm</b></div>
<div>臀围:<b>98 cm</b></div>
<div>腰身比:<b>0.96(自动)</b></div>
</div>
</div>
<div class="sec">
<div class="sec__title">生活方式</div>
<div class="kv" style="grid-template-columns:1fr">
<div>吸烟量:<b>10 支/日</b></div>
<div>饮酒量:<b>偶尔</b></div>
<div>运动:<b>3 次/周 · 30 分钟/次</b></div>
<div>摄盐情况:<b>偏重</b></div>
<div>心理调整:<b>良好</b></div>
<div>遵医行为:<b>一般(服药不规律)</b></div>
</div>
</div>
<div class="sec">
<div class="sec__title">用药情况</div>
<div class="med-card"><b>苯磺酸氨氯地平片</b><br>用法用量:口服,一日1次,一次1片</div>
<div class="kv">
<div>用药依从性:<b>一般</b></div>
<div>药物不良反应:<b></b></div>
</div>
</div>
<div class="sec">
<div class="sec__title">风险评估与生活建议</div>
<div class="kv" style="grid-template-columns:1fr">
<div>原因:<b>服药不规律、高盐饮食、吸烟</b></div>
<div>措施:<b>调整降压方案,加强戒烟与限盐宣教</b></div>
<div>膳食处方:<b>低盐低脂,每日主食控制在 6 两以内</b></div>
<div>运动处方:<b>每日快走 30 分钟,每周不少于 5 次</b></div>
</div>
</div>
<div class="sec">
<div class="sec__title">员工签字</div>
<div class="sign-box"><span>岳春生</span></div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随访记录</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; transition: transform 0.15s; }
.card:active { transform: scale(0.98); }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card__date { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__form { font-size: 12px; color: #1677ff; background: #e6f2ff; border-radius: 8px; padding: 2px 8px; }
.card__times { font-size: 11px; color: #319cff; background: rgba(49,156,255,.1); border-radius: 10px; padding: 2px 10px; font-weight: 500; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.card__foot { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; }
.effect { font-size: 12px; padding: 2px 10px; border-radius: 10px; font-weight: 500; }
.effect--good { background: #e6f9ef; color: #52c41a; }
.effect--mid { background: #fff3e6; color: #fa8c16; }
.effect--bad { background: #fff1f0; color: #f5222d; }
.next { font-size: 12px; color: #999; }
/* 底部固定操作区 */
.footer { flex-shrink: 0; background: #fff; padding: 10px 16px 14px; box-shadow: 0 -2px 6px rgba(0,0,0,0.04); }
.footer__btn { width: 100%; height: 44px; border-radius: 22px; border: none; background: #1677ff; color: #fff; font-size: 16px; font-weight: 500; cursor: pointer; }
.footer__link { text-align: center; font-size: 13px; color: #1677ff; margin-top: 8px; cursor: pointer; }
/* 申请随访弹窗 */
.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 60; display: none; align-items: flex-end; }
.overlay.open { display: flex; }
.sheet { width: 100%; background: #fff; border-radius: 20px 20px 0 0; padding: 20px 20px 28px; }
.sheet__title { font-size: 16px; font-weight: 700; color: #1a1a1a; margin-bottom: 16px; }
.sheet__field { margin-bottom: 14px; }
.sheet__field label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; }
.sheet__field input, .sheet__field textarea { width: 100%; border: 1px solid #e0e0e0; border-radius: 8px; padding: 0 12px; font-size: 14px; color: #333; outline: none; font-family: inherit; }
.sheet__field input { height: 40px; }
.sheet__field textarea { height: 88px; padding: 10px 12px; resize: none; }
.sheet__field input:focus, .sheet__field textarea:focus { border-color: #1677ff; }
.sheet__foot { display: flex; gap: 12px; margin-top: 20px; }
.sheet__foot button { flex: 1; height: 44px; border-radius: 22px; font-size: 15px; cursor: pointer; }
.btn-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-confirm { background: #1677ff; color: #fff; border: none; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">随访记录</span></div>
<div class="scroll-content">
<div class="card" onclick="window.location.href='follow-detail.html'">
<div class="card__head">
<div style="display:flex;align-items:center;gap:8px">
<span class="card__date">2026-08-20</span>
<span class="card__times">第3次</span>
</div>
<span class="card__form">门诊随访</span>
</div>
<div class="card__info">
<div><b>随访医生:</b>石维国</div>
<div><b>症状:</b>偶有头晕,血压波动</div>
</div>
<div class="card__foot">
<span class="effect effect--good">随访效果 · 良好</span>
<span class="next">下次随访 2026-09-20</span>
</div>
</div>
<div class="card" onclick="window.location.href='follow-detail.html'">
<div class="card__head">
<div style="display:flex;align-items:center;gap:8px">
<span class="card__date">2026-07-20</span>
<span class="card__times">第2次</span>
</div>
<span class="card__form">巡诊随访</span>
</div>
<div class="card__info">
<div><b>随访医生:</b>石维国</div>
<div><b>症状:</b>血压偏高,偶有头晕</div>
</div>
<div class="card__foot">
<span class="effect effect--mid">随访效果 · 一般</span>
<span class="next">下次随访 2026-08-20</span>
</div>
</div>
<div class="card" onclick="window.location.href='follow-detail.html'">
<div class="card__head">
<div style="display:flex;align-items:center;gap:8px">
<span class="card__date">2026-06-15</span>
<span class="card__times">第1次</span>
</div>
<span class="card__form">电话随访</span>
</div>
<div class="card__info">
<div><b>随访医生:</b>李静</div>
<div><b>症状:</b>血糖控制尚可</div>
</div>
<div class="card__foot">
<span class="effect effect--good">随访效果 · 良好</span>
<span class="next">下次随访 2026-07-20</span>
</div>
</div>
</div>
<!-- 底部固定操作区 -->
<div class="footer">
<button class="footer__btn" onclick="openApplySheet()">申请随访</button>
<div class="footer__link" onclick="window.location.href='follow-apply-records.html'">申请记录</div>
</div>
<!-- 申请随访弹窗 -->
<div class="overlay" id="applySheet">
<div class="sheet">
<div class="sheet__title">申请随访</div>
<div class="sheet__field">
<label>申请随访日期</label>
<input type="date" id="applyDate" value="2026-09-16">
</div>
<div class="sheet__field">
<label>症状描述</label>
<textarea id="applySymptom" placeholder="请输入症状描述"></textarea>
</div>
<div class="sheet__foot">
<button class="btn-cancel" onclick="closeApplySheet()">取消</button>
<button class="btn-confirm" onclick="submitApply()">提交</button>
</div>
</div>
</div>
</div>
<script>
function openApplySheet() { document.getElementById('applySheet').classList.add('open'); }
function closeApplySheet() { document.getElementById('applySheet').classList.remove('open'); }
function submitApply() {
var date = document.getElementById('applyDate').value;
var symptom = document.getElementById('applySymptom').value.trim();
if (!date) { alert('请选择申请随访日期'); return; }
if (!symptom) { alert('请填写症状描述'); return; }
closeApplySheet();
alert('随访申请已提交');
}
</script>
</body>
</html>
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的预约</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 12px 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.tab-bar { display: flex; background: #fff; border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
.tab-bar__item { flex: 1; text-align: center; padding: 10px 0; font-size: 13px; color: #666; cursor: pointer; border-bottom: 2px solid transparent; }
.tab-bar__item--active { color: #1677ff; font-weight: 600; border-bottom-color: #1677ff; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card__room { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__status { font-size: 12px; padding: 2px 10px; border-radius: 10px; font-weight: 500; }
.card__status--booked { background: #fff7e6; color: #fa8c16; }
.card__status--done { background: #e6f9ef; color: #52c41a; }
.card__status--cancelled { background: #f5f5f5; color: #999; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.card__main { display: flex; gap: 10px; align-items: center; }
.card__img { width: 64px; height: 64px; border-radius: 8px; background: #f5f7fa; border: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: center; color: #bbb; font-size: 11px; flex-shrink: 0; }
.card__qr { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 3px; }
.qr-mini { width: 56px; height: 56px; padding: 3px; background: #fff; border: 1px solid #eee; border-radius: 6px; display: grid; grid-template-columns: repeat(21, 1fr); grid-template-rows: repeat(21, 1fr); }
.qr-cell { background: #fff; }
.qr-cell--dark { background: #1a1a1a; }
.card__foot { display: flex; justify-content: flex-end; align-items: center; margin-top: 10px; }
.btn-cancel { font-size: 12px; color: #f5222d; border: 1px solid #f5222d; border-radius: 14px; padding: 4px 14px; background: #fff; cursor: pointer; }
/* 取消预约二次确认弹窗 */
.dialog-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 70; display: none; align-items: center; justify-content: center; }
.dialog-overlay.open { display: flex; }
.dialog { width: 280px; background: #fff; border-radius: 14px; padding: 24px 20px 16px; text-align: center; }
.dialog__title { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.dialog__text { font-size: 13px; color: #666; margin-bottom: 20px; }
.dialog__foot { display: flex; gap: 12px; }
.dialog__foot button { flex: 1; height: 40px; border-radius: 20px; font-size: 14px; cursor: pointer; }
.btn-dialog-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-dialog-confirm { background: #f5222d; color: #fff; border: none; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">我的预约</span></div>
<div class="scroll-content">
<div class="tab-bar">
<div class="tab-bar__item tab-bar__item--active">全部</div>
<div class="tab-bar__item" onclick="window.location.href='hut-booking-booked.html'">已预约</div>
<div class="tab-bar__item" onclick="window.location.href='hut-booking-done.html'">已完成</div>
</div>
<div class="card" onclick="goDetail('樊学站点健康小屋','booked','血压检测仪','2026-09-09','09:00 - 09:30')">
<div class="card__head">
<span class="card__room">2026-09-09 09:00 - 09:30</span>
<span class="card__status card__status--booked">已预约</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>血压检测仪</div>
<div><b>健康小屋:</b>樊学站点健康小屋</div>
</div>
<div class="card__qr"><div class="qr-mini" data-seed="101"></div></div>
</div>
<div class="card__foot">
<button class="btn-cancel" onclick="event.stopPropagation();openCancelDialog(this)">取消预约</button>
</div>
</div>
<div class="card" onclick="goDetail('兴隆园健康小屋','booked','血糖检测仪','2026-09-10','10:00 - 10:15')">
<div class="card__head">
<span class="card__room">2026-09-10 10:00 - 10:15</span>
<span class="card__status card__status--booked">已预约</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>血糖检测仪</div>
<div><b>健康小屋:</b>兴隆园健康小屋</div>
</div>
<div class="card__qr"><div class="qr-mini" data-seed="202"></div></div>
</div>
<div class="card__foot">
<button class="btn-cancel" onclick="event.stopPropagation();openCancelDialog(this)">取消预约</button>
</div>
</div>
<div class="card" onclick="goDetail('樊学站点健康小屋','done','体脂秤','2026-09-02','09:00 - 09:15')">
<div class="card__head">
<span class="card__room">2026-09-02 09:00 - 09:15</span>
<span class="card__status card__status--done">已完成</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>体脂秤</div>
<div><b>健康小屋:</b>樊学站点健康小屋</div>
</div>
</div>
</div>
<div class="card" onclick="goDetail('兴隆园健康小屋','done','血压检测仪','2026-08-25','14:00 - 14:30')">
<div class="card__head">
<span class="card__room">2026-08-25 14:00 - 14:30</span>
<span class="card__status card__status--done">已完成</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>血压检测仪</div>
<div><b>健康小屋:</b>兴隆园健康小屋</div>
</div>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
<!-- 取消预约二次确认弹窗 -->
<div class="dialog-overlay" id="cancelDialog">
<div class="dialog">
<div class="dialog__title">取消预约</div>
<div class="dialog__text">确定要取消该预约吗?取消后需重新预约。</div>
<div class="dialog__foot">
<button class="btn-dialog-cancel" onclick="closeCancelDialog()">暂不取消</button>
<button class="btn-dialog-confirm" onclick="confirmCancel()">确定取消</button>
</div>
</div>
</div>
</div>
<script>
// 跳转预约详情,携带预约信息
function goDetail(hut, status, dev, date, time) {
var q = new URLSearchParams({ hut: hut, status: status, dev: dev, date: date, time: time });
window.location.href = 'hut-booking-detail.html?' + q.toString();
}
// 取消预约:二次确认后原地改为已取消状态
var cancelCard = null;
function openCancelDialog(btn) {
cancelCard = btn.closest('.card');
document.getElementById('cancelDialog').classList.add('open');
}
function closeCancelDialog() { document.getElementById('cancelDialog').classList.remove('open'); }
function confirmCancel() {
if (cancelCard) {
var badge = cancelCard.querySelector('.card__status');
badge.textContent = '已取消';
badge.className = 'card__status card__status--cancelled';
var foot = cancelCard.querySelector('.card__foot');
if (foot) foot.remove();
var qr = cancelCard.querySelector('.card__qr');
if (qr) qr.remove();
}
closeCancelDialog();
}
// 生成卡片小二维码(21×21 伪码,含三个定位角框,按 data-seed 确定性填充)
document.querySelectorAll('.qr-mini').forEach(function (box) {
var N = 21, s = parseInt(box.dataset.seed, 10) || 1;
function rand() { s = (s * 9301 + 49297) % 233280; return s / 233280; }
function finder(r, c) {
if (r < 7 && c < 7) return true;
if (r < 7 && c >= N - 7) return true;
if (r >= N - 7 && c < 7) return true;
return false;
}
function finderDark(r, c) {
var local = function (rr, cc) {
var outer = rr === 0 || rr === 6 || cc === 0 || cc === 6;
var inner = rr >= 2 && rr <= 4 && cc >= 2 && cc <= 4;
return outer || inner;
};
if (r < 7 && c < 7) return local(r, c);
if (r < 7 && c >= N - 7) return local(r, c - (N - 7));
if (r >= N - 7 && c < 7) return local(r - (N - 7), c);
return false;
}
for (var r = 0; r < N; r++) {
for (var c = 0; c < N; c++) {
var cell = document.createElement('div');
cell.className = 'qr-cell';
if (finder(r, c)) {
if (finderDark(r, c)) cell.classList.add('qr-cell--dark');
} else if (rand() > 0.5) {
cell.classList.add('qr-cell--dark');
}
box.appendChild(cell);
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的预约 · 已预约</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 12px 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.tab-bar { display: flex; background: #fff; border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
.tab-bar__item { flex: 1; text-align: center; padding: 10px 0; font-size: 13px; color: #666; cursor: pointer; border-bottom: 2px solid transparent; }
.tab-bar__item--active { color: #1677ff; font-weight: 600; border-bottom-color: #1677ff; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card__room { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__status { font-size: 12px; padding: 2px 10px; border-radius: 10px; font-weight: 500; background: #fff7e6; color: #fa8c16; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.card__main { display: flex; gap: 10px; align-items: center; }
.card__img { width: 64px; height: 64px; border-radius: 8px; background: #f5f7fa; border: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: center; color: #bbb; font-size: 11px; flex-shrink: 0; }
.card__qr { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 3px; }
.qr-mini { width: 56px; height: 56px; padding: 3px; background: #fff; border: 1px solid #eee; border-radius: 6px; display: grid; grid-template-columns: repeat(21, 1fr); grid-template-rows: repeat(21, 1fr); }
.qr-cell { background: #fff; }
.qr-cell--dark { background: #1a1a1a; }
.card__foot { display: flex; justify-content: flex-end; align-items: center; margin-top: 10px; }
.btn-cancel { font-size: 12px; color: #f5222d; border: 1px solid #f5222d; border-radius: 14px; padding: 4px 14px; background: #fff; cursor: pointer; }
/* 取消预约二次确认弹窗 */
.dialog-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 70; display: none; align-items: center; justify-content: center; }
.dialog-overlay.open { display: flex; }
.dialog { width: 280px; background: #fff; border-radius: 14px; padding: 24px 20px 16px; text-align: center; }
.dialog__title { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.dialog__text { font-size: 13px; color: #666; margin-bottom: 20px; }
.dialog__foot { display: flex; gap: 12px; }
.dialog__foot button { flex: 1; height: 40px; border-radius: 20px; font-size: 14px; cursor: pointer; }
.btn-dialog-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-dialog-confirm { background: #f5222d; color: #fff; border: none; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">我的预约</span></div>
<div class="scroll-content">
<div class="tab-bar">
<div class="tab-bar__item" onclick="window.location.href='hut-booking-all.html'">全部</div>
<div class="tab-bar__item tab-bar__item--active">已预约</div>
<div class="tab-bar__item" onclick="window.location.href='hut-booking-done.html'">已完成</div>
</div>
<div class="card" onclick="goDetail('樊学站点健康小屋','booked','血压检测仪','2026-09-09','09:00 - 09:30')">
<div class="card__head">
<span class="card__room">2026-09-09 09:00 - 09:30</span>
<span class="card__status">已预约</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>血压检测仪</div>
<div><b>健康小屋:</b>樊学站点健康小屋</div>
</div>
<div class="card__qr"><div class="qr-mini" data-seed="101"></div></div>
</div>
<div class="card__foot">
<button class="btn-cancel" onclick="event.stopPropagation();openCancelDialog(this)">取消预约</button>
</div>
</div>
<div class="card" onclick="goDetail('兴隆园健康小屋','booked','血糖检测仪','2026-09-10','10:00 - 10:15')">
<div class="card__head">
<span class="card__room">2026-09-10 10:00 - 10:15</span>
<span class="card__status">已预约</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>血糖检测仪</div>
<div><b>健康小屋:</b>兴隆园健康小屋</div>
</div>
<div class="card__qr"><div class="qr-mini" data-seed="202"></div></div>
</div>
<div class="card__foot">
<button class="btn-cancel" onclick="event.stopPropagation();openCancelDialog(this)">取消预约</button>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
<!-- 取消预约二次确认弹窗 -->
<div class="dialog-overlay" id="cancelDialog">
<div class="dialog">
<div class="dialog__title">取消预约</div>
<div class="dialog__text">确定要取消该预约吗?取消后需重新预约。</div>
<div class="dialog__foot">
<button class="btn-dialog-cancel" onclick="closeCancelDialog()">暂不取消</button>
<button class="btn-dialog-confirm" onclick="confirmCancel()">确定取消</button>
</div>
</div>
</div>
</div>
<script>
// 跳转预约详情,携带预约信息
function goDetail(hut, status, dev, date, time) {
var q = new URLSearchParams({ hut: hut, status: status, dev: dev, date: date, time: time });
window.location.href = 'hut-booking-detail.html?' + q.toString();
}
// 取消预约:二次确认后原地改为已取消状态
var cancelCard = null;
function openCancelDialog(btn) {
cancelCard = btn.closest('.card');
document.getElementById('cancelDialog').classList.add('open');
}
function closeCancelDialog() { document.getElementById('cancelDialog').classList.remove('open'); }
function confirmCancel() {
if (cancelCard) {
var badge = cancelCard.querySelector('.card__status');
badge.textContent = '已取消';
badge.style.background = '#f5f5f5';
badge.style.color = '#999';
var foot = cancelCard.querySelector('.card__foot');
if (foot) foot.remove();
var qr = cancelCard.querySelector('.card__qr');
if (qr) qr.remove();
}
closeCancelDialog();
}
// 生成卡片小二维码(21×21 伪码,含三个定位角框,按 data-seed 确定性填充)
document.querySelectorAll('.qr-mini').forEach(function (box) {
var N = 21, s = parseInt(box.dataset.seed, 10) || 1;
function rand() { s = (s * 9301 + 49297) % 233280; return s / 233280; }
function finder(r, c) {
if (r < 7 && c < 7) return true;
if (r < 7 && c >= N - 7) return true;
if (r >= N - 7 && c < 7) return true;
return false;
}
function finderDark(r, c) {
var local = function (rr, cc) {
var outer = rr === 0 || rr === 6 || cc === 0 || cc === 6;
var inner = rr >= 2 && rr <= 4 && cc >= 2 && cc <= 4;
return outer || inner;
};
if (r < 7 && c < 7) return local(r, c);
if (r < 7 && c >= N - 7) return local(r, c - (N - 7));
if (r >= N - 7 && c < 7) return local(r - (N - 7), c);
return false;
}
for (var r = 0; r < N; r++) {
for (var c = 0; c < N; c++) {
var cell = document.createElement('div');
cell.className = 'qr-cell';
if (finder(r, c)) {
if (finderDark(r, c)) cell.classList.add('qr-cell--dark');
} else if (rand() > 0.5) {
cell.classList.add('qr-cell--dark');
}
box.appendChild(cell);
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>预约详情</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.qrcode-card { background: #fff; border-radius: 14px; padding: 24px 20px; display: flex; flex-direction: column; align-items: center; box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 12px; }
.qrcode-card__head { display: flex; align-items: center; gap: 10px; }
.qrcode-card__title { font-size: 16px; font-weight: 700; color: #1a1a1a; }
.status { font-size: 12px; padding: 2px 10px; border-radius: 10px; font-weight: 500; }
.status--booked { background: #fff7e6; color: #fa8c16; }
.status--done { background: #e6f9ef; color: #52c41a; }
.status--cancelled { background: #f5f5f5; color: #999; }
.qrcode-card__hint { font-size: 12px; color: #999; margin: 6px 0 18px; }
.qr-box { width: 180px; height: 180px; padding: 10px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: grid; grid-template-columns: repeat(21, 1fr); grid-template-rows: repeat(21, 1fr); gap: 1px; }
.qr-cell { background: #fff; }
.qr-cell--dark { background: #1a1a1a; }
.qrcode-card__expire { font-size: 12px; color: #fa8c16; margin-top: 14px; }
.info-card { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.info-card__status { display: none; justify-content: flex-end; margin-bottom: 10px; }
.row { display: flex; font-size: 13px; line-height: 1.7; margin-bottom: 8px; align-items: center; }
.row:last-child { margin-bottom: 0; }
.row__label { width: 96px; color: #999; flex-shrink: 0; }
.row__value { flex: 1; color: #333; }
.row__value--bold { font-weight: 600; color: #1a1a1a; }
.nav-btn { width: 28px; height: 28px; border-radius: 50%; background: #e6f2ff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; cursor: pointer; }
.cancel-bar { margin-top: 16px; display: none; }
.btn-cancel-booking { width: 100%; height: 44px; border-radius: 22px; border: 1px solid #f5222d; background: #fff; color: #f5222d; font-size: 15px; cursor: pointer; }
/* 取消预约二次确认弹窗 */
.dialog-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 70; display: none; align-items: center; justify-content: center; }
.dialog-overlay.open { display: flex; }
.dialog { width: 280px; background: #fff; border-radius: 14px; padding: 24px 20px 16px; text-align: center; }
.dialog__title { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.dialog__text { font-size: 13px; color: #666; margin-bottom: 20px; }
.dialog__foot { display: flex; gap: 12px; }
.dialog__foot button { flex: 1; height: 40px; border-radius: 20px; font-size: 14px; cursor: pointer; }
.btn-dialog-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-dialog-confirm { background: #f5222d; color: #fff; border: none; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="hut-booking-all.html"></a><span class="nav-title">预约详情</span></div>
<div class="scroll-content">
<!-- 电子二维码 + 预约状态 -->
<div class="qrcode-card" id="qrCard">
<div class="qrcode-card__head">
<div class="qrcode-card__title">预约电子码</div>
<span class="status status--booked" id="statusBadge">已预约</span>
</div>
<div class="qrcode-card__hint">请向医务人员出示,扫码核验预约信息</div>
<div class="qr-box" id="qrBox"></div>
<div class="qrcode-card__expire">本码仅用于本次预约核验,请勿转借他人</div>
</div>
<!-- 预约信息 -->
<div class="info-card">
<div class="info-card__status" id="infoStatus"><span class="status" id="statusBadge2">已预约</span></div>
<div class="row"><span class="row__label">预约单号</span><span class="row__value row__value--bold" id="fNo"></span></div>
<div class="row"><span class="row__label">预约人</span><span class="row__value">张旭</span></div>
<div class="row"><span class="row__label">预约手机号</span><span class="row__value">18544555589</span></div>
<div class="row"><span class="row__label">预约健康小屋</span><span class="row__value" id="fHut"></span></div>
<div class="row">
<span class="row__label">健康小屋位置</span>
<span class="row__value" id="fAddr"></span>
<span class="nav-btn"><svg width="15" height="15" viewBox="0 0 24 24" fill="#1677ff"><path d="M2 21l21-9L2 3v7l15 2-15 2z"/></svg></span>
</div>
<div class="row"><span class="row__label">预约设备</span><span class="row__value" id="fDev"></span></div>
<div class="row"><span class="row__label">预约时段</span><span class="row__value" id="fTime"></span></div>
<div class="row" id="rowCheckin" style="display:none"><span class="row__label">签到时间</span><span class="row__value" id="fCheckin"></span></div>
<div class="row" id="rowCancelTime" style="display:none"><span class="row__label">取消时间</span><span class="row__value" id="fCancelTime"></span></div>
<div class="row"><span class="row__label">创建时间</span><span class="row__value" id="fCreated"></span></div>
</div>
<!-- 取消预约(仅已预约状态显示) -->
<div class="cancel-bar" id="cancelBar">
<button class="btn-cancel-booking" onclick="openCancelDialog()">取消预约</button>
</div>
</div>
<div class="bottom-spacer"></div>
<!-- 取消预约二次确认弹窗 -->
<div class="dialog-overlay" id="cancelDialog">
<div class="dialog">
<div class="dialog__title">取消预约</div>
<div class="dialog__text">确定要取消该预约吗?取消后需重新预约。</div>
<div class="dialog__foot">
<button class="btn-dialog-cancel" onclick="closeCancelDialog()">暂不取消</button>
<button class="btn-dialog-confirm" onclick="confirmCancel()">确定取消</button>
</div>
</div>
</div>
</div>
<script>
var ADDR = {
'樊学站点健康小屋': '定边县樊学镇樊学站点院内',
'兴隆园健康小屋': '西安市未央区CQ兴隆园小区',
'机关健康小屋': 'CQ能源公司机关大楼一层'
};
var q = new URLSearchParams(location.search);
var hut = q.get('hut') || '樊学站点健康小屋';
var dev = q.get('dev') || '血压检测仪';
var date = q.get('date') || '2026-09-09';
var time = q.get('time') || '09:00 - 09:30';
var status = q.get('status') || 'booked';
// 预约状态:已预约徽标在电子码卡片上;已完成/已隐藏电子码时在信息卡片右上角补显
function statusInfo() {
if (status === 'done') return { t: '已完成', c: 'status--done' };
if (status === 'cancelled') return { t: '已取消', c: 'status--cancelled' };
return { t: '已预约', c: 'status--booked' };
}
var badge = document.getElementById('statusBadge');
function renderStatus() {
var s = statusInfo();
badge.textContent = s.t;
badge.className = 'status ' + s.c;
var alt = document.getElementById('statusBadge2');
alt.textContent = s.t;
alt.className = 'status ' + s.c;
}
if (status === 'done' || status === 'cancelled') {
document.getElementById('qrCard').style.display = 'none';
document.getElementById('infoStatus').style.display = 'flex';
}
// 已取消:预约时间下方展示取消时间(取预约时段开始前 1 小时)
function showCancelTime() {
var start = (time.split(' - ')[0] || '09:00').split(':');
var dc = new Date(date + 'T00:00:00');
dc.setHours(parseInt(start[0], 10), parseInt(start[1], 10) - 60);
var p = function (n) { return (n < 10 ? '0' : '') + n; };
document.getElementById('fCancelTime').textContent = date + ' ' + p(dc.getHours()) + ':' + p(dc.getMinutes());
document.getElementById('rowCancelTime').style.display = 'flex';
}
if (status === 'cancelled') showCancelTime();
renderStatus();
// 取消预约:仅已预约状态展示按钮,点击后弹二次确认,确认才改为已取消
var cancelBar = document.getElementById('cancelBar');
if (status === 'booked') cancelBar.style.display = 'block';
function openCancelDialog() { document.getElementById('cancelDialog').classList.add('open'); }
function closeCancelDialog() { document.getElementById('cancelDialog').classList.remove('open'); }
function confirmCancel() {
status = 'cancelled';
renderStatus();
cancelBar.style.display = 'none';
document.getElementById('qrCard').style.display = 'none';
document.getElementById('infoStatus').style.display = 'flex';
showCancelTime();
closeCancelDialog();
}
// 预约单号:按预约信息生成确定性编号
var seedStr = hut + dev + date;
var seed = 0;
for (var i = 0; i < seedStr.length; i++) seed = (seed * 31 + seedStr.charCodeAt(i)) % 100000;
document.getElementById('fNo').textContent = 'HB' + date.replace(/-/g, '') + ('0000' + seed).slice(-4);
document.getElementById('fHut').textContent = hut;
document.getElementById('fAddr').textContent = ADDR[hut] || ADDR['樊学站点健康小屋'];
document.getElementById('fDev').textContent = dev;
document.getElementById('fTime').textContent = date + ' ' + time;
// 已完成:不显示电子码,预约时间下方展示签到时间(取预约时段开始后 3 分钟)
if (status === 'done') {
document.getElementById('qrCard').style.display = 'none';
var start = (time.split(' - ')[0] || '09:00').split(':');
var d0 = new Date(date + 'T00:00:00');
d0.setHours(parseInt(start[0], 10), parseInt(start[1], 10) + 3);
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
document.getElementById('fCheckin').textContent = date + ' ' + pad(d0.getHours()) + ':' + pad(d0.getMinutes());
document.getElementById('rowCheckin').style.display = 'flex';
}
// 创建时间:预约日期前一天
var d = new Date(date + 'T00:00:00');
d.setDate(d.getDate() - 1);
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
document.getElementById('fCreated').textContent = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' 16:32';
// 生成 21×21 伪二维码(含三个定位角框,按预约单号作 seed 确定性填充)
(function () {
var N = 21, s = seed || 1;
function rand() { s = (s * 9301 + 49297) % 233280; return s / 233280; }
var box = document.getElementById('qrBox');
function finder(r, c) {
if (r < 7 && c < 7) return true;
if (r < 7 && c >= N - 7) return true;
if (r >= N - 7 && c < 7) return true;
return false;
}
function finderDark(r, c) {
var local = function (rr, cc) {
var outer = rr === 0 || rr === 6 || cc === 0 || cc === 6;
var inner = rr >= 2 && rr <= 4 && cc >= 2 && cc <= 4;
return outer || inner;
};
if (r < 7 && c < 7) return local(r, c);
if (r < 7 && c >= N - 7) return local(r, c - (N - 7));
if (r >= N - 7 && c < 7) return local(r - (N - 7), c);
return false;
}
for (var r = 0; r < N; r++) {
for (var c = 0; c < N; c++) {
var cell = document.createElement('div');
cell.className = 'qr-cell';
if (finder(r, c)) {
if (finderDark(r, c)) cell.classList.add('qr-cell--dark');
} else if (rand() > 0.5) {
cell.classList.add('qr-cell--dark');
}
box.appendChild(cell);
}
}
})();
</script>
</body>
</html>
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的预约 · 已完成</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 12px 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.tab-bar { display: flex; background: #fff; border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
.tab-bar__item { flex: 1; text-align: center; padding: 10px 0; font-size: 13px; color: #666; cursor: pointer; border-bottom: 2px solid transparent; }
.tab-bar__item--active { color: #1677ff; font-weight: 600; border-bottom-color: #1677ff; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card__room { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__status { font-size: 12px; padding: 2px 10px; border-radius: 10px; font-weight: 500; background: #e6f9ef; color: #52c41a; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.card__main { display: flex; gap: 10px; align-items: center; }
.card__img { width: 64px; height: 64px; border-radius: 8px; background: #f5f7fa; border: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: center; color: #bbb; font-size: 11px; flex-shrink: 0; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">我的预约</span></div>
<div class="scroll-content">
<div class="tab-bar">
<div class="tab-bar__item" onclick="window.location.href='hut-booking-all.html'">全部</div>
<div class="tab-bar__item" onclick="window.location.href='hut-booking-booked.html'">已预约</div>
<div class="tab-bar__item tab-bar__item--active">已完成</div>
</div>
<div class="card" onclick="goDetail('樊学站点健康小屋','done','体脂秤','2026-09-02','09:00 - 09:15')">
<div class="card__head">
<span class="card__room">2026-09-02 09:00 - 09:15</span>
<span class="card__status">已完成</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>体脂秤</div>
<div><b>健康小屋:</b>樊学站点健康小屋</div>
</div>
</div>
</div>
<div class="card" onclick="goDetail('兴隆园健康小屋','done','血压检测仪','2026-08-25','14:00 - 14:30')">
<div class="card__head">
<span class="card__room">2026-08-25 14:00 - 14:30</span>
<span class="card__status">已完成</span>
</div>
<div class="card__main">
<div class="card__img">设备图片</div>
<div class="card__info">
<div><b>设备:</b>血压检测仪</div>
<div><b>健康小屋:</b>兴隆园健康小屋</div>
</div>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
<script>
// 跳转预约详情,携带预约信息
function goDetail(hut, status, dev, date, time) {
var q = new URLSearchParams({ hut: hut, status: status, dev: dev, date: date, time: time });
window.location.href = 'hut-booking-detail.html?' + q.toString();
}
</script>
</body>
</html>
@@ -0,0 +1,255 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>预约信息</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #fff; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 预约人/设备信息 */
.ti-row { display: flex; align-items: center; justify-content: space-between; padding: 16px; }
.ti-row__label { font-size: 17px; font-weight: 700; color: #1a1a1a; }
.ti-row__value { font-size: 16px; color: #999; }
.divider { height: 10px; background: #f0f2f5; }
.sec-title { padding: 18px 16px 4px; font-size: 18px; font-weight: 700; color: #1a1a1a; }
.sec-title--sub { font-size: 16px; font-weight: 400; color: #333; padding-top: 20px; }
/* 日历 */
.cal__head { display: flex; align-items: center; justify-content: center; gap: 30px; margin: 10px 0 2px; }
.cal__arrow { cursor: pointer; color: #666; font-size: 20px; line-height: 1; padding: 6px 12px; user-select: none; }
.cal__arrow--off { color: #d0d0d0; cursor: default; }
.cal__month { font-size: 19px; font-weight: 700; color: #1a1a1a; }
.cal__week, .cal__grid { display: grid; grid-template-columns: repeat(7, 1fr); padding: 0 10px; }
.cal__week span { text-align: center; font-size: 15px; color: #333; font-weight: 600; padding: 14px 0 6px; }
.cal-cell { height: 48px; display: flex; align-items: center; justify-content: center; }
.cal-cell span { width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; color: #333; }
.cal-cell--dim span, .cal-cell--off span { color: #c8c9cc; }
.cal-cell--sel span { background: #00b578; color: #fff; font-weight: 600; }
.cal-cell--pick { cursor: pointer; }
/* 时段网格 */
.slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px 12px; padding: 10px 16px 16px; }
.slot-chip { width: 100%; height: 44px; border: 1px solid #e0e0e0; border-radius: 22px; background: #fff; font-size: 15px; color: #333; cursor: pointer; }
.slot-chip--active { border-color: #00b578; color: #00b578; background: #e8f8f1; }
.slot-chip--off { background: #d9d9d9; border-color: #d9d9d9; color: #aaa; cursor: not-allowed; }
/* 底部确定按钮 */
.footbar { flex-shrink: 0; padding: 10px 16px 24px; background: #fff; border-top: 1px solid #f0f0f0; }
.btn-submit { width: 100%; height: 46px; border: none; border-radius: 23px; background: #00b578; color: #fff; font-size: 16px; font-weight: 600; cursor: pointer; }
.btn-submit--disabled { background: #d9d9d9; cursor: not-allowed; }
/* 确定预约二次确认弹窗 */
.dialog-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 70; display: none; align-items: center; justify-content: center; }
.dialog-overlay.open { display: flex; }
.dialog { width: 330px; background: #fff; border-radius: 14px; padding: 24px 20px 16px; text-align: center; }
.dialog__title { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; }
.dialog__info { background: #f7f8fa; border-radius: 10px; padding: 10px 14px; margin-bottom: 20px; text-align: left; }
.dialog__row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; line-height: 2.2; }
.dialog__row span:first-child { color: #999; flex-shrink: 0; }
.dialog__row span:last-child { color: #1a1a1a; font-weight: 600; white-space: nowrap; text-align: right; }
.dialog__foot { display: flex; gap: 12px; }
.dialog__foot button { flex: 1; height: 40px; border-radius: 20px; font-size: 14px; cursor: pointer; }
.btn-dialog-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-dialog-confirm { background: #00b578; color: #fff; border: none; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="hut-detail.html"></a><span class="nav-title">预约信息</span></div>
<div class="scroll-content">
<!-- 预约人 / 预约设备 -->
<div class="ti-row"><span class="ti-row__label">预约人</span><span class="ti-row__value">张旭-男-健康科</span></div>
<div class="ti-row"><span class="ti-row__label">预约设备</span><span class="ti-row__value" id="tiDev">血压检测仪</span></div>
<div class="divider"></div>
<!-- 选择时间 -->
<div class="sec-title">选择时间</div>
<div class="cal__head">
<span class="cal__arrow" id="calPrev"></span>
<span class="cal__month" id="calMonth">2026年09月</span>
<span class="cal__arrow" id="calNext"></span>
</div>
<div class="cal__week"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
<div class="cal__grid" id="calGrid"></div>
<!-- 选择具体时间 -->
<div class="sec-title sec-title--sub">选择具体时间</div>
<div class="slots" id="slotGrid"></div>
</div>
<!-- 底部确定 -->
<div class="footbar">
<button class="btn-submit btn-submit--disabled" id="btnConfirm" disabled>确定预约</button>
</div>
<!-- 确定预约二次确认弹窗 -->
<div class="dialog-overlay" id="confirmDialog">
<div class="dialog">
<div class="dialog__title">确认预约</div>
<div class="dialog__info">
<div class="dialog__row"><span>预约设备</span><span id="dlgDev"></span></div>
<div class="dialog__row"><span>预约时段</span><span id="dlgTime"></span></div>
</div>
<div class="dialog__foot">
<button class="btn-dialog-cancel" id="dlgCancelBtn">取消</button>
<button class="btn-dialog-confirm" id="dlgOkBtn">确定预约</button>
</div>
</div>
</div>
</div>
<script>
// 原型时间锚定 2026-09-10 12:00(与列表页"今天 09-10"、状态栏时间一致)
var TODAY = { y: 2026, m: 9, d: 10, h: 12, min: 0 };
var view = { y: 2026, m: 9 };
var FULL = { 13: 1, 17: 1, 18: 1, 19: 1, 20: 1, 21: 1, 22: 1, 23: 1, 24: 1, 25: 1, 26: 1, 27: 1, 28: 1, 29: 1, 30: 1 }; // 9月已约满/不可约
var sel = null;
var slotSel = null;
var params = new URLSearchParams(location.search);
document.getElementById('tiDev').textContent = params.get('name') || '血压检测仪';
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function daysInMonth(y, m) { return new Date(y, m, 0).getDate(); }
function firstWeekday(y, m) { return new Date(y, m - 1, 1).getDay(); }
function isPast(y, m, d) { return y < TODAY.y || (y === TODAY.y && m < TODAY.m) || (y === TODAY.y && m === TODAY.m && d < TODAY.d); }
function isOff(y, m, d) { return isPast(y, m, d) || (y === 2026 && m === 9 && FULL[d]); }
function renderCal() {
document.getElementById('calMonth').textContent = view.y + '年' + pad(view.m) + '月';
document.getElementById('calPrev').className = view.y === 2026 && view.m === 9 ? 'cal__arrow cal__arrow--off' : 'cal__arrow';
document.getElementById('calNext').className = view.y === 2026 && view.m === 12 ? 'cal__arrow cal__arrow--off' : 'cal__arrow';
var grid = document.getElementById('calGrid');
grid.innerHTML = '';
var prevM = view.m === 1 ? 12 : view.m - 1, prevY = view.m === 1 ? view.y - 1 : view.y;
var nextM = view.m === 12 ? 1 : view.m + 1, nextY = view.m === 12 ? view.y + 1 : view.y;
var dim = daysInMonth(view.y, view.m), lead = firstWeekday(view.y, view.m);
var cells = Math.ceil((lead + dim) / 7) * 7;
for (var i = 0; i < cells; i++) {
(function (i) {
var cell = document.createElement('div');
cell.className = 'cal-cell';
var span = document.createElement('span');
if (i < lead) {
span.textContent = daysInMonth(prevY, prevM) - lead + 1 + i;
cell.classList.add('cal-cell--dim');
} else if (i >= lead + dim) {
span.textContent = i - (lead + dim) + 1;
cell.classList.add('cal-cell--dim');
} else {
var d = i - lead + 1;
span.textContent = d;
if (isOff(view.y, view.m, d)) {
cell.classList.add('cal-cell--off');
} else {
cell.classList.add('cal-cell--pick');
if (sel && sel.y === view.y && sel.m === view.m && sel.d === d) cell.classList.add('cal-cell--sel');
cell.addEventListener('click', function () {
sel = { y: view.y, m: view.m, d: d };
clearSlots();
renderCal();
});
}
}
cell.appendChild(span);
grid.appendChild(cell);
})(i);
}
}
// 时段:off 停用 / full 已约满 / 今天已开始的时刻均置灰不可选
var SLOTS = [
{ t: '09:30 - 09:50' }, { t: '09:50 - 10:10', full: true }, { t: '10:10 - 10:30' },
{ t: '10:30 - 10:50' }, { t: '10:50 - 11:10' }, { t: '11:10 - 11:30' },
{ t: '11:30 - 11:50' }, { t: '12:00 - 12:20' }, { t: '12:20 - 12:40' },
{ t: '12:40 - 13:00' }, { t: '13:00 - 13:20', off: true }, { t: '13:20 - 13:40', off: true }
];
function slotOff(y, m, d, s) {
if (s.off || s.full) return true;
if (y === TODAY.y && m === TODAY.m && d === TODAY.d) {
var hm = s.t.split(' - ')[0].split(':');
return (+hm[0]) * 60 + (+hm[1]) < TODAY.h * 60 + TODAY.min;
}
return false;
}
function renderSlots() {
var wrap = document.getElementById('slotGrid');
wrap.innerHTML = '';
SLOTS.forEach(function (s) {
var cell = document.createElement('div');
var btn = document.createElement('button');
btn.type = 'button';
var off = slotOff(sel.y, sel.m, sel.d, s);
btn.className = 'slot-chip' + (off ? ' slot-chip--off' : '') + (slotSel === s.t ? ' slot-chip--active' : '');
btn.textContent = s.t;
if (off) {
btn.disabled = true;
} else {
btn.addEventListener('click', function () {
slotSel = s.t;
renderSlots();
setConfirm(true);
});
}
cell.appendChild(btn);
wrap.appendChild(cell);
});
}
function clearSlots() { slotSel = null; renderSlots(); setConfirm(false); }
function setConfirm(on) {
var b = document.getElementById('btnConfirm');
b.disabled = !on;
b.className = 'btn-submit' + (on ? '' : ' btn-submit--disabled');
}
// 确定预约:先弹设备/时间段二次确认,确认后进入预约详情(已预约)
function selDateStr() { return sel.y + '-' + pad(sel.m) + '-' + pad(sel.d); }
document.getElementById('btnConfirm').addEventListener('click', function () {
if (!slotSel) return;
document.getElementById('dlgDev').textContent = document.getElementById('tiDev').textContent;
document.getElementById('dlgTime').textContent = selDateStr() + ' ' + slotSel;
document.getElementById('confirmDialog').classList.add('open');
});
document.getElementById('dlgCancelBtn').addEventListener('click', function () {
document.getElementById('confirmDialog').classList.remove('open');
});
document.getElementById('dlgOkBtn').addEventListener('click', function () {
location.href = 'hut-booking-detail.html?dev=' + encodeURIComponent(document.getElementById('tiDev').textContent)
+ '&date=' + selDateStr()
+ '&time=' + encodeURIComponent(slotSel)
+ '&status=booked';
});
document.getElementById('calPrev').addEventListener('click', function () {
if (view.y === 2026 && view.m === 9) return;
view.m -= 1; if (view.m === 0) { view.m = 12; view.y -= 1; }
clearSlots(); renderCal();
});
document.getElementById('calNext').addEventListener('click', function () {
if (view.y === 2026 && view.m === 12) return;
view.m += 1; if (view.m === 13) { view.m = 1; view.y += 1; }
clearSlots(); renderCal();
});
sel = { y: TODAY.y, m: TODAY.m, d: TODAY.d };
renderCal();
renderSlots();
</script>
</body>
</html>
@@ -0,0 +1,315 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>预约设备</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #fff; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 小屋信息 */
.hut { display: flex; gap: 12px; padding: 14px 16px; align-items: flex-start; }
.hut__img { width: 84px; height: 84px; border-radius: 8px; background: linear-gradient(135deg, #e8f4ff, #d6e8ff); display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px; flex-shrink: 0; }
.hut__body { flex: 1; min-width: 0; }
.hut__name { font-size: 16px; font-weight: 700; color: #1a1a1a; line-height: 1.4; }
.hut__addr { font-size: 13px; color: #999; margin-top: 8px; }
.hut__time { font-size: 13px; color: #999; margin-top: 4px; }
.hut__nav { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 4px; margin-left: 4px; }
.hut__nav-btn { width: 36px; height: 36px; border-radius: 50%; background: #c9cdd4; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.hut__dist { font-size: 13px; color: #666; }
.hut-tip { margin: 0 16px 4px; padding: 7px 10px; border-radius: 6px; background: #fff3e6; color: #fa8c16; font-size: 12px; display: none; align-items: center; gap: 5px; }
/* 设备分类 Tab */
.tabs { display: flex; padding: 0 8px; border-bottom: 1px solid #f0f0f0; }
.tab { flex: 1; text-align: center; font-size: 15px; color: #333; padding: 12px 0 10px; cursor: pointer; position: relative; }
.tab--active { color: #1a1a1a; font-weight: 600; }
.tab--active::after { content: ''; position: absolute; left: 50%; transform: translateX(-50%); bottom: 0; width: 28px; height: 3px; border-radius: 2px; background: linear-gradient(90deg, #3a7bf2, #00c48c); }
/* 设备列表 */
.dev-list { padding: 0 16px; }
.dev { display: flex; gap: 12px; padding: 14px 0; border-bottom: 1px solid #f0f0f0; align-items: center; cursor: pointer; }
.dev:last-child { border-bottom: none; }
.dev__img { width: 84px; height: 84px; border-radius: 8px; background: #f5f7fa; border: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: center; color: #bbb; font-size: 12px; flex-shrink: 0; }
.dev__body { flex: 1; min-width: 0; }
.dev__name { font-size: 16px; font-weight: 600; color: #1a1a1a; }
.dev__desc { font-size: 13px; color: #999; margin-top: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn-book { flex-shrink: 0; height: 34px; padding: 0 14px; border-radius: 17px; border: none; background: #00b578; color: #fff; font-size: 14px; cursor: pointer; }
.btn-book--disabled { background: #d9d9d9; cursor: not-allowed; }
/* 预约弹窗 */
.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 60; display: none; align-items: flex-end; }
.overlay.open { display: flex; }
.sheet { width: 100%; background: #fff; border-radius: 20px 20px 0 0; padding: 20px 20px 28px; }
.sheet__title { font-size: 16px; font-weight: 700; color: #1a1a1a; margin-bottom: 4px; }
.sheet__sub { font-size: 12px; color: #999; margin-bottom: 16px; }
.sheet__label { font-size: 13px; color: #666; margin-bottom: 8px; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.chip { padding: 7px 14px; border-radius: 16px; border: 1px solid #e0e0e0; font-size: 13px; color: #333; cursor: pointer; }
.chip--active { border-color: #00b578; color: #00b578; background: #e8f8f1; }
.chip--disabled { color: #ccc; border-color: #f0f0f0; cursor: not-allowed; }
.sheet__foot { display: flex; gap: 12px; margin-top: 20px; }
.sheet__foot button { flex: 1; height: 44px; border-radius: 22px; font-size: 15px; cursor: pointer; }
.btn-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-confirm { background: #00b578; color: #fff; border: none; }
.notice { font-size: 13px; color: #666; line-height: 1.9; white-space: pre-line; }
.sheet__dev { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.sheet__dev-name { font-size: 16px; font-weight: 700; color: #1a1a1a; }
.sheet__dev-tag { padding: 2px 8px; border-radius: 5px; font-size: 11px; color: #fff; }
.sheet__dev-tag--measure { background: #00b578; }
.sheet__dev-tag--psych { background: #7a72e6; }
.sheet__dev-tag--treat { background: #3a7bf2; }
.sheet__dev-tag--monitor { background: #fa8c16; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="hut-list.html"></a><span class="nav-title">预约设备</span></div>
<div class="scroll-content">
<!-- 小屋信息 -->
<div class="hut">
<div class="hut__img">小屋图片</div>
<div class="hut__body">
<div class="hut__name">樊学站点健康小屋</div>
<div class="hut__addr">定边县樊学镇樊学站点院内</div>
<div class="hut__time">07:30 - 12:00 | 12:00 - 18:30</div>
</div>
<div class="hut__nav">
<div class="hut__nav-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="#fff"><path d="M2 21l21-9L2 3v7l15 2-15 2z"/></svg></div>
<span class="hut__dist">3.5 km</span>
</div>
</div>
<!-- 不可用提示 -->
<div class="hut-tip" id="hutTip"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fa8c16" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16.5v.01"/></svg>当前所在单位不在可用范围</div>
<!-- 设备分类 -->
<div class="tabs" id="tabs">
<span class="tab tab--active" data-cat="measure">测量设备</span>
<span class="tab" data-cat="psych">心理检测</span>
<span class="tab" data-cat="treat">治疗设备</span>
<span class="tab" data-cat="monitor">监测设备</span>
</div>
<!-- 设备列表 -->
<div class="dev-list" id="devList">
<div class="dev" data-cat="measure">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">血压检测仪</div>
<div class="dev__desc">自动测量血压与脉搏,语音引导操作</div>
</div>
<button class="btn-book" onclick="openSheet('血压检测仪')">立即预约</button>
</div>
<div class="dev" data-cat="measure">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">血糖检测仪</div>
<div class="dev__desc">指尖采血快速检测,5 秒出结果</div>
</div>
<button class="btn-book" onclick="openSheet('血糖检测仪')">立即预约</button>
</div>
<div class="dev" data-cat="measure">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">体脂秤</div>
<div class="dev__desc">多频生物电阻抗技术,体成分分析</div>
</div>
<button class="btn-book" onclick="openSheet('体脂秤')">立即预约</button>
</div>
<div class="dev" data-cat="treat" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">智能艾灸治疗仪</div>
<div class="dev__desc">恒温艾灸理疗,缓解疲劳不适</div>
</div>
<button class="btn-book" onclick="openSheet('智能艾灸治疗仪')">立即预约</button>
</div>
<div class="dev" data-cat="treat" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">中频脉冲治疗仪</div>
<div class="dev__desc">中频脉冲理疗,改善局部血液循环</div>
</div>
<button class="btn-book" onclick="openSheet('中频脉冲治疗仪')">立即预约</button>
</div>
<div class="dev" data-cat="monitor" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">动态血压监测仪</div>
<div class="dev__desc">监测设备免预约,到店登记使用</div>
</div>
<button class="btn-book btn-book--disabled" disabled>免预约</button>
</div>
<div class="dev" data-cat="monitor" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">动态心电监测仪</div>
<div class="dev__desc">监测设备免预约,到店登记使用</div>
</div>
<button class="btn-book btn-book--disabled" disabled>免预约</button>
</div>
<div class="dev" data-cat="psych" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">经络深睡眠舱</div>
<div class="dev__desc">心理学、催眠技术</div>
</div>
<button class="btn-book" onclick="openSheet('经络深睡眠舱')">立即预约</button>
</div>
<div class="dev" data-cat="psych" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">3D心理数字训练舱</div>
<div class="dev__desc">3D情景互动训练,改善情绪与认知</div>
</div>
<button class="btn-book" onclick="openSheet('3D心理数字训练舱')">立即预约</button>
</div>
<div class="dev" data-cat="psych" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">沉浸式冥想放松舱</div>
<div class="dev__desc">沉浸式冥想引导,舒缓压力放松身心</div>
</div>
<button class="btn-book" onclick="openSheet('沉浸式冥想放松舱')">立即预约</button>
</div>
<div class="dev" data-cat="psych" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">运动心理康养舱</div>
<div class="dev__desc">运动与心理双向调节,缓解身心疲劳</div>
</div>
<button class="btn-book" onclick="openSheet('运动心理康养舱')">立即预约</button>
</div>
<div class="dev" data-cat="psych" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">心理智能穿戴设备</div>
<div class="dev__desc">情绪、压力等心理指标实时监测</div>
</div>
<button class="btn-book" onclick="openSheet('心理智能穿戴设备')">立即预约</button>
</div>
<div class="dev" data-cat="psych" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">心理测评一体机</div>
<div class="dev__desc">多种专业心理量表,自动生成测评报告</div>
</div>
<button class="btn-book" onclick="openSheet('心理测评一体机')">立即预约</button>
</div>
<div class="dev" data-cat="psych" style="display:none">
<div class="dev__img">设备图片</div>
<div class="dev__body">
<div class="dev__name">心理测试一体机</div>
<div class="dev__desc">常规心理测试,评估心理健康状态</div>
</div>
<button class="btn-book" onclick="openSheet('心理测试一体机')">立即预约</button>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
<!-- 预约须知弹窗 -->
<div class="overlay" id="bookSheet">
<div class="sheet">
<div class="sheet__title">预约须知</div>
<div class="sheet__dev">
<span class="sheet__dev-name" id="sheetDevName">血压检测仪</span>
<span class="sheet__dev-tag" id="sheetDevTag">测量设备</span>
</div>
<div class="notice" id="noticeBody"></div>
<div class="sheet__foot">
<button class="btn-cancel" onclick="closeSheet()">取消</button>
<button class="btn-confirm" id="btnGoBooking">确定</button>
</div>
</div>
</div>
</div>
<script>
var CAT_LABEL = { measure: '测量设备', psych: '心理检测', treat: '治疗设备', monitor: '监测设备' };
var DEV_INFO = {
'血压检测仪': 'measure', '血糖检测仪': 'measure', '体脂秤': 'measure',
'经络深睡眠舱': 'psych', '3D心理数字训练舱': 'psych', '沉浸式冥想放松舱': 'psych',
'运动心理康养舱': 'psych', '心理智能穿戴设备': 'psych', '心理测评一体机': 'psych', '心理测试一体机': 'psych',
'智能艾灸治疗仪': 'treat', '中频脉冲治疗仪': 'treat',
'动态血压监测仪': 'monitor', '动态心电监测仪': 'monitor'
};
// 每台设备各自的预约须知
var NOTICES = {
'血压检测仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 测量前静坐 5 分钟,避免剧烈运动、吸烟、饮咖啡后立即测量;\n3. 单次预约时长 30 分钟,每人每日最多预约 2 台设备;\n4. 测量数据将自动同步至个人健康档案。',
'血糖检测仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 建议空腹或餐后 2 小时检测,采血前用温水洗手促进循环;\n3. 单次预约时长 30 分钟,每人每日最多预约 2 台设备;\n4. 检测数据将自动同步至个人健康档案。',
'体脂秤': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 测量时赤足上秤、穿着轻薄衣物,测前避免大量饮水与剧烈运动;\n3. 单次预约时长 30 分钟,每人每日最多预约 2 台设备;\n4. 心脏起搏器植入者等特殊人群请勿使用。',
'经络深睡眠舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次体验 30 分钟,入场请摘除手表、首饰等金属物品;\n3. 着装宽松,舱内温度较高,如有不适请随时按呼叫铃;\n4. 孕妇及心脑血管疾病急性期人群不建议使用。',
'3D心理数字训练舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次训练 30 分钟,训练前请听从工作人员引导完成基础测评;\n3. 训练过程中如出现眩晕等不适请立即暂停;\n4. 训练报告将自动同步至个人健康档案。',
'沉浸式冥想放松舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次体验 30 分钟,入场请保持安静,手机调至静音;\n3. 着装宽松舒适,舱内备有一次性卫生垫;\n4. 有幽闭恐惧史请提前告知工作人员。',
'运动心理康养舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次体验 30 分钟,请穿着运动服装与防滑袜入场;\n3. 体验前后适量补水,避免空腹或饱腹状态参与;\n4. 高血压、心脏病人群请遵医嘱使用。',
'心理智能穿戴设备': '1. 请按预约时段到店,凭个人电子码签到领取设备;\n2. 佩戴于手腕、松紧适度,单次采集 30 分钟;\n3. 采集期间保持情绪平稳,避免剧烈活动影响数据;\n4. 设备请勿浸水、磕碰,使用完毕归还工作人员。',
'心理测评一体机': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 测评约 20-30 分钟,请在安静状态下独立作答,答案无对错之分;\n3. 测评结束自动生成报告,可在个人健康档案中查看;\n4. 结果异常建议预约专业心理咨询进一步评估。',
'心理测试一体机': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次测试约 20 分钟,请在安静状态下独立作答;\n3. 测试结束即出结果,结果仅供自我了解参考;\n4. 测试数据将自动同步至个人健康档案。',
'智能艾灸治疗仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次理疗 30 分钟,理疗部位皮肤保持清洁干燥;\n3. 温热感适宜即可,如有灼痛请立即告知工作人员;\n4. 孕妇、皮肤破损及糖尿病末梢感觉障碍者请勿使用。',
'中频脉冲治疗仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次理疗 30 分钟,强度由低到高缓慢调节,以耐受为宜;\n3. 皮肤破损、感染部位请勿放置电极片;\n4. 孕妇、心脏起搏器植入者请勿使用。',
'动态血压监测仪': '1. 免预约设备,到店登记后领取使用;\n2. 佩戴 24 小时,按日常作息活动,测量时保持手臂静止;\n3. 请勿自行摘除袖带或触碰设备按键;\n4. 监测结束归还设备,报告由工作人员解读并同步健康档案。',
'动态心电监测仪': '1. 免预约设备,到店登记后领取使用;\n2. 佩戴期间避免大量出汗、淋雨,电极片脱落请及时回店更换;\n3. 请记录佩戴期间不适症状的时间,便于报告分析;\n4. 监测结束归还设备,报告由专业医师解读。'
};
var DEFAULT_NOTICE = '1. 请按预约时段到店,凭个人电子码签到使用设备;\n2. 单次时长 30 分钟,每人每日最多预约 2 台设备;\n3. 如无法按时到店,请提前在预约记录中取消;\n4. 具体注意事项请咨询小屋工作人员。';
var currentDev = '血压检测仪';
function openSheet(dev) {
currentDev = dev;
document.getElementById('sheetDevName').textContent = dev;
var cat = DEV_INFO[dev] || 'measure';
var tag = document.getElementById('sheetDevTag');
tag.textContent = CAT_LABEL[cat];
tag.className = 'sheet__dev-tag sheet__dev-tag--' + cat;
document.getElementById('noticeBody').textContent = NOTICES[dev] || DEFAULT_NOTICE;
document.getElementById('bookSheet').classList.add('open');
}
function closeSheet() { document.getElementById('bookSheet').classList.remove('open'); }
// 预约须知确定 → 预约信息页
document.getElementById('btnGoBooking').addEventListener('click', function () {
location.href = 'hut-booking-info.html?name=' + encodeURIComponent(currentDev);
});
// 设备分类 Tab 切换
document.querySelectorAll('#tabs .tab').forEach(function (tab) {
tab.addEventListener('click', function () {
document.querySelectorAll('#tabs .tab').forEach(function (t) { t.classList.remove('tab--active'); });
tab.classList.add('tab--active');
document.querySelectorAll('#devList .dev').forEach(function (dev) {
dev.style.display = dev.dataset.cat === tab.dataset.cat ? 'flex' : 'none';
});
});
});
// 点击设备条目跳转设备详情,预约状态随参数带过去
document.querySelectorAll('#devList .btn-book').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.stopPropagation(); });
});
document.querySelectorAll('#devList .dev').forEach(function (row) {
row.addEventListener('click', function () {
var bookable = row.querySelector('.btn-book--disabled') ? 0 : 1;
var available = new URLSearchParams(location.search).get('available') === '0' ? 0 : 1;
location.href = 'hut-device-detail.html?name=' + encodeURIComponent(row.querySelector('.dev__name').textContent)
+ '&cat=' + row.dataset.cat + '&bookable=' + bookable + '&available=' + available;
});
});
// 接收列表页带来的可用状态:available=0 表示当前单位不在可用范围
if (new URLSearchParams(location.search).get('available') === '0') {
document.getElementById('hutTip').style.display = 'flex';
document.querySelectorAll('.btn-book:not(.btn-book--disabled)').forEach(function (btn) {
btn.disabled = true;
btn.classList.add('btn-book--disabled');
});
}
</script>
</body>
</html>
@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备详情</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #fff; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 设备大图 */
.hero { height: 250px; background: linear-gradient(135deg, #eef5ff, #e2ecfb); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #9db4d0; }
.hero__txt { font-size: 13px; }
/* 名称与归属信息 */
.head { padding: 16px 16px 18px; }
.head__name-row { display: flex; align-items: center; gap: 10px; }
.head__name { font-size: 20px; font-weight: 700; color: #1a1a1a; }
.tag { flex-shrink: 0; padding: 3px 10px; border-radius: 6px; font-size: 12px; color: #fff; }
.tag--measure { background: #00b578; }
.tag--psych { background: #7a72e6; }
.tag--treat { background: #3a7bf2; }
.tag--monitor { background: #fa8c16; }
.head__hut { font-size: 14px; color: #999; margin-top: 10px; }
.head__addr { font-size: 14px; color: #999; margin-top: 4px; }
.divider { height: 10px; background: #f0f2f5; }
/* 设备介绍 */
.intro { padding: 18px 16px 24px; }
.intro__title { font-size: 17px; font-weight: 700; color: #1a1a1a; margin-bottom: 12px; }
.intro__text { font-size: 15px; color: #666; line-height: 1.9; white-space: pre-line; }
/* 底部预约栏 */
.footbar { flex-shrink: 0; padding: 10px 16px 24px; background: #fff; border-top: 1px solid #f0f0f0; }
.btn-main { width: 100%; height: 46px; border: none; border-radius: 23px; background: linear-gradient(90deg, #00c48c, #3a7bf2); color: #fff; font-size: 16px; font-weight: 600; cursor: pointer; }
.btn-main--disabled { background: #d9d9d9; cursor: not-allowed; }
/* 预约弹窗 */
.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 60; display: none; align-items: flex-end; }
.overlay.open { display: flex; }
.sheet { width: 100%; background: #fff; border-radius: 20px 20px 0 0; padding: 20px 20px 28px; }
.sheet__title { font-size: 16px; font-weight: 700; color: #1a1a1a; margin-bottom: 4px; }
.sheet__sub { font-size: 12px; color: #999; margin-bottom: 16px; }
.sheet__label { font-size: 13px; color: #666; margin-bottom: 8px; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.chip { padding: 7px 14px; border-radius: 16px; border: 1px solid #e0e0e0; font-size: 13px; color: #333; cursor: pointer; }
.chip--active { border-color: #00b578; color: #00b578; background: #e8f8f1; }
.chip--disabled { color: #ccc; border-color: #f0f0f0; cursor: not-allowed; }
.sheet__foot { display: flex; gap: 12px; margin-top: 20px; }
.sheet__foot button { flex: 1; height: 44px; border-radius: 22px; font-size: 15px; cursor: pointer; }
.btn-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-confirm { background: #00b578; color: #fff; border: none; }
.notice { font-size: 13px; color: #666; line-height: 1.9; white-space: pre-line; }
.sheet__dev { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.sheet__dev-name { font-size: 16px; font-weight: 700; color: #1a1a1a; }
.sheet__dev-tag { padding: 2px 8px; border-radius: 5px; font-size: 11px; color: #fff; }
.sheet__dev-tag--measure { background: #00b578; }
.sheet__dev-tag--psych { background: #7a72e6; }
.sheet__dev-tag--treat { background: #3a7bf2; }
.sheet__dev-tag--monitor { background: #fa8c16; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" id="navBack" href="hut-detail.html"></a><span class="nav-title">设备详情</span></div>
<div class="scroll-content">
<!-- 设备大图 -->
<div class="hero">
<svg width="72" height="72" viewBox="0 0 24 24" fill="none" stroke="#b8cbe2" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M7 20h10M12 16v4"/><path d="M8 8h8M8 11h5"/></svg>
<span class="hero__txt">设备图片</span>
</div>
<!-- 名称与归属 -->
<div class="head">
<div class="head__name-row">
<span class="head__name" id="devName">血压检测仪</span>
<span class="tag tag--measure" id="devTag">测量设备</span>
</div>
<div class="head__hut">樊学站点健康小屋</div>
<div class="head__addr">定边县樊学镇樊学站点院内</div>
</div>
<div class="divider"></div>
<!-- 设备介绍 -->
<div class="intro">
<div class="intro__title">设备介绍</div>
<div class="intro__text" id="devIntro"></div>
</div>
</div>
<!-- 底部预约栏 -->
<div class="footbar">
<button class="btn-main" id="btnMain" onclick="openSheet()">立即预约</button>
</div>
<!-- 预约须知弹窗 -->
<div class="overlay" id="bookSheet">
<div class="sheet">
<div class="sheet__title">预约须知</div>
<div class="sheet__dev">
<span class="sheet__dev-name" id="sheetDevName">血压检测仪</span>
<span class="sheet__dev-tag" id="sheetDevTag">测量设备</span>
</div>
<div class="notice" id="noticeBody"></div>
<div class="sheet__foot">
<button class="btn-cancel" onclick="closeSheet()">取消</button>
<button class="btn-confirm" id="btnGoBooking">确定</button>
</div>
</div>
</div>
</div>
<script>
var CAT_LABEL = { measure: '测量设备', psych: '心理检测', treat: '治疗设备', monitor: '监测设备' };
var INTROS = {
'血压检测仪': '采用示波法自动测量收缩压、舒张压与脉搏,全程语音引导,适合中老年员工自主操作。\n测量数据自动同步至个人健康档案,异常数值现场提示并给出复测建议。\n适用于高血压筛查、日常血压监测与用药效果跟踪。',
'血糖检测仪': '指尖采血快速检测,5 秒出结果,试纸自动校准,操作简单。\n支持空腹血糖与餐后血糖记录,测量数据自动同步至个人健康档案。\n适用于糖尿病筛查、血糖日常监测与随访管理。',
'体脂秤': '采用多频生物电阻抗技术,一次测量体重、体脂率、肌肉量、基础代谢等十余项体成分指标。\n测量报告自动上传健康档案,可查看历史趋势曲线。\n适用于超重肥胖干预与体重管理效果评估。',
'经络深睡眠舱': '融合经络理论与现代催眠技术,通过温热理疗、经络疏导与助眠音疗,帮助身心进入深度放松状态。\n适用于睡眠质量差、易醒多梦、精神紧张人群的放松调理。',
'3D心理数字训练舱': '通过 3D 沉浸情景互动开展认知与情绪训练,结合脑电生物反馈实时调节训练难度。\n适用于压力管理、情绪调节与认知功能维护。',
'沉浸式冥想放松舱': '提供沉浸式冥想引导与音乐放松,配合呼吸训练与光影氛围,快速缓解身心疲劳。\n适用于高压人群减压、焦虑舒缓与日常情绪调适。',
'运动心理康养舱': '将运动康复与心理调适相结合,通过低强度有氧运动配合心理放松训练,实现身心双向调节。\n适用于久坐人群与亚健康状态的康复调理。',
'心理智能穿戴设备': '实时监测心率变异性、皮电、呼吸等情绪与压力相关指标,自动生成个人心理状态报告。\n适用于压力评估、情绪监测与放松训练效果评价。',
'心理测评一体机': '内置多种专业心理量表,涵盖情绪、压力、睡眠、职业倦怠等维度,测评结束自动生成报告。\n支持匿名测评,结果同步至个人健康档案,异常结果建议寻求专业咨询。',
'心理测试一体机': '提供常规心理测试与心理健康评估,操作简便,测试完成即出结果。\n适用于员工心理健康普查与日常自我评估。',
'智能艾灸治疗仪': '恒温艾灸理疗,模拟传统艾灸温热刺激,无烟无尘,自动控温防烫。\n适用于肩颈腰背不适、畏寒肢冷、疲劳综合征的调理。',
'中频脉冲治疗仪': '中频治疗仪是模拟传统的中医针灸,用 CPU 控制的脉冲电流针对穴道起到疏导血管、神经,达到顺畅传达的功效。\n适用症状:\n镇痛作用:肩周炎、肱骨外上髁炎、颈椎病、腰椎间盘突出症、退行性骨性关节病、风湿性关节炎、类风湿关节炎、捩伤、挫伤、肌纤维织炎、肌肉劳损、狭窄性腱鞘炎、盆腔炎、附件炎、慢性咽喉炎、乳腺增生、坐骨神经痛、周围神经损伤、关节挛缩、慢性前列腺炎。\n软化瘢痕松解粘连:瘢痕、瘢痕挛缩、术后粘连、肠粘连、炎症后硬化、注射后硬结、血肿机化、狭窄性腱鞘炎。\n兴奋神经肌肉:神经或肌肉伤病后肌肉功能障碍、废用性肌萎缩。',
'动态血压监测仪': '便携佩戴式动态血压监测,24 小时连续记录血压波动,自动生成监测日报。\n免预约设备,到店登记后领取使用,数据回传至个人健康档案。',
'动态心电监测仪': '便携式动态心电监测,长时间记录心电波形,捕捉偶发心律失常。\n免预约设备,到店登记后领取使用,报告由专业医师解读。'
};
var DEFAULT_INTRO = '本设备为健康小屋配套设备,具体功能与使用方式请到店咨询工作人员。';
// 每台设备各自的预约须知
var NOTICES = {
'血压检测仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 测量前静坐 5 分钟,避免剧烈运动、吸烟、饮咖啡后立即测量;\n3. 单次预约时长 30 分钟,每人每日最多预约 2 台设备;\n4. 测量数据将自动同步至个人健康档案。',
'血糖检测仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 建议空腹或餐后 2 小时检测,采血前用温水洗手促进循环;\n3. 单次预约时长 30 分钟,每人每日最多预约 2 台设备;\n4. 检测数据将自动同步至个人健康档案。',
'体脂秤': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 测量时赤足上秤、穿着轻薄衣物,测前避免大量饮水与剧烈运动;\n3. 单次预约时长 30 分钟,每人每日最多预约 2 台设备;\n4. 心脏起搏器植入者等特殊人群请勿使用。',
'经络深睡眠舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次体验 30 分钟,入场请摘除手表、首饰等金属物品;\n3. 着装宽松,舱内温度较高,如有不适请随时按呼叫铃;\n4. 孕妇及心脑血管疾病急性期人群不建议使用。',
'3D心理数字训练舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次训练 30 分钟,训练前请听从工作人员引导完成基础测评;\n3. 训练过程中如出现眩晕等不适请立即暂停;\n4. 训练报告将自动同步至个人健康档案。',
'沉浸式冥想放松舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次体验 30 分钟,入场请保持安静,手机调至静音;\n3. 着装宽松舒适,舱内备有一次性卫生垫;\n4. 有幽闭恐惧史请提前告知工作人员。',
'运动心理康养舱': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次体验 30 分钟,请穿着运动服装与防滑袜入场;\n3. 体验前后适量补水,避免空腹或饱腹状态参与;\n4. 高血压、心脏病人群请遵医嘱使用。',
'心理智能穿戴设备': '1. 请按预约时段到店,凭个人电子码签到领取设备;\n2. 佩戴于手腕、松紧适度,单次采集 30 分钟;\n3. 采集期间保持情绪平稳,避免剧烈活动影响数据;\n4. 设备请勿浸水、磕碰,使用完毕归还工作人员。',
'心理测评一体机': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 测评约 20-30 分钟,请在安静状态下独立作答,答案无对错之分;\n3. 测评结束自动生成报告,可在个人健康档案中查看;\n4. 结果异常建议预约专业心理咨询进一步评估。',
'心理测试一体机': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次测试约 20 分钟,请在安静状态下独立作答;\n3. 测试结束即出结果,结果仅供自我了解参考;\n4. 测试数据将自动同步至个人健康档案。',
'智能艾灸治疗仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次理疗 30 分钟,理疗部位皮肤保持清洁干燥;\n3. 温热感适宜即可,如有灼痛请立即告知工作人员;\n4. 孕妇、皮肤破损及糖尿病末梢感觉障碍者请勿使用。',
'中频脉冲治疗仪': '1. 请按预约时段到店,凭个人电子码签到使用;\n2. 单次理疗 30 分钟,强度由低到高缓慢调节,以耐受为宜;\n3. 皮肤破损、感染部位请勿放置电极片;\n4. 孕妇、心脏起搏器植入者请勿使用。',
'动态血压监测仪': '1. 免预约设备,到店登记后领取使用;\n2. 佩戴 24 小时,按日常作息活动,测量时保持手臂静止;\n3. 请勿自行摘除袖带或触碰设备按键;\n4. 监测结束归还设备,报告由工作人员解读并同步健康档案。',
'动态心电监测仪': '1. 免预约设备,到店登记后领取使用;\n2. 佩戴期间避免大量出汗、淋雨,电极片脱落请及时回店更换;\n3. 请记录佩戴期间不适症状的时间,便于报告分析;\n4. 监测结束归还设备,报告由专业医师解读。'
};
var DEFAULT_NOTICE = '1. 请按预约时段到店,凭个人电子码签到使用设备;\n2. 单次时长 30 分钟,每人每日最多预约 2 台设备;\n3. 如无法按时到店,请提前在预约记录中取消;\n4. 具体注意事项请咨询小屋工作人员。';
// 从列表页带过来的参数:name 设备名 / cat 分类 / bookable 是否可预约 / available 小屋是否可用
var params = new URLSearchParams(location.search);
var devName = params.get('name') || '血压检测仪';
var cat = params.get('cat') || 'measure';
var bookable = params.get('bookable') !== '0';
var available = params.get('available') !== '0';
document.title = devName + ' - 设备详情';
document.getElementById('devName').textContent = devName;
var tag = document.getElementById('devTag');
tag.textContent = CAT_LABEL[cat] || '健康设备';
tag.className = 'tag tag--' + cat;
document.getElementById('devIntro').textContent = INTROS[devName] || DEFAULT_INTRO;
document.getElementById('sheetDevName').textContent = devName;
var sheetTag = document.getElementById('sheetDevTag');
sheetTag.textContent = CAT_LABEL[cat] || '健康设备';
sheetTag.className = 'sheet__dev-tag sheet__dev-tag--' + cat;
document.getElementById('noticeBody').textContent = NOTICES[devName] || DEFAULT_NOTICE;
var btnMain = document.getElementById('btnMain');
if (!bookable) {
btnMain.textContent = '免预约';
btnMain.disabled = true;
btnMain.classList.add('btn-main--disabled');
} else if (!available) {
btnMain.textContent = '立即预约';
btnMain.disabled = true;
btnMain.classList.add('btn-main--disabled');
}
// 返回列表时保留可用状态参数
if (params.get('available') === '0') {
document.getElementById('navBack').href = 'hut-detail.html?available=0';
}
function openSheet() { if (btnMain.disabled) return; document.getElementById('bookSheet').classList.add('open'); }
function closeSheet() { document.getElementById('bookSheet').classList.remove('open'); }
// 预约须知确定 → 预约信息页
document.getElementById('btnGoBooking').addEventListener('click', function () {
location.href = 'hut-booking-info.html?name=' + encodeURIComponent(devName);
});
</script>
</body>
</html>
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线预约</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.card { background: #fff; border-radius: 12px; padding: 12px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; transition: transform 0.15s; display: flex; gap: 12px; flex-wrap: wrap; }
.card:active { transform: scale(0.98); }
.card__img { width: 92px; height: 92px; border-radius: 8px; background: linear-gradient(135deg, #e8f4ff, #d6e8ff); display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px; flex-shrink: 0; }
.card__body { flex: 1; min-width: 0; }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__loc { display: flex; align-items: center; gap: 3px; flex-shrink: 0; margin-left: 6px; font-size: 13px; color: #1677ff; font-weight: 600; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.card__tip { width: 100%; margin-top: 10px; padding: 7px 10px; border-radius: 6px; background: #fff3e6; color: #fa8c16; font-size: 12px; display: flex; align-items: center; gap: 5px; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">在线预约</span></div>
<div class="scroll-content">
<div class="card" onclick="window.location.href='hut-detail.html?available=1'">
<div class="card__img">小屋图片</div>
<div class="card__body">
<div class="card__head">
<span class="card__name">樊学站点健康小屋</span>
<span class="card__loc">3.5 km<svg width="14" height="14" viewBox="0 0 24 24" fill="#1677ff"><path d="M2 21l21-9L2 3v7l15 2-15 2z"/></svg></span>
</div>
<div class="card__info">
<div><b>开放时间:</b>07:30 - 12:00 | 12:00 - 18:30</div>
<div><b>设备数量:</b>4</div>
<div><b>地址:</b>定边县樊学镇樊学站点院内</div>
</div>
</div>
</div>
<div class="card" onclick="window.location.href='hut-detail.html?available=1'">
<div class="card__img">小屋图片</div>
<div class="card__body">
<div class="card__head">
<span class="card__name">兴隆园健康小屋</span>
<span class="card__loc">1.2 km<svg width="14" height="14" viewBox="0 0 24 24" fill="#1677ff"><path d="M2 21l21-9L2 3v7l15 2-15 2z"/></svg></span>
</div>
<div class="card__info">
<div><b>开放时间:</b>08:00 - 12:00 | 14:00 - 18:00</div>
<div><b>设备数量:</b>6</div>
<div><b>地址:</b>西安市未央区CQ兴隆园小区</div>
</div>
</div>
</div>
<div class="card" onclick="window.location.href='hut-detail.html?available=0'">
<div class="card__img">小屋图片</div>
<div class="card__body">
<div class="card__head">
<span class="card__name">机关健康小屋</span>
<span class="card__loc">2.6 km<svg width="14" height="14" viewBox="0 0 24 24" fill="#1677ff"><path d="M2 21l21-9L2 3v7l15 2-15 2z"/></svg></span>
</div>
<div class="card__info">
<div><b>开放时间:</b>07:30 - 12:00 | 12:00 - 18:30</div>
<div><b>设备数量:</b>5</div>
<div><b>地址:</b>CQ能源公司机关大楼一层</div>
</div>
</div>
<div class="card__tip"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fa8c16" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16.5v.01"/></svg>当前所在单位不在可用范围</div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测量记录</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.card { background: #fff; border-radius: 12px; padding: 12px; margin-bottom: 12px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; transition: transform 0.15s; }
.card:active { transform: scale(0.98); }
.card__img { width: 64px; height: 64px; border-radius: 8px; background: #f5f7fa; border: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: center; color: #bbb; font-size: 11px; flex-shrink: 0; }
.card__info { flex: 1; min-width: 0; }
.card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__meta { font-size: 12px; color: #999; margin-top: 4px; }
.card__arrow { font-size: 18px; color: #ccc; flex-shrink: 0; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">测量记录</span></div>
<div class="scroll-content">
<div class="card" onclick="goReport('多功能按摩椅','樊学站点健康小屋','2026-09-08 14:30')">
<div class="card__img">设备图片</div>
<div class="card__info">
<div class="card__name">多功能按摩椅</div>
<div class="card__meta">樊学站点健康小屋</div>
<div class="card__meta">2026-09-08 14:30</div>
</div>
<span class="card__arrow"></span>
</div>
<div class="card" onclick="goReport('中频治疗仪','樊学站点健康小屋','2026-09-08 10:15')">
<div class="card__img">设备图片</div>
<div class="card__info">
<div class="card__name">中频治疗仪</div>
<div class="card__meta">樊学站点健康小屋</div>
<div class="card__meta">2026-09-08 10:15</div>
</div>
<span class="card__arrow"></span>
</div>
<div class="card" onclick="goReport('经络深睡眠舱','兴隆园健康小屋','2026-09-05 16:20')">
<div class="card__img">设备图片</div>
<div class="card__info">
<div class="card__name">经络深睡眠舱</div>
<div class="card__meta">兴隆园健康小屋</div>
<div class="card__meta">2026-09-05 16:20</div>
</div>
<span class="card__arrow"></span>
</div>
<div class="card" onclick="goReport('红外热治疗仪','樊学站点健康小屋','2026-09-03 09:40')">
<div class="card__img">设备图片</div>
<div class="card__info">
<div class="card__name">红外热治疗仪</div>
<div class="card__meta">樊学站点健康小屋</div>
<div class="card__meta">2026-09-03 09:40</div>
</div>
<span class="card__arrow"></span>
</div>
<div class="card" onclick="goReport('多功能理疗床','兴隆园健康小屋','2026-08-30 15:10')">
<div class="card__img">设备图片</div>
<div class="card__info">
<div class="card__name">多功能理疗床</div>
<div class="card__meta">兴隆园健康小屋</div>
<div class="card__meta">2026-08-30 15:10</div>
</div>
<span class="card__arrow"></span>
</div>
<div class="card" onclick="goReport('沉浸式冥想训练','兴隆园健康小屋','2026-08-28 11:25')">
<div class="card__img">设备图片</div>
<div class="card__info">
<div class="card__name">沉浸式冥想训练</div>
<div class="card__meta">兴隆园健康小屋</div>
<div class="card__meta">2026-08-28 11:25</div>
</div>
<span class="card__arrow"></span>
</div>
<div class="card" onclick="goReport('多功能按摩椅','兴隆园健康小屋','2026-08-25 09:00')">
<div class="card__img">设备图片</div>
<div class="card__info">
<div class="card__name">多功能按摩椅</div>
<div class="card__meta">兴隆园健康小屋</div>
<div class="card__meta">2026-08-25 09:00</div>
</div>
<span class="card__arrow"></span>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
<script>
// 每条测量记录对应各自的测量报告
function goReport(dev, hut, time) {
var q = new URLSearchParams({ dev: dev, hut: hut, time: time });
window.location.href = 'hut-measure-report.html?' + q.toString();
}
</script>
</body>
</html>
@@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测评报告</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.head { background: linear-gradient(135deg, #1677ff, #3a94ff); border-radius: 12px; padding: 18px 16px; color: #fff; margin-bottom: 12px; }
.head__title { font-size: 17px; font-weight: 700; }
.head__meta { font-size: 12px; opacity: 0.9; margin-top: 6px; }
.sec { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.sec__title { font-size: 15px; font-weight: 700; color: #1a1a1a; margin-bottom: 12px; }
.info-row { display: flex; font-size: 13px; line-height: 2; }
.info-row__label { width: 84px; color: #999; flex-shrink: 0; }
.info-row__val { flex: 1; color: #333; }
.metric { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }
.metric:last-child { border-bottom: none; padding-bottom: 0; }
.metric__name { font-size: 13px; color: #666; }
.metric__val { font-size: 14px; font-weight: 600; color: #1a1a1a; display: flex; align-items: center; gap: 8px; }
.tag { font-size: 11px; padding: 2px 8px; border-radius: 8px; font-weight: 400; }
.tag--ok { background: #e6f9ef; color: #52c41a; }
.tag--warn { background: #fff7e6; color: #fa8c16; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="hut-measure-list.html"></a><span class="nav-title">测评报告</span></div>
<div class="scroll-content">
<div class="head">
<div class="head__title" id="headTitle">测评报告</div>
<div class="head__meta" id="headMeta"></div>
</div>
<div class="sec">
<div class="sec__title">本次测量信息</div>
<div class="info-row"><span class="info-row__label">设备名称</span><span class="info-row__val" id="fDev"></span></div>
<div class="info-row"><span class="info-row__label">健康小屋</span><span class="info-row__val" id="fHut"></span></div>
<div class="info-row"><span class="info-row__label">测量时间</span><span class="info-row__val" id="fTime"></span></div>
<div class="info-row"><span class="info-row__label">使用时长</span><span class="info-row__val" id="fDuration"></span></div>
</div>
<div class="sec">
<div class="sec__title">监测数据</div>
<div id="metricList"></div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
<script>
// 各设备的最终监测数据
var REPORTS = {
'多功能按摩椅': {
title: '按摩理疗测评报告', duration: '30 分钟',
metrics: [
{ name: '疲劳缓解度', val: '82 分', tag: '良好', ok: true },
{ name: '肌肉紧张度', val: '轻度(中→轻)', tag: '良好', ok: true },
{ name: '平均心率', val: '78 次/分', tag: '正常', ok: true }
]
},
'中频治疗仪': {
title: '中频治疗测评报告', duration: '20 分钟',
metrics: [
{ name: '治疗部位', val: '腰部', tag: '—', ok: true },
{ name: '治疗强度', val: '18 mA', tag: '正常', ok: true },
{ name: '疼痛评分(VAS', val: '5 分 → 3 分', tag: '好转', ok: true }
]
},
'经络深睡眠舱': {
title: '深睡眠测评报告', duration: '40 分钟',
metrics: [
{ name: '睡眠质量评分', val: '86 分', tag: '良好', ok: true },
{ name: '入睡时长', val: '12 分钟', tag: '正常', ok: true },
{ name: '心率变异性(HRV', val: '58 ms', tag: '正常', ok: true },
{ name: '呼吸率', val: '15 次/分', tag: '正常', ok: true }
]
},
'红外热治疗仪': {
title: '红外理疗测评报告', duration: '25 分钟',
metrics: [
{ name: '治疗部位', val: '肩部', tag: '—', ok: true },
{ name: '体表温升', val: '+2.3 ℃', tag: '正常', ok: true },
{ name: '局部血液循环', val: '改善明显', tag: '良好', ok: true },
{ name: '皮肤状态', val: '轻微发红', tag: '关注', ok: false }
]
},
'多功能理疗床': {
title: '脊柱理疗测评报告', duration: '30 分钟',
metrics: [
{ name: '脊柱压力分布', val: '改善', tag: '良好', ok: true },
{ name: '腰肌紧张度', val: '轻度', tag: '正常', ok: true },
{ name: '体态评分', val: '76 分', tag: '良好', ok: true },
{ name: '舒适度评分', val: '90 分', tag: '良好', ok: true }
]
},
'沉浸式冥想训练': {
title: '冥想放松测评报告', duration: '30 分钟',
metrics: [
{ name: '压力指数', val: '68 → 42', tag: '明显改善', ok: true },
{ name: '专注度评分', val: '81 分', tag: '良好', ok: true },
{ name: '放松度评分', val: '88 分', tag: '良好', ok: true },
{ name: '呼吸训练完成度', val: '95 %', tag: '良好', ok: true }
]
}
};
// 未知设备的兜底数据
var FALLBACK = {
title: '健康测评报告', duration: '30 分钟',
metrics: [{ name: '综合评分', val: '85 分', tag: '良好', ok: true }]
};
var q = new URLSearchParams(location.search);
var dev = q.get('dev') || '多功能按摩椅';
var hut = q.get('hut') || '樊学站点健康小屋';
var time = q.get('time') || '2026-09-08 14:30';
var r = REPORTS[dev] || FALLBACK;
document.title = r.title;
document.getElementById('headTitle').textContent = r.title;
document.getElementById('headMeta').textContent = '岳春生 · 测量时间 ' + time + ' · ' + hut;
document.getElementById('fDev').textContent = dev;
document.getElementById('fHut').textContent = hut;
document.getElementById('fTime').textContent = time;
document.getElementById('fDuration').textContent = r.duration;
// 最终监测数据
document.getElementById('metricList').innerHTML = r.metrics.map(function (m) {
return '<div class="metric"><span class="metric__name">' + m.name + '</span>' +
'<span class="metric__val">' + m.val + '<span class="tag ' + (m.ok ? 'tag--ok' : 'tag--warn') + '">' + m.tag + '</span></span></div>';
}).join('');
</script>
</body>
</html>
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的电子码</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.qrcode-card { background: #fff; border-radius: 14px; padding: 24px 20px; display: flex; flex-direction: column; align-items: center; box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 12px; }
.qrcode-card__title { font-size: 16px; font-weight: 700; color: #1a1a1a; }
.qrcode-card__hint { font-size: 12px; color: #999; margin: 6px 0 18px; }
.qr-box { width: 180px; height: 180px; padding: 10px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: grid; grid-template-columns: repeat(21, 1fr); grid-template-rows: repeat(21, 1fr); gap: 1px; }
.qr-cell { background: #fff; }
.qr-cell--dark { background: #1a1a1a; }
.qrcode-card__expire { font-size: 12px; color: #fa8c16; margin-top: 14px; }
.info-card { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.row { display: flex; font-size: 13px; line-height: 1.7; margin-bottom: 8px; }
.row:last-child { margin-bottom: 0; }
.row__label { width: 84px; color: #999; flex-shrink: 0; }
.row__value { flex: 1; color: #333; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">我的电子码</span></div>
<div class="scroll-content">
<div class="qrcode-card">
<div class="qrcode-card__title">个人身份二维码</div>
<div class="qrcode-card__hint">请向医务人员出示,扫码核验身份</div>
<div class="qr-box" id="qrBox"></div>
<div class="qrcode-card__expire">本码仅用于身份核验,请勿转借他人</div>
</div>
<div class="info-card">
<div class="row"><span class="row__label">姓名</span><span class="row__value">张旭</span></div>
<div class="row"><span class="row__label">员工编号</span><span class="row__value">56999852</span></div>
<div class="row"><span class="row__label">单位</span><span class="row__value">公司机关</span></div>
<div class="row"><span class="row__label">部门</span><span class="row__value">健康科</span></div>
<div class="row"><span class="row__label">手机号</span><span class="row__value">18544555589</span></div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
<script>
// 生成 21×21 伪二维码(含三个定位角框,中间按员工编号作 seed 确定性填充)
(function () {
var N = 21, seed = 56999852;
function rand() { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }
var box = document.getElementById('qrBox');
// 定位角:左上、右上、左下
function finder(r, c) {
if (r < 7 && c < 7) return true;
if (r < 7 && c >= N - 7) return true;
if (r >= N - 7 && c < 7) return true;
return false;
}
function finderDark(r, c) {
var local = function (rr, cc) {
var outer = rr === 0 || rr === 6 || cc === 0 || cc === 6;
var inner = rr >= 2 && rr <= 4 && cc >= 2 && cc <= 4;
return outer || inner;
};
if (r < 7 && c < 7) return local(r, c);
if (r < 7 && c >= N - 7) return local(r, c - (N - 7));
if (r >= N - 7 && c < 7) return local(r - (N - 7), c);
return false;
}
for (var r = 0; r < N; r++) {
for (var c = 0; c < N; c++) {
var cell = document.createElement('div');
cell.className = 'qr-cell';
if (finder(r, c)) {
if (finderDark(r, c)) cell.classList.add('qr-cell--dark');
} else if (rand() > 0.5) {
cell.classList.add('qr-cell--dark');
}
box.appendChild(cell);
}
}
})();
</script>
</body>
</html>
+153
View File
@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一线医疗</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; }
.scroll-content::-webkit-scrollbar { display: none; }
/* 我的医疗点卡片 */
.my-point { margin: 16px; background: linear-gradient(135deg, #1677ff, #3a94ff); border-radius: 14px; padding: 18px 16px; color: #fff; cursor: pointer; box-shadow: 0 4px 12px rgba(22,119,255,0.25); }
.my-point__label { font-size: 12px; opacity: 0.85; margin-bottom: 8px; }
.my-point__name { font-size: 19px; font-weight: 700; margin-bottom: 8px; }
.my-point__meta { font-size: 12px; opacity: 0.9; line-height: 1.8; }
.my-point__foot { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
.my-point__tag { font-size: 11px; background: rgba(255,255,255,0.22); border-radius: 10px; padding: 3px 10px; }
.my-point__go { font-size: 13px; font-weight: 500; }
/* 分组 */
.section-title { font-size: 16px; font-weight: 700; color: #1a1a1a; padding: 12px 16px 10px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 0 16px; }
.func-card { background: #fff; border-radius: 12px; padding: 18px 14px; display: flex; flex-direction: column; align-items: center; gap: 10px; cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.06); transition: transform 0.15s; }
.func-card:active { transform: scale(0.97); }
.func-card__icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.func-card__label { font-size: 14px; font-weight: 500; color: #1a1a1a; }
.func-card__desc { font-size: 11px; color: #999; text-align: center; line-height: 1.4; }
.ic-blue { background: #e6f2ff; } .ic-green { background: #e6f9ef; } .ic-orange { background: #fff3e6; } .ic-purple { background: #f0edff; }
/* 健康小屋渐变入口卡片 */
.hut-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 0 16px; }
.hut-card { position: relative; border-radius: 14px; padding: 16px; color: #fff; cursor: pointer; overflow: hidden; display: flex; flex-direction: column; transition: transform 0.15s; }
.hut-card:active { transform: scale(0.97); }
.hut-card__icon { width: 34px; height: 34px; border: 1.5px solid rgba(255,255,255,0.85); border-radius: 9px; display: flex; align-items: center; justify-content: center; margin-bottom: 14px; }
.hut-card__title { font-size: 16px; font-weight: 700; margin-bottom: 8px; }
.hut-card__desc { font-size: 11px; line-height: 1.6; opacity: 0.95; }
.hut-card__deco { position: absolute; right: -10px; bottom: -10px; opacity: 0.16; pointer-events: none; }
.hut-card--green { background: linear-gradient(135deg, #3ede80, #0eb95e); }
.hut-card--blue { background: linear-gradient(135deg, #30c8f2, #1f8fe0); }
.hut-card--red { background: linear-gradient(135deg, #ff8a85, #f2565e); }
.hut-card--purple { background: linear-gradient(135deg, #9aa6f5, #7a72e6); }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar">
<span class="time">12:00</span>
<div class="icons">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg>
</div>
</div>
<div class="nav-bar">
<span class="nav-back" onclick="window.location.href='../home/index.html'"></span>
<span class="nav-title">一线医疗</span>
</div>
<div class="scroll-content">
<!-- 我的医疗点 -->
<div class="my-point" onclick="window.location.href='medical-point.html'">
<div class="my-point__label">我所分配的医疗点</div>
<div class="my-point__name">吴堡作业区医疗点</div>
<div class="my-point__meta">
<div>所属医院:CQ能源职工医院</div>
<div>联系电话:020-88556985</div>
</div>
<div class="my-point__foot">
<span class="my-point__tag">医护 3 人</span>
<span class="my-point__go">查看详情 </span>
</div>
</div>
<!-- 一线医疗 -->
<div class="section-title">一线医疗</div>
<div class="grid">
<div class="func-card" onclick="window.location.href='visit-list.html'">
<div class="func-card__icon ic-blue">📋</div>
<div class="func-card__label">就诊查看</div>
<div class="func-card__desc">就诊记录与详情</div>
</div>
<div class="func-card" onclick="window.location.href='follow-list.html'">
<div class="func-card__icon ic-green">🩺</div>
<div class="func-card__label">随访记录</div>
<div class="func-card__desc">历次随访记录</div>
</div>
<div class="func-card" onclick="window.location.href='medication-plan.html'">
<div class="func-card__icon ic-orange">💊</div>
<div class="func-card__label">慢病管理</div>
<div class="func-card__desc">慢病类型与用药方案</div>
</div>
<div class="func-card" onclick="window.location.href='nearby-list.html'">
<div class="func-card__icon ic-purple">📍</div>
<div class="func-card__label">附近医疗点</div>
<div class="func-card__desc">附近医疗点列表</div>
</div>
</div>
<!-- 健康小屋 -->
<div class="section-title">健康小屋</div>
<div class="hut-grid">
<div class="hut-card hut-card--green" onclick="window.location.href='hut-list.html'">
<div class="hut-card__icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>
</div>
<div class="hut-card__title">在线预约</div>
<div class="hut-card__desc">自主选择使用日期、时段和设备</div>
<svg class="hut-card__deco" width="84" height="84" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>
</div>
<div class="hut-card hut-card--blue" onclick="window.location.href='hut-qrcode.html'">
<div class="hut-card__icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM21 14v3M14 21h3M21 21h.01"/></svg>
</div>
<div class="hut-card__title">我的电子码</div>
<div class="hut-card__desc">预约身份证明,查看预约二维码</div>
<svg class="hut-card__deco" width="84" height="84" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM21 14v3M14 21h3M21 21h.01"/></svg>
</div>
<div class="hut-card hut-card--red" onclick="window.location.href='hut-booking-all.html'">
<div class="hut-card__icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4M12 13v3l2 1.2"/></svg>
</div>
<div class="hut-card__title">我的预约</div>
<div class="hut-card__desc">查看自己所有预约的项目及详细信息</div>
<svg class="hut-card__deco" width="84" height="84" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4M12 13v3l2 1.2"/></svg>
</div>
<div class="hut-card hut-card--purple" onclick="window.location.href='hut-measure-list.html'">
<div class="hut-card__icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="9" r="3.5"/><path d="M5 20c1.2-3.2 3.8-5 7-5s5.8 1.8 7 5"/></svg>
</div>
<div class="hut-card__title">测量记录</div>
<div class="hut-card__desc">查看历史测量记录,关注自身健康</div>
<svg class="hut-card__deco" width="84" height="84" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="9" r="3.5"/><path d="M5 20c1.2-3.2 3.8-5 7-5s5.8 1.8 7 5"/></svg>
</div>
</div>
<div style="height:16px"></div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>医护人员</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.avatar { width: 48px; height: 48px; border-radius: 50%; background: #e6f2ff; color: #1677ff; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 600; flex-shrink: 0; }
.card__info { flex: 1; }
.card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__title { font-size: 13px; color: #666; margin-top: 2px; }
.card__phone { font-size: 12px; color: #999; margin-top: 4px; }
.btn-call { width: 32px; height: 32px; border-radius: 50%; border: none; background: #1677ff; color: #fff; font-size: 15px; cursor: pointer; flex-shrink: 0; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="medical-point.html"></a><span class="nav-title">医护人员</span></div>
<div class="scroll-content">
<div class="card">
<div class="avatar"></div>
<div class="card__info">
<div class="card__name">石维国</div>
<div class="card__title">主治医师</div>
<div class="card__phone">13956282214</div>
</div>
<button class="btn-call">📞</button>
</div>
<div class="card">
<div class="avatar"></div>
<div class="card__info">
<div class="card__name">李静</div>
<div class="card__title">护师</div>
<div class="card__phone">138****2291</div>
</div>
<button class="btn-call">📞</button>
</div>
<div class="card">
<div class="avatar"></div>
<div class="card__info">
<div class="card__name">王振华</div>
<div class="card__title">执业医师</div>
<div class="card__phone">137****8834</div>
</div>
<button class="btn-call">📞</button>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分配医疗点</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; }
.scroll-content::-webkit-scrollbar { display: none; }
.hero-img { width: 100%; height: 170px; background: linear-gradient(135deg, #e8f4ff, #d6e8ff); display: flex; align-items: center; justify-content: center; color: #999; font-size: 14px; }
.info-section { background: #fff; padding: 16px; margin-bottom: 10px; }
.detail-name { font-size: 18px; font-weight: 700; color: #1a1a1a; }
.info-row { font-size: 14px; color: #666; margin-top: 10px; line-height: 1.6; }
.info-row label { color: #999; margin-right: 4px; }
.action-btns { display: flex; gap: 12px; margin-top: 16px; }
.action-btns .btn { flex: 1; height: 40px; border-radius: 20px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; }
.btn-phone { background: #1677ff; color: #fff; }
.btn-nav { background: #fff; color: #1677ff; border: 1px solid #1677ff !important; }
.section-title { font-size: 16px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; }
.intro-text { font-size: 14px; color: #666; line-height: 1.8; }
.staff-card { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid #f0f0f0; }
.staff-card:last-child { border-bottom: none; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background: #e6f2ff; color: #1677ff; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 600; flex-shrink: 0; }
.staff-card__info { flex: 1; }
.staff-card__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.staff-card__title { font-size: 12px; color: #999; margin-top: 2px; }
.role { font-size: 11px; padding: 2px 10px; border-radius: 10px; font-weight: 500; flex-shrink: 0; }
.role--doctor { background: #e6f2ff; color: #1677ff; }
.role--nurse { background: #e6f9ef; color: #52c41a; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">分配医疗点</span></div>
<div class="scroll-content">
<div class="hero-img">医疗点图片</div>
<div class="info-section">
<div class="detail-name">吴堡作业区医疗点</div>
<div class="info-row"><label>所属医院:</label>CQ能源职工医院</div>
<div class="info-row"><label>所属单位:</label>第一采油厂</div>
<div class="info-row"><label>负责人:</label>石维国</div>
<div class="info-row"><label>联系电话:</label>020-88556985</div>
<div class="info-row"><label>地址:</label>西安市未央区凤城五路与开元路交叉口CQ兴隆园小区</div>
<div class="info-row"><label>辐射范围:</label>吴堡作业区及周边站点</div>
<div class="action-btns">
<button class="btn btn-phone">📞 电话</button>
<button class="btn btn-nav">📍 导航</button>
</div>
</div>
<div class="info-section">
<div class="section-title">医疗点简介</div>
<div class="intro-text">吴堡作业区医疗点隶属于CQ能源职工医院,为一线员工提供日常诊疗、慢病随访、健康宣教等基础医疗服务,配备急救药品与常用监测设备。</div>
</div>
<div class="info-section">
<div class="section-title">医护人员</div>
<div class="staff-card">
<div class="avatar"></div>
<div class="staff-card__info"><div class="staff-card__name">石维国</div><div class="staff-card__title">主治医师</div></div>
<span class="role role--doctor">医生</span>
</div>
<div class="staff-card">
<div class="avatar"></div>
<div class="staff-card__info"><div class="staff-card__name">李静</div><div class="staff-card__title">护师</div></div>
<span class="role role--nurse">护士</span>
</div>
<div class="staff-card">
<div class="avatar"></div>
<div class="staff-card__info"><div class="staff-card__name">王振华</div><div class="staff-card__title">执业医师</div></div>
<span class="role role--doctor">医生</span>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>慢病管理</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.sec { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.sec--highlight { border: 1px solid #b7eb8f; background: #f6ffed; }
.sec__title { font-size: 15px; font-weight: 700; color: #1a1a1a; border-left: 3px solid #1677ff; padding-left: 8px; margin-bottom: 12px; }
.sec--highlight .sec__title { border-left-color: #52c41a; }
.tag { display: inline-block; padding: 4px 14px; border-radius: 6px; font-size: 13px; margin-right: 10px; font-weight: 500; }
.tag--ht { background: rgba(24,144,255,.12); color: #1890ff; }
.tag--dm { background: rgba(42,193,139,.12); color: #2ac18b; }
.med-card { background: #fff; border: 1px solid #e6f4ff; border-radius: 10px; padding: 12px 14px; font-size: 13px; color: #4a5a7f; line-height: 1.9; margin-bottom: 10px; }
.med-card:last-child { margin-bottom: 0; }
.med-card__name { color: #333; font-size: 14px; font-weight: 600; margin-bottom: 2px; }
.med-card__end { color: #52c41a; }
.med-card__end--over { color: #969eb0; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">慢病管理</span></div>
<div class="scroll-content">
<div class="sec">
<div class="sec__title">我的慢病</div>
<div>
<span class="tag tag--ht">高血压</span>
<span class="tag tag--dm">糖尿病</span>
</div>
</div>
<div class="sec sec--highlight">
<div class="sec__title">用药方案</div>
<div class="med-card">
<div class="med-card__name">苯磺酸氨氯地平片</div>
<div>规格:5mg×7片</div>
<div>用法用量:口服,一日1次,一次1片</div>
<div>服用开始日期:2026-06-01</div>
<div>服用结束日期:<span class="med-card__end">长期</span></div>
</div>
<div class="med-card">
<div class="med-card__name">二甲双胍缓释片</div>
<div>规格:0.5g×30片</div>
<div>用法用量:口服,一日2次,一次1片</div>
<div>服用开始日期:2026-03-15</div>
<div>服用结束日期:<span class="med-card__end">长期</span></div>
</div>
<div class="med-card">
<div class="med-card__name">硝苯地平控释片</div>
<div>规格:30mg×7片</div>
<div>用法用量:口服,一日1次,一次1片</div>
<div>服用开始日期:2024-09-01</div>
<div>服用结束日期:<span class="med-card__end--over">2025-08-31</span></div>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>医疗点详情</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; }
.scroll-content::-webkit-scrollbar { display: none; }
.hero-img { width: 100%; height: 170px; background: linear-gradient(135deg, #e8f4ff, #d6e8ff); display: flex; align-items: center; justify-content: center; color: #999; font-size: 14px; }
.info-section { background: #fff; padding: 16px; margin-bottom: 10px; }
.detail-name { font-size: 18px; font-weight: 700; color: #1a1a1a; }
.info-row { font-size: 14px; color: #666; margin-top: 10px; line-height: 1.6; }
.info-row label { color: #999; margin-right: 4px; }
.action-btns { display: flex; gap: 12px; margin-top: 16px; }
.action-btns .btn { flex: 1; height: 40px; border-radius: 20px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; }
.btn-phone { background: #1677ff; color: #fff; }
.btn-nav { background: #fff; color: #1677ff; border: 1px solid #1677ff !important; }
.section-title { font-size: 16px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; }
.intro-text { font-size: 14px; color: #666; line-height: 1.8; }
.staff-card { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid #f0f0f0; }
.staff-card:last-child { border-bottom: none; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background: #e6f2ff; color: #1677ff; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 600; flex-shrink: 0; }
.staff-card__info { flex: 1; }
.staff-card__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
.staff-card__title { font-size: 12px; color: #999; margin-top: 2px; }
.role { font-size: 11px; padding: 2px 10px; border-radius: 10px; font-weight: 500; flex-shrink: 0; }
.role--doctor { background: #e6f2ff; color: #1677ff; }
.role--nurse { background: #e6f9ef; color: #52c41a; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="nearby-list.html"></a><span class="nav-title">医疗点详情</span></div>
<div class="scroll-content">
<div class="hero-img">医疗点图片</div>
<div class="info-section">
<div class="detail-name">吴堡作业区医疗点</div>
<div class="info-row"><label>所属医院:</label>CQ能源职工医院</div>
<div class="info-row"><label>所属单位:</label>第一采油厂</div>
<div class="info-row"><label>联系电话:</label>020-88556985</div>
<div class="info-row"><label>地址:</label>西安市未央区凤城五路与开元路交叉口CQ兴隆园小区</div>
<div class="action-btns"><button class="btn btn-phone">📞 电话</button><button class="btn btn-nav">📍 导航</button></div>
</div>
<div class="info-section">
<div class="section-title">医疗点简介</div>
<div class="intro-text">吴堡作业区医疗点隶属于CQ能源职工医院,为一线员工提供日常诊疗、慢病随访、健康宣教等基础医疗服务,配备急救药品与常用监测设备。</div>
</div>
<div class="info-section">
<div class="section-title">医护人员</div>
<div class="staff-card">
<div class="avatar"></div>
<div class="staff-card__info"><div class="staff-card__name">石维国</div><div class="staff-card__title">主治医师</div></div>
<span class="role role--doctor">医生</span>
</div>
<div class="staff-card">
<div class="avatar"></div>
<div class="staff-card__info"><div class="staff-card__name">李静</div><div class="staff-card__title">护师</div></div>
<span class="role role--nurse">护士</span>
</div>
<div class="staff-card">
<div class="avatar"></div>
<div class="staff-card__info"><div class="staff-card__name">王振华</div><div class="staff-card__title">执业医师</div></div>
<span class="role role--doctor">医生</span>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>附近医疗</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.card { background: #fff; border-radius: 12px; padding: 12px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; transition: transform 0.15s; display: flex; gap: 12px; }
.card:active { transform: scale(0.98); }
.card__img { width: 84px; height: 84px; border-radius: 8px; background: linear-gradient(135deg, #e8f4ff, #d6e8ff); display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px; flex-shrink: 0; }
.card__body { flex: 1; min-width: 0; }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__dist { font-size: 13px; color: #1677ff; font-weight: 600; flex-shrink: 0; margin-left: 6px; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">附近医疗</span></div>
<div class="scroll-content">
<div class="card" onclick="window.location.href='nearby-detail.html'">
<div class="card__img">医疗点图片</div>
<div class="card__body">
<div class="card__head">
<span class="card__name">吴堡作业区医疗点</span>
<span class="card__dist">1.2 km</span>
</div>
<div class="card__info">
<div><b>医护人员:</b>3 人</div>
<div>📍 西安市未央区凤城五路与开元路交叉口</div>
</div>
</div>
</div>
<div class="card" onclick="window.location.href='nearby-detail.html'">
<div class="card__img">医疗点图片</div>
<div class="card__body">
<div class="card__head">
<span class="card__name">樊学站点健康小屋</span>
<span class="card__dist">3.5 km</span>
</div>
<div class="card__info">
<div><b>医护人员:</b>2 人</div>
<div>📍 定边县樊学镇樊学站点院内</div>
</div>
</div>
</div>
<div class="card" onclick="window.location.href='nearby-detail.html'">
<div class="card__img">医疗点图片</div>
<div class="card__body">
<div class="card__head">
<span class="card__name">定边采油厂医疗点</span>
<span class="card__dist">8.0 km</span>
</div>
<div class="card__info">
<div><b>医护人员:</b>4 人</div>
<div>📍 定边县定边镇新区采油厂家属区</div>
</div>
</div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>就诊详情</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.sec { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
.sec__title { font-size: 15px; font-weight: 700; color: #1a1a1a; border-left: 3px solid #1677ff; padding-left: 8px; margin-bottom: 12px; }
.namecard { display: flex; gap: 14px; align-items: center; }
.namecard__name { font-size: 17px; font-weight: 700; color: #1a1a1a; margin-bottom: 4px; }
.namecard__meta { font-size: 12px; color: #666; line-height: 1.7; }
.row { display: flex; font-size: 13px; line-height: 1.7; margin-bottom: 8px; }
.row__label { width: 74px; color: #999; flex-shrink: 0; }
.row__value { flex: 1; color: #333; }
.row--block { flex-direction: column; }
.row--block .row__value { margin-top: 2px; }
.med-card { background: #f2f6ff; border-radius: 10px; padding: 10px 14px; font-size: 12px; color: #4a5a7f; line-height: 1.9; margin-bottom: 10px; }
.med-card b { color: #333; font-size: 13px; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="visit-list.html"></a><span class="nav-title">就诊详情</span></div>
<div class="scroll-content">
<div class="sec">
<div class="namecard">
<div class="namecard__info">
<div class="namecard__name">岳春生</div>
<div class="namecard__meta">男 · 54岁 · 员工编号 1035678</div>
<div class="namecard__meta">联系电话:13956282214</div>
<div class="namecard__meta">单位部门:CQ能源陕E作业区 / 管理四中心</div>
</div>
</div>
</div>
<div class="sec">
<div class="sec__title">就诊信息</div>
<div class="row"><span class="row__label">就诊医生</span><span class="row__value">石维国</span></div>
<div class="row"><span class="row__label">就诊日期</span><span class="row__value">2026-09-04 10:32</span></div>
<div class="row"><span class="row__label">是否复诊</span><span class="row__value">复诊</span></div>
<div class="row row--block"><span class="row__label">主诉</span><span class="row__value">晨起后头晕、后颈部发紧,测血压 165/102mmHg</span></div>
<div class="row row--block"><span class="row__label">查体</span><span class="row__value">BP 162/100mmHg,心率 78 次/分,律齐,双肺呼吸音清,未闻及干湿啰音。</span></div>
<div class="row row--block"><span class="row__label">现病史</span><span class="row__value">近1周劳累后症状加重,自述降压药服用不规律。</span></div>
<div class="row row--block"><span class="row__label">既往史</span><span class="row__value">高血压8年,2型糖尿病4年,规律服药。</span></div>
<div class="row"><span class="row__label">过敏史</span><span class="row__value">青霉素过敏</span></div>
<div class="row row--block"><span class="row__label">诊断</span><span class="row__value" style="font-weight:600">原发性高血压3级(很高危);2型糖尿病</span></div>
</div>
<div class="sec">
<div class="sec__title">医药信息</div>
<div class="med-card"><b>苯磺酸氨氯地平片</b>5mg×7片 · 23.80元)<br>数量:2盒 用法用量:口服,一日1次,一次1片</div>
<div class="med-card"><b>硝苯地平控释片</b>30mg×7片 · 34.20元)<br>数量:1盒 用法用量:口服,一日1次,一次1片</div>
</div>
<div class="sec">
<div class="sec__title">生活建议</div>
<div class="row row--block"><span class="row__label">医嘱</span><span class="row__value">规律服药,每日早晚监测血压并记录,2周后复查。</span></div>
<div class="row row--block"><span class="row__label">运动处方</span><span class="row__value">每日快走30分钟,避免剧烈运动。</span></div>
<div class="row row--block"><span class="row__label">膳食处方</span><span class="row__value">低盐(&lt;5g/日)低脂饮食,控制主食量。</span></div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
</body>
</html>
@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>就诊记录</title>
<style>
* { 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; min-height: 844px; background: #fff; 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; }
.phone-status-bar { height: 44px; background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.phone-status-bar .time { font-size: 15px; font-weight: 700; }
.phone-status-bar .icons { display: flex; gap: 6px; align-items: center; }
.nav-bar { height: 44px; display: flex; align-items: center; padding: 0 16px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; position: relative; }
.nav-bar .nav-back { font-size: 14px; color: #1a1a1a; cursor: pointer; text-decoration: none; }
.nav-bar .nav-title { position: absolute; left: 50%; transform: translateX(-50%); font-size: 17px; font-weight: 600; color: #1a1a1a; }
.scroll-content { flex: 1; overflow-y: auto; background: #f5f5f5; padding: 12px 16px; }
.scroll-content::-webkit-scrollbar { display: none; }
.filter-bar { display: flex; align-items: center; gap: 10px; background: #fff; border-radius: 10px; padding: 12px 14px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; }
.filter-bar__label { font-size: 13px; color: #666; flex-shrink: 0; }
.filter-bar__value { flex: 1; font-size: 13px; color: #333; }
.filter-bar__arrow { font-size: 16px; color: #ccc; }
.card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); cursor: pointer; transition: transform 0.15s; }
.card:active { transform: scale(0.98); }
.card__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.card__time { font-size: 15px; font-weight: 600; color: #1a1a1a; }
.card__type { font-size: 11px; padding: 2px 10px; border-radius: 8px; font-weight: 500; }
.type-followup { background: #e6f2ff; color: #1677ff; }
.type-first { background: #e6f9ef; color: #52c41a; }
.card__info { font-size: 13px; color: #666; line-height: 1.8; }
.card__info b { color: #333; font-weight: 500; }
.empty { text-align: center; color: #999; padding: 60px 0; font-size: 14px; display: none; }
/* 底部弹窗 */
.overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.45); z-index: 60; display: none; align-items: flex-end; }
.overlay.open { display: flex; }
.sheet { width: 100%; background: #fff; border-radius: 20px 20px 0 0; padding: 20px 20px 28px; }
.sheet__title { font-size: 16px; font-weight: 700; color: #1a1a1a; margin-bottom: 16px; }
.sheet__label { font-size: 13px; color: #666; margin-bottom: 8px; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.chip-row--grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.chip { padding: 7px 14px; border-radius: 16px; border: 1px solid #e0e0e0; font-size: 13px; color: #333; cursor: pointer; text-align: center; }
.chip--active { border-color: #1677ff; color: #1677ff; background: #e6f2ff; }
.sheet__foot { display: flex; gap: 12px; margin-top: 20px; }
.sheet__foot button { flex: 1; height: 44px; border-radius: 22px; font-size: 15px; cursor: pointer; }
.btn-cancel { background: #f5f5f5; color: #666; border: none; }
.btn-confirm { background: #1677ff; color: #fff; border: none; }
.bottom-spacer { height: 34px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="phone-status-bar"><span class="time">12:00</span><div class="icons"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6l5 5 5-5"/><path d="M4 9l3 3 3-3" opacity="0.5"/></svg><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="6" width="18" height="12" rx="2"/><path d="M21 10v4"/></svg></div></div>
<div class="nav-bar"><a class="nav-back" href="index.html"></a><span class="nav-title">就诊记录</span></div>
<div class="scroll-content">
<!-- [交互] 点击打开年月联动选择弹窗 -->
<div class="filter-bar" onclick="openMonthSheet()">
<span class="filter-bar__label">就诊月份</span>
<span class="filter-bar__value" id="monthText">全部</span>
<span class="filter-bar__arrow"></span>
</div>
<div class="card" data-month="2026-09" onclick="window.location.href='visit-detail.html'">
<div class="card__head">
<span class="card__time">2026-09-04 10:32</span>
<span class="card__type type-followup">复诊</span>
</div>
<div class="card__info">
<div><b>就诊医疗点:</b>吴堡作业区医疗点</div>
<div><b>就诊医生:</b>石维国</div>
<div><b>诊断:</b>原发性高血压3级(很高危);2型糖尿病</div>
</div>
</div>
<div class="card" data-month="2026-08" onclick="window.location.href='visit-detail.html'">
<div class="card__head">
<span class="card__time">2026-08-12 09:15</span>
<span class="card__type type-first">初诊</span>
</div>
<div class="card__info">
<div><b>就诊医疗点:</b>吴堡作业区医疗点</div>
<div><b>就诊医生:</b>石维国</div>
<div><b>诊断:</b>高血压2级(高危)</div>
</div>
</div>
<div class="card" data-month="2026-07" onclick="window.location.href='visit-detail.html'">
<div class="card__head">
<span class="card__time">2026-07-20 14:00</span>
<span class="card__type type-first">初诊</span>
</div>
<div class="card__info">
<div><b>就诊医疗点:</b>吴堡作业区医疗点</div>
<div><b>就诊医生:</b>王振华</div>
<div><b>诊断:</b>2型糖尿病</div>
</div>
</div>
<div class="card" data-month="2026-06" onclick="window.location.href='visit-detail.html'">
<div class="card__head">
<span class="card__time">2026-06-18 08:50</span>
<span class="card__type type-first">初诊</span>
</div>
<div class="card__info">
<div><b>就诊医疗点:</b>吴堡作业区医疗点</div>
<div><b>就诊医生:</b>李静</div>
<div><b>诊断:</b>高血压1级</div>
</div>
</div>
<div class="card" data-month="2025-11" onclick="window.location.href='visit-detail.html'">
<div class="card__head">
<span class="card__time">2025-11-10 09:40</span>
<span class="card__type type-followup">复诊</span>
</div>
<div class="card__info">
<div><b>就诊医疗点:</b>吴堡作业区医疗点</div>
<div><b>就诊医生:</b>石维国</div>
<div><b>诊断:</b>高血压1级</div>
</div>
</div>
<div class="empty" id="empty">该月份暂无就诊记录</div>
</div>
<div class="bottom-spacer"></div>
<!-- 年月联动选择弹窗 -->
<div class="overlay" id="monthSheet">
<div class="sheet">
<div class="sheet__title">选择就诊月份</div>
<div class="sheet__label">年份</div>
<div class="chip-row" id="yearChips">
<span class="chip chip--active" data-year="all">全部</span>
<span class="chip" data-year="2026">2026年</span>
<span class="chip" data-year="2025">2025年</span>
</div>
<div class="sheet__label">月份</div>
<div class="chip-row chip-row--grid" id="monthChips">
<span class="chip" data-month="01">1月</span>
<span class="chip" data-month="02">2月</span>
<span class="chip" data-month="03">3月</span>
<span class="chip" data-month="04">4月</span>
<span class="chip" data-month="05">5月</span>
<span class="chip" data-month="06">6月</span>
<span class="chip" data-month="07">7月</span>
<span class="chip" data-month="08">8月</span>
<span class="chip" data-month="09">9月</span>
<span class="chip" data-month="10">10月</span>
<span class="chip" data-month="11">11月</span>
<span class="chip" data-month="12">12月</span>
</div>
<div class="sheet__foot">
<button class="btn-cancel" onclick="resetMonth()">重置</button>
<button class="btn-confirm" onclick="applyMonth()">确定</button>
</div>
</div>
</div>
</div>
<script>
var currentYear = 'all';
var currentMonth = null;
function openMonthSheet() {
// 回显当前选中状态
document.querySelectorAll('#yearChips .chip').forEach(function (c) {
c.classList.toggle('chip--active', c.getAttribute('data-year') === currentYear);
});
document.querySelectorAll('#monthChips .chip').forEach(function (c) {
c.classList.toggle('chip--active', c.getAttribute('data-month') === currentMonth);
});
document.getElementById('monthSheet').classList.add('open');
}
// 年份联动:选中「全部」时清空月份;选中具体年份时保留月份选择
document.querySelectorAll('#yearChips .chip').forEach(function (chip) {
chip.addEventListener('click', function () {
document.querySelectorAll('#yearChips .chip').forEach(function (c) { c.classList.remove('chip--active'); });
chip.classList.add('chip--active');
currentYear = chip.getAttribute('data-year');
if (currentYear === 'all') {
currentMonth = null;
document.querySelectorAll('#monthChips .chip').forEach(function (c) { c.classList.remove('chip--active'); });
}
});
});
// 月份选择
document.querySelectorAll('#monthChips .chip').forEach(function (chip) {
chip.addEventListener('click', function () {
document.querySelectorAll('#monthChips .chip').forEach(function (c) { c.classList.remove('chip--active'); });
chip.classList.add('chip--active');
currentMonth = chip.getAttribute('data-month');
});
});
function resetMonth() {
currentYear = 'all';
currentMonth = null;
document.querySelectorAll('#yearChips .chip').forEach(function (c) {
c.classList.toggle('chip--active', c.getAttribute('data-year') === 'all');
});
document.querySelectorAll('#monthChips .chip').forEach(function (c) { c.classList.remove('chip--active'); });
document.getElementById('monthText').textContent = '全部';
document.getElementById('monthSheet').classList.remove('open');
applyFilter();
}
function applyMonth() {
var text = '全部';
if (currentYear !== 'all') {
text = currentYear + '年';
if (currentMonth) { text += parseInt(currentMonth, 10) + '月'; }
}
document.getElementById('monthText').textContent = text;
document.getElementById('monthSheet').classList.remove('open');
applyFilter();
}
function applyFilter() {
var cards = document.querySelectorAll('.card');
var count = 0;
cards.forEach(function (c) {
var dm = c.getAttribute('data-month');
var show;
if (currentYear === 'all') {
show = true;
} else if (currentMonth) {
show = dm === (currentYear + '-' + currentMonth);
} else {
show = dm.indexOf(currentYear + '-') === 0;
}
c.style.display = show ? 'block' : 'none';
if (show) count++;
});
document.getElementById('empty').style.display = count === 0 ? 'block' : 'none';
}
</script>
</body>
</html>
@@ -68,6 +68,78 @@
.stepper__item--done:hover .stepper__label { text-decoration: underline; text-underline-offset: 3px; }
.stepper__line { width: 44px; height: 3px; background: #e8e8e8; margin: 0 8px; margin-bottom: 22px; border-radius: 2px; }
.stepper__line--done { background: var(--primary); }
/* 原型演示状态下拉框(导航栏右上角) */
.demo-select {
margin-left: auto; flex-shrink: 0; font-family: inherit;
font-size: 11px; font-weight: 600; color: var(--text-secondary);
border: 1px solid #e8e8e8; border-radius: 12px; padding: 3px 6px;
background: #f5f5f5; cursor: pointer; outline: none;
}
/* 预约结果横幅 */
.result-banner {
display: flex; align-items: flex-start; gap: 12px;
border-radius: 20px; padding: 16px; margin-bottom: 14px;
}
.result-banner--success { background: linear-gradient(135deg, #f6ffed 0%, #ffffff 100%); border: 1px solid #b7eb8f; }
.result-banner--fail { background: linear-gradient(135deg, #fff1f0 0%, #ffffff 100%); border: 1px solid #ffccc7; }
.result-banner__icon {
width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.result-banner--success .result-banner__icon { background: #52c41a; }
.result-banner--fail .result-banner__icon { background: #ff4d4f; }
.result-banner--success .icon-cross { display: none; }
.result-banner--fail .icon-check { display: none; }
.result-banner__title { font-size: 18px; font-weight: 800; margin-bottom: 4px; }
.result-banner--success .result-banner__title { color: #389e0d; }
.result-banner--fail .result-banner__title { color: #cf1322; }
.result-banner__sub { font-size: 12px; color: var(--text-muted); line-height: 1.5; }
.result-banner__reason {
display: none; align-items: center; gap: 6px; margin-top: 6px;
font-size: 13px; color: var(--text-secondary);
}
.result-banner--fail .result-banner__reason { display: flex; }
.reason-help {
width: 16px; height: 16px; border-radius: 50%; border: 1px solid #ff4d4f; color: #ff4d4f;
display: inline-flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; cursor: pointer; flex-shrink: 0; transition: all 0.2s;
}
.reason-help:hover { background: #ff4d4f; color: #fff; }
/* 失败原因详情弹窗 */
.modal-overlay {
position: absolute; inset: 0; background: rgba(0,0,0,0.42); z-index: 120;
display: flex; align-items: center; justify-content: center; padding: 24px;
}
.modal-overlay.hidden { display: none; }
.reason-modal {
width: 100%; background: #fff; border-radius: 24px; padding: 22px 20px 18px;
box-shadow: 0 20px 50px rgba(0,0,0,0.16);
}
.reason-modal__title { font-size: 17px; font-weight: 700; text-align: center; margin-bottom: 16px; }
.reason-modal__item { margin-bottom: 14px; }
.reason-modal__label { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }
.reason-modal__text { font-size: 14px; font-weight: 600; color: var(--text-primary); line-height: 1.6; }
.reason-modal__text--danger { color: #cf1322; }
.reason-modal__btn {
width: 100%; height: 44px; border-radius: 22px; border: none; cursor: pointer;
background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 100%); color: #fff;
font-size: 15px; font-weight: 600; margin-top: 4px;
}
/* 取消预约确认弹窗 */
.cancel-modal {
width: 100%; background: #fff; border-radius: 24px; padding: 22px 20px 18px;
box-shadow: 0 20px 50px rgba(0,0,0,0.16);
}
.cancel-modal__title { font-size: 17px; font-weight: 700; text-align: center; margin-bottom: 6px; }
.cancel-modal__desc { font-size: 12px; color: var(--text-muted); text-align: center; line-height: 1.6; margin-bottom: 14px; }
.cancel-modal__order {
background: #f7f9fb; border-radius: 12px; padding: 10px 14px; margin-bottom: 16px;
font-size: 13px; font-weight: 600; color: var(--text-secondary); text-align: center;
}
.cancel-modal__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
.cancel-modal__btn { height: 44px; border-radius: 22px; font-size: 15px; font-weight: 600; border: none; cursor: pointer; }
.cancel-modal__btn--ghost { background: #f5f5f5; color: var(--text-secondary); }
.cancel-modal__btn--danger { background: var(--danger); color: #fff; box-shadow: 0 6px 16px rgba(255,77,79,0.28); }
.section-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; }
/* 信息卡片 */
.info-card {
@@ -135,6 +207,11 @@
<div class="nav-bar">
<a class="nav-back" href="index.html">&#8249;</a>
<span class="nav-title">体检预约</span>
<!-- [交互] 原型演示:切换预约成功/失败两态 -->
<select class="demo-select" id="demoSelect" title="原型演示:切换预约结果状态" onchange="setResult(this.value)">
<option value="success">预约成功</option>
<option value="fail">预约失败</option>
</select>
</div>
<div class="scroll-content">
<div class="page-body">
@@ -148,7 +225,23 @@
<div class="stepper__item stepper__item--active"><div class="stepper__dot">&#10003;</div><div class="stepper__label">预约日期</div></div>
</div>
<!-- 预约成功信息 -->
<!-- [交互] 预约结果横幅:成功/失败两态,由右上角演示切换器控制 -->
<div class="result-banner result-banner--success" id="resultBanner">
<div class="result-banner__icon">
<svg class="icon-check" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
<svg class="icon-cross" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</div>
<div>
<div class="result-banner__title" id="resultTitle">预约成功</div>
<div class="result-banner__sub" id="resultSub">请按预约日期准时到检</div>
<div class="result-banner__reason">
<span>失败原因:所选日期号源已约满</span>
<span class="reason-help" onclick="openReasonModal()">?</span>
</div>
</div>
</div>
<!-- 预约信息 -->
<div class="info-card">
<div class="section-title">预约信息</div>
<div class="info-card__row">
@@ -173,8 +266,8 @@
</div>
</div>
<!-- 体检须知 -->
<div class="tips-card">
<!-- [交互] 体检须知:仅预约成功态显示,失败态隐藏 -->
<div class="tips-card" id="tipsCard">
<div class="tips-card__title">体检须知</div>
<div class="tips-card__item"><span class="tips-card__dot"></span><span>体检前一天请勿饮酒,避免高脂饮食。</span></div>
<div class="tips-card__item"><span class="tips-card__dot"></span><span>体检当天需空腹,请勿进食或饮水。</span></div>
@@ -184,13 +277,47 @@
</div>
</div>
<div class="bottom-actions">
<!-- [交互] 取消预约 -->
<button class="bottom-actions__btn bottom-actions__btn--ghost" onclick="window.location.href='booking-date.html'">取消预约</button>
<!-- [交互] 查看预约详情,跳转至 my-appointments-detail.html -->
<button class="bottom-actions__btn bottom-actions__btn--primary" onclick="window.location.href='my-appointments-detail.html?done=0&total=9'">查看预约详情</button>
<!-- [交互] 预约成功态底部按钮 -->
<div class="bottom-actions" id="actionsSuccess">
<!-- [交互] 取消预约改为二次确认弹窗 -->
<button class="bottom-actions__btn bottom-actions__btn--ghost" onclick="openCancelModal()">取消预约</button>
<!-- [交互] 现场体检,跳转至 my-appointments-all.html(常规·待体检,含签到码) -->
<button class="bottom-actions__btn bottom-actions__btn--primary" onclick="window.location.href='my-appointments-all.html?type=routine&status=pending'">现场体检</button>
</div>
<!-- [交互] 预约失败态底部按钮:重新预约,跳回预约日期页 -->
<div class="bottom-actions" id="actionsFail" style="display: none;">
<button class="bottom-actions__btn bottom-actions__btn--primary" onclick="window.location.href='booking-date.html'">重新预约</button>
</div>
<div class="toast" id="stepToast">当前已预约体检,无法重新选择,请先取消预约</div>
<!-- [交互] 点击失败原因旁的小问号,弹窗查看具体失败原因 -->
<div class="modal-overlay hidden" id="reasonModal">
<div class="reason-modal">
<div class="reason-modal__title">失败原因详情</div>
<div class="reason-modal__item">
<div class="reason-modal__label">失败原因</div>
<div class="reason-modal__text reason-modal__text--danger">号源争抢失败:宁夏宝石花医院 2026年3月23日 体检号源已被其他员工约满</div>
</div>
<div class="reason-modal__item">
<div class="reason-modal__label">发生时间</div>
<div class="reason-modal__text">2026-03-16 12:00:35</div>
</div>
<button class="reason-modal__btn" onclick="closeReasonModal()">知道了</button>
</div>
</div>
<!-- [交互] 取消预约二次确认弹窗 -->
<div class="modal-overlay hidden" id="cancelModal">
<div class="cancel-modal">
<div class="cancel-modal__title">确认取消预约</div>
<div class="cancel-modal__desc">取消后名额将释放,如仍需体检请重新预约</div>
<div class="cancel-modal__order">宁夏宝石花医院 · 2026年3月23日</div>
<div class="cancel-modal__actions">
<button class="cancel-modal__btn cancel-modal__btn--ghost" onclick="closeCancelModal()">再想想</button>
<button class="cancel-modal__btn cancel-modal__btn--danger" onclick="confirmCancel()">确认取消</button>
</div>
</div>
</div>
</div>
<script>
let toastTimer = null;
@@ -200,6 +327,45 @@
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toast.classList.remove('show'); }, 2200);
}
// [交互] 原型演示:切换预约成功/失败两态(横幅、底部按钮、体检须知、步骤条提示联动)
function setResult(state) {
const fail = state === 'fail';
const banner = document.getElementById('resultBanner');
banner.classList.toggle('result-banner--fail', fail);
banner.classList.toggle('result-banner--success', !fail);
document.getElementById('resultTitle').textContent = fail ? '预约失败' : '预约成功';
document.getElementById('resultSub').style.display = fail ? 'none' : '';
document.getElementById('actionsSuccess').style.display = fail ? 'none' : 'flex';
document.getElementById('actionsFail').style.display = fail ? 'flex' : 'none';
// 预约失败时隐藏体检须知(未成功到检,须知无意义)
document.getElementById('tipsCard').style.display = fail ? 'none' : '';
document.getElementById('stepToast').textContent = fail
? '预约未成功,请点击下方「重新预约」按钮重新预约'
: '当前已预约体检,无法重新选择,请先取消预约';
}
function openReasonModal() {
document.getElementById('reasonModal').classList.remove('hidden');
}
function closeReasonModal() {
document.getElementById('reasonModal').classList.add('hidden');
}
// [交互] 取消预约二次确认:确认后提示已取消,稍后跳回预约日期页
function openCancelModal() {
document.getElementById('cancelModal').classList.remove('hidden');
}
function closeCancelModal() {
document.getElementById('cancelModal').classList.add('hidden');
}
function confirmCancel() {
closeCancelModal();
const toast = document.getElementById('stepToast');
toast.textContent = '预约已取消';
showToast();
setTimeout(() => { window.location.href = 'booking-date.html'; }, 1200);
}
</script>
</body>
</html>
+67 -7
View File
@@ -75,6 +75,24 @@
.fee-banner__label { font-size: 13px; color: var(--primary-dark); font-weight: 600; }
.fee-banner__value { font-size: 24px; font-weight: 800; color: var(--primary-dark); }
.section-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; }
/* 预约状态条 */
.status-card {
background: #fff; border-radius: 20px; padding: 14px 16px; margin-bottom: 14px;
box-shadow: var(--shadow); border: 1px solid rgba(19,194,194,0.06);
}
.status-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.status-badge {
display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700;
padding: 4px 12px; border-radius: 12px; flex-shrink: 0;
}
.status-badge--open { background: #f6ffed; color: #389e0d; border: 1px solid #b7eb8f; }
.status-badge__dot { width: 6px; height: 6px; border-radius: 50%; background: #52c41a; }
.status-range { font-size: 12px; color: var(--text-secondary); font-weight: 600; }
.status-tip {
display: flex; align-items: center; gap: 6px; margin-top: 10px; padding-top: 10px;
border-top: 1px dashed #f0f0f0; font-size: 12px; color: var(--text-muted); line-height: 1.5;
}
.status-tip svg { flex-shrink: 0; }
.date-card {
background: #fff; border-radius: 20px; padding: 18px 16px; margin-bottom: 14px;
box-shadow: var(--shadow); border: 1px solid rgba(19,194,194,0.06);
@@ -196,6 +214,19 @@
.confirm-modal__btn--primary {
background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 100%); color: #fff;
}
/* 预约提交 loading */
.loading-overlay {
position: absolute; inset: 0; background: rgba(255,255,255,0.88); z-index: 150;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
}
.loading-overlay.hidden { display: none; }
.loading-spinner {
width: 44px; height: 44px; border-radius: 50%;
border: 4px solid rgba(19,194,194,0.15); border-top-color: var(--primary);
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { font-size: 14px; color: var(--text-secondary); font-weight: 600; }
.toast {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(0.9);
background: rgba(0,0,0,0.75); color: #fff; padding: 14px 22px; border-radius: 12px;
@@ -241,6 +272,17 @@
<!-- <div class="summary-item"><div class="summary-item__label">预计费用</div><div class="summary-item__value">&yen;680</div></div>-->
</div>
</div>
<!-- [交互] 预约状态与可预约日期范围,日期范围由脚本按「近30天」规则动态生成 -->
<div class="status-card">
<div class="status-row">
<span class="status-badge status-badge--open"><span class="status-badge__dot"></span>已开启预约</span>
<span class="status-range" id="rangeText"></span>
</div>
<div class="status-tip">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8c8c8c" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
<span>至多可预约近30天内的体检日期,超出范围日期不可选择</span>
</div>
</div>
<div class="date-card">
<div class="calendar-header">
<span class="calendar-header__month" id="calendarMonth"></span>
@@ -279,10 +321,6 @@
<span>体检医院</span>
<span class="confirm-modal__value">宁夏宝石花医院</span>
</div>
<div class="confirm-modal__row">
<span>体检项目数</span>
<span class="confirm-modal__value" id="confirmCount">9 项</span>
</div>
<div class="confirm-modal__row">
<span>体检日期</span>
<span class="confirm-modal__value confirm-modal__value--highlight" id="confirmDate">--</span>
@@ -290,14 +328,25 @@
</div>
<div class="confirm-modal__actions">
<button class="confirm-modal__btn confirm-modal__btn--ghost" onclick="closeConfirm()">取消</button>
<button class="confirm-modal__btn confirm-modal__btn--primary" onclick="window.location.href='booking-date-selected.html'">确认预约</button>
<button class="confirm-modal__btn confirm-modal__btn--primary" onclick="submitBooking()">确认预约</button>
</div>
</div>
</div>
<!-- [交互] 确认预约后圆圈loading过渡,约1.8秒后跳转预约结果页 -->
<div class="loading-overlay hidden" id="loadingOverlay">
<div class="loading-spinner"></div>
<div class="loading-text">正在为您预约体检...</div>
</div>
<div class="toast" id="stepToast">当前已提交项目,如需重新选择医院,请先点击下方重选项目按钮</div>
</div>
<script>
const today = new Date(2026, 2, 16); // 2026-03-16
// 近30天可预约范围:今天 ~ 今天+30天
const maxBookDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
function fmtDate(d) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
document.getElementById('rangeText').textContent = `预约开放日期 : ${fmtDate(today)} 至 ${fmtDate(maxBookDate)}`;
let currentYear = today.getFullYear();
let currentMonth = today.getMonth();
let selectedDate = null;
@@ -321,8 +370,9 @@
function getSlots(year, month, day) {
const d = new Date(year, month, day);
const dow = d.getDay();
// 已过日期
// 已过日期或超出近30天范围
if (d < new Date(today.getFullYear(), today.getMonth(), today.getDate())) return -1;
if (d > maxBookDate) return -1;
// 周末不可约
if (dow === 0 || dow === 6) return 0;
// 模拟剩余人数(用日期做伪随机)
@@ -334,8 +384,10 @@
function renderCalendar() {
grid.innerHTML = '';
monthLabel.textContent = `${currentYear}年${currentMonth + 1}月`;
// 不允许切换到当月之前
// 不允许切换到当月之前,也不允许翻过近30天范围所在月份
prevBtn.disabled = (currentYear === today.getFullYear() && currentMonth <= today.getMonth());
nextBtn.disabled = (currentYear > maxBookDate.getFullYear()) ||
(currentYear === maxBookDate.getFullYear() && currentMonth >= maxBookDate.getMonth());
const firstDay = new Date(currentYear, currentMonth, 1);
// 周一为起始(getDay: 0=周日,转为周一起始)
@@ -412,6 +464,14 @@
function closeConfirm() {
document.getElementById('confirmModal').classList.add('hidden');
}
// 提交预约:关闭弹窗 → 圆圈loading → 1.8秒后跳转预约结果页
function submitBooking() {
closeConfirm();
document.getElementById('loadingOverlay').classList.remove('hidden');
setTimeout(() => {
window.location.href = 'booking-date-selected.html';
}, 1800);
}
</script>
</body>
</html>
@@ -110,26 +110,6 @@
.stepper__item--done:hover .stepper__label { text-decoration: underline; text-underline-offset: 3px; }
.stepper__line { width: 44px; height: 3px; background: #e8e8e8; margin: 0 8px; margin-bottom: 22px; border-radius: 2px; }
.stepper__line--done { background: var(--primary); }
.fee-card {
background: linear-gradient(135deg, #f0fffe 0%, #f7fffd 100%);
border: 1px solid #d7f5f3;
border-radius: 18px;
padding: 16px 18px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.fee-card__label {
font-size: 13px;
color: var(--primary-dark);
font-weight: 600;
}
.fee-card__amount {
font-size: 26px;
font-weight: 800;
color: var(--primary-dark);
}
.section-title {
font-size: 16px;
font-weight: 700;
@@ -297,15 +277,16 @@
.modal-field:last-of-type { border-bottom: none; }
.modal-field__label { font-size: 14px; color: var(--text-muted); }
.modal-field__value { font-size: 14px; color: var(--text-primary); font-weight: 600; }
.marriage-toggle {
/* 体检类型切换(已婚体检 / 未婚体检) */
.checkup-type-toggle {
display: inline-flex; background: #f5f5f5; border-radius: 20px; overflow: hidden;
}
.marriage-toggle__btn {
.checkup-type-toggle__btn {
padding: 6px 16px; font-size: 13px; font-weight: 600; border: none;
background: transparent; color: var(--text-muted); cursor: pointer;
border-radius: 20px; transition: all 0.2s ease;
}
.marriage-toggle__btn.active {
.checkup-type-toggle__btn.active {
background: var(--primary); color: #fff;
}
.modal-confirm-btn {
@@ -348,32 +329,35 @@
<div class="stepper__item stepper__item--future"><div class="stepper__dot">3</div><div class="stepper__label">预约日期</div></div>
</div>
<!-- 费用展示 -->
<div class="fee-card">
<div class="fee-card__label">您本年度的体检费用(元)</div>
<div class="fee-card__amount">1500.00</div>
</div>
<!-- 医院列表 -->
<div class="section-title">选择医院</div>
<div class="hospital-card active" onclick="selectHospital(this)">
<div class="hospital-card__info">
<div class="hospital-card__icon">🏥</div>
<div><div class="hospital-card__name">宁夏宝石花医院</div><div class="hospital-card__addr">宁夏银川市</div></div>
<div>
<div class="hospital-card__name">宁夏宝石花医院</div>
<div class="hospital-card__addr">宁夏银川市</div>
</div>
</div>
<div class="radio-dot"></div>
</div>
<div class="hospital-card" onclick="selectHospital(this)">
<div class="hospital-card__info">
<div class="hospital-card__icon">🏥</div>
<div><div class="hospital-card__name">CQ星龙园医院</div><div class="hospital-card__addr">陕西西安市</div></div>
<div>
<div class="hospital-card__name">CQ星龙园医院</div>
<div class="hospital-card__addr">陕西西安市</div>
</div>
</div>
<div class="radio-dot"></div>
</div>
<div class="hospital-card" onclick="selectHospital(this)">
<div class="hospital-card__info">
<div class="hospital-card__icon">🏥</div>
<div><div class="hospital-card__name">西安CQ医院</div><div class="hospital-card__addr">陕西西安市</div></div>
<div>
<div class="hospital-card__name">西安CQ医院</div>
<div class="hospital-card__addr">陕西西安市</div>
</div>
</div>
<div class="radio-dot"></div>
</div>
@@ -400,17 +384,21 @@
</div>
<div class="modal-field">
<span class="modal-field__label">身份证号</span>
<span class="modal-field__value">6101021994********</span>
<span class="modal-field__value">610102199403154729</span>
</div>
<div class="modal-field">
<span class="modal-field__label">性别</span>
<span class="modal-field__value"></span>
</div>
<div class="modal-field">
<span class="modal-field__label">婚姻状况</span>
<div class="marriage-toggle">
<button class="marriage-toggle__btn active" onclick="toggleMarriage(this)">已婚</button>
<button class="marriage-toggle__btn" onclick="toggleMarriage(this)">未婚</button>
<span class="modal-field__label">年龄</span>
<span class="modal-field__value">32</span>
</div>
<div class="modal-field">
<span class="modal-field__label">体检类型</span>
<div class="checkup-type-toggle">
<button class="checkup-type-toggle__btn active" onclick="toggleCheckupType(this)">已婚体检</button>
<button class="checkup-type-toggle__btn" onclick="toggleCheckupType(this)">未婚体检</button>
</div>
</div>
<button class="modal-confirm-btn" onclick="closeModal()">确认</button>
@@ -433,8 +421,9 @@
function closeModal() {
document.getElementById('personModal').classList.add('hidden');
}
function toggleMarriage(btn) {
document.querySelectorAll('.marriage-toggle__btn').forEach(b => b.classList.remove('active'));
// [交互] 切换体检类型(已婚体检 / 未婚体检)
function toggleCheckupType(btn) {
document.querySelectorAll('.checkup-type-toggle__btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
</script>
+397 -87
View File
@@ -81,14 +81,34 @@
.stepper__item--done:hover .stepper__label { text-decoration: underline; text-underline-offset: 3px; }
.stepper__line { width: 44px; height: 3px; background: #e8e8e8; margin: 0 8px; margin-bottom: 22px; border-radius: 2px; }
.stepper__line--done { background: var(--primary); }
/* 费用卡片 */
.fee-banner {
/* 体检额度卡片 */
.quota-card {
background: linear-gradient(135deg, #f0fffe 0%, #f7fffd 100%);
border: 1px solid #d7f5f3; border-radius: 18px; padding: 14px 16px;
display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;
border: 1px solid #d7f5f3; border-radius: 18px; padding: 14px 16px 12px;
margin-bottom: 14px;
}
.fee-banner__label { font-size: 13px; color: var(--primary-dark); font-weight: 600; }
.fee-banner__value { font-size: 24px; font-weight: 800; color: var(--primary-dark); }
.quota-card__main {
display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 10px;
}
.quota-card__label { font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; }
.quota-card__value { font-size: 24px; font-weight: 800; color: var(--primary-dark); line-height: 1.15; }
.quota-card__value--secondary { font-size: 20px; color: var(--text-primary); }
.quota-card__bar {
height: 8px; background: rgba(19,194,194,0.12); border-radius: 4px;
overflow: hidden; margin-bottom: 12px;
}
.quota-card__bar-fill {
height: 100%; width: 0; border-radius: 4px;
background: linear-gradient(90deg, #13c2c2, #36cfc9); transition: width 0.3s;
}
.quota-card__bar-fill--over { background: linear-gradient(90deg, #ff4d4f, #ff7875); }
.quota-card__grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
border-top: 1px dashed #cdeeeb; padding-top: 12px;
}
.quota-cell__label { font-size: 12px; color: var(--text-secondary); }
.quota-cell__value { font-size: 15px; font-weight: 700; color: var(--text-primary); margin-top: 3px; }
.quota-cell__value--danger { color: var(--danger); }
/* 项目列表 */
.section-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; }
.project-tabs {
@@ -110,6 +130,7 @@
.project-list-panel::-webkit-scrollbar { display: none; }
.project-grid { display: flex; flex-direction: column; gap: 10px; }
.project-card {
position: relative;
background: #fff; border-radius: 14px; padding: 14px 16px; border: 2px solid #eef5f5;
cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
@@ -120,6 +141,9 @@
cursor: default; background: #fff; border-color: #eef5f5;
}
.project-card.project-card--readonly:hover { border-color: #eef5f5; }
/* 额度已满后剩余项目置灰,不可再选 */
.project-card--disabled { opacity: 0.45; cursor: not-allowed; }
.project-card--disabled:hover { border-color: #eef5f5; background: #fff; }
.project-card__check {
width: 22px; height: 22px; border-radius: 6px; border: 2px solid #d9d9d9;
display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all 0.2s;
@@ -131,16 +155,22 @@
.project-card.selected .project-card__check { background: var(--primary); border-color: var(--primary); }
.project-card.selected .project-card__check::after { opacity: 1; }
.project-card__info { flex: 1; min-width: 0; }
.project-card__name { font-size: 14px; font-weight: 600; margin-bottom: 2px; }
.project-card__desc { font-size: 12px; color: var(--text-muted); }
.project-card__name { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
/* 项目示意图标块:沿用医院卡渐变底 + emoji 占位风格 */
.project-card__img {
width: 52px; height: 52px; border-radius: 14px;
background: linear-gradient(135deg, #e6fffb 0%, #b5f5ec 100%);
display: flex; align-items: center; justify-content: center;
font-size: 24px; flex-shrink: 0;
}
.project-card.selected .project-card__img {
background: linear-gradient(135deg, #b5f5ec 0%, #87e8de 100%);
}
.project-card__side {
flex-shrink: 0; display: flex; align-items: center; gap: 10px;
margin-left: auto; padding-left: 8px;
}
.project-card__tags {
display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
}
.project-card__price { font-size: 13px; color: var(--primary-dark); font-weight: 700; }
.project-card__price { font-size: 14px; color: var(--primary-dark); font-weight: 700; }
.project-card__badge {
display: inline-flex; align-items: center; justify-content: center; height: 22px;
padding: 0 10px; border-radius: 11px; background: #f5f5f5;
@@ -149,6 +179,32 @@
.project-card__badge--key {
background: #fff1f0; color: #ff4d4f;
}
.project-card__meta {
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
/* AI 推荐标签分三档,字号阶梯放大 + 深色实底强化等级对比:
强烈推荐 12px 红橙实底 > 建议加做 11px 紫蓝实底 > 可选参考 10px 浅蓝描边 */
.project-card__badge--ai {
color: #fff; cursor: pointer; font-weight: 700;
}
.project-card__badge--ai-high {
height: 26px; padding: 0 12px; border-radius: 13px; font-size: 12px;
background: linear-gradient(135deg, #cf1322 0%, #f5222d 55%, #ff7a45 100%);
box-shadow: 0 3px 8px rgba(207, 19, 34, 0.35);
}
.project-card__badge--ai-mid {
height: 24px; padding: 0 11px; border-radius: 12px; font-size: 11px;
background: linear-gradient(135deg, #531dab 0%, #722ed1 50%, #1677ff 100%);
box-shadow: 0 2px 6px rgba(83, 29, 171, 0.35);
}
.project-card__badge--ai-low {
height: 20px; padding: 0 8px; border-radius: 10px; font-size: 10px; font-weight: 600;
background: #f0f5ff; color: #597ef7; border: 1px solid #d6e4ff;
}
/* 列表卡片上的 AI 推荐标签固定在卡片右上角(弹窗内的同名标签不受影响) */
.project-card__badge--ai-corner {
position: absolute; top: 6px; right: 16px;
}
.project-card__arrow {
flex-shrink: 0; color: #d0d0d0; font-size: 16px; cursor: pointer;
transition: color 0.2s;
@@ -159,23 +215,6 @@
position: absolute; left: 16px; right: 16px; bottom: 18px;
display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding-bottom: 16px;
}
.summary-card {
background: #fff; border-radius: 18px; padding: 12px 14px;
box-shadow: 0 10px 24px rgba(19, 194, 194, 0.10); border: 1px solid rgba(19, 194, 194, 0.08);
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px;
margin-bottom: 14px;
}
.summary-card__title {
display: none;
}
.summary-row {
display: flex; justify-content: space-between; align-items: center;
font-size: 11px; color: var(--text-secondary); margin-bottom: 0; gap: 8px;
}
.summary-row:last-child { margin-bottom: 0; }
.summary-row__value { font-weight: 700; color: var(--text-primary); white-space: nowrap; }
.summary-row__value--highlight { color: var(--primary-dark); font-size: 13px; }
.summary-row__value--danger { color: var(--danger); }
.bottom-actions__btn {
width: 100%; height: 56px; border-radius: 18px; font-size: 15px; font-weight: 600; border: none; cursor: pointer;
}
@@ -213,6 +252,7 @@
.confirm-modal__row:last-child { margin-bottom: 0; }
.confirm-modal__value { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.confirm-modal__value--highlight { color: var(--primary-dark); }
.confirm-modal__value--limit { color: var(--text-secondary); font-weight: 600; }
.confirm-modal__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.confirm-modal__btn {
height: 44px; border-radius: 22px; font-size: 15px; font-weight: 600; border: none; cursor: pointer;
@@ -221,6 +261,44 @@
.confirm-modal__btn--primary {
background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 100%); color: #fff;
}
/* 互斥项目弹窗:二选一选项行 */
.mutex-options { margin-bottom: 18px; }
.mutex-option {
display: flex; align-items: center; gap: 10px;
padding: 12px 14px; border: 2px solid #eef5f5; border-radius: 14px;
background: #fff; cursor: pointer; transition: all 0.2s;
}
.mutex-option + .mutex-option { margin-top: 10px; }
.mutex-option.active { border-color: var(--primary); background: var(--primary-light); }
.mutex-option__radio {
width: 18px; height: 18px; border-radius: 50%; border: 2px solid #d9d9d9;
flex-shrink: 0; position: relative; transition: all 0.2s;
}
.mutex-option.active .mutex-option__radio { border-color: var(--primary); }
.mutex-option.active .mutex-option__radio::after {
content: ''; position: absolute; left: 3px; top: 3px; width: 8px; height: 8px;
border-radius: 50%; background: var(--primary);
}
.mutex-option__name { flex: 1; min-width: 0; font-size: 14px; font-weight: 600; }
.mutex-option__price { font-size: 13px; font-weight: 700; color: var(--primary-dark); }
/* AI 推荐理由弹窗 */
.ai-modal { padding: 24px 20px 18px; }
.ai-modal__project {
font-size: 15px; font-weight: 700; color: #7c5cfc; text-align: center; margin-bottom: 10px;
}
.ai-modal__level {
display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 12px;
}
.ai-modal__level-cap { font-size: 12px; color: var(--text-muted); }
.ai-modal__level-desc {
font-size: 12px; line-height: 1.6; color: var(--text-secondary); text-align: center;
margin: -4px 0 12px;
}
.ai-modal__reason {
background: #f7f9fb; border-radius: 14px; padding: 14px 16px; margin-bottom: 18px;
font-size: 13px; color: var(--text-secondary); line-height: 1.7; text-align: left;
}
.ai-modal__close { width: 100%; }
</style>
</head>
<body>
@@ -246,28 +324,32 @@
<div class="stepper__line"></div>
<div class="stepper__item stepper__item--future"><div class="stepper__dot">3</div><div class="stepper__label">预约日期</div></div>
</div>
<!-- 费用 -->
<div class="fee-banner">
<span class="fee-banner__label">您本年度的体检费用(元)</span>
<span class="fee-banner__value">1500.00</span>
</div>
<div class="summary-card">
<div class="summary-card__title">费用汇总</div>
<div class="summary-row">
<span>已选</span>
<span class="summary-row__value summary-row__value--highlight" id="totalFee">¥0.00</span>
<!-- 体检额度 -->
<div class="quota-card">
<div class="quota-card__main">
<div>
<div class="quota-card__label">年度体检总额度(元)</div>
<div class="quota-card__value" id="annualLimitText">¥0.00</div>
</div>
<div>
<div class="quota-card__label">已选项目费用</div>
<div class="quota-card__value quota-card__value--secondary" id="totalFee">¥0.00</div>
</div>
</div>
<div class="summary-row">
<span>必检项目</span>
<span class="summary-row__value" id="requiredFee">¥0.00</span>
</div>
<div class="summary-row">
<span>选检项目</span>
<span class="summary-row__value" id="optionalFee">¥0.00</span>
</div>
<div class="summary-row">
<span>可用额度</span>
<span class="summary-row__value" id="availableFee">¥0.00</span>
<div class="quota-card__bar"><div class="quota-card__bar-fill" id="quotaBarFill"></div></div>
<div class="quota-card__grid">
<div class="quota-cell">
<div class="quota-cell__label">必检项目</div>
<div class="quota-cell__value" id="requiredFee">¥0.00</div>
</div>
<div class="quota-cell">
<div class="quota-cell__label">选检项目</div>
<div class="quota-cell__value" id="optionalFee">¥0.00</div>
</div>
<div class="quota-cell">
<div class="quota-cell__label">可用额度</div>
<div class="quota-cell__value" id="availableFee">¥0.00</div>
</div>
</div>
</div>
<!-- 项目列表 -->
@@ -298,8 +380,20 @@
<span class="confirm-modal__value" id="confirmCount">0 项</span>
</div>
<div class="confirm-modal__row">
<span>总体检费用</span>
<span class="confirm-modal__value confirm-modal__value--highlight" id="confirmFee">¥0.00</span>
<span>项目总额</span>
<span class="confirm-modal__value confirm-modal__value--highlight" id="confirmTotalFee">¥0.00</span>
</div>
<div class="confirm-modal__row">
<span>必检项目</span>
<span class="confirm-modal__value" id="confirmRequiredFee">¥0.00</span>
</div>
<div class="confirm-modal__row">
<span>体检总额度</span>
<span class="confirm-modal__value confirm-modal__value--limit" id="confirmAnnualLimit">≤ ¥0.00</span>
</div>
<div class="confirm-modal__row">
<span>可用额度</span>
<span class="confirm-modal__value confirm-modal__value--limit" id="confirmAvailableFee">¥0.00</span>
</div>
</div>
<div class="confirm-modal__actions">
@@ -308,22 +402,83 @@
</div>
</div>
</div>
<!-- [交互] 轻触 AI 推荐标签弹出推荐理由,点「知道了」关闭 -->
<div class="modal-overlay hidden" id="aiModal">
<div class="confirm-modal ai-modal">
<div class="confirm-modal__title">✨ 智能推荐</div>
<div class="ai-modal__project" id="aiModalProject"></div>
<div class="ai-modal__level">
<span class="ai-modal__level-cap">推荐等级</span>
<span class="project-card__badge" id="aiModalLevel"></span>
</div>
<div class="ai-modal__level-desc" id="aiModalLevelDesc"></div>
<div class="ai-modal__reason" id="aiModalReason"></div>
<button class="confirm-modal__btn confirm-modal__btn--primary ai-modal__close" onclick="closeAiReason()">知道了</button>
</div>
</div>
<!-- [交互] 关联项目提醒:同步选中 / 取消选择 -->
<div class="modal-overlay hidden" id="linkModal">
<div class="confirm-modal">
<div class="confirm-modal__title">🔗 关联项目提醒</div>
<div class="confirm-modal__desc" id="linkModalDesc"></div>
<div class="confirm-modal__info" id="linkModalInfo"></div>
<div class="confirm-modal__actions">
<button class="confirm-modal__btn confirm-modal__btn--ghost" onclick="closeLinkModal()">取消选择</button>
<button class="confirm-modal__btn confirm-modal__btn--primary" onclick="confirmLinkSelect()">同步选中</button>
</div>
</div>
</div>
<!-- [交互] 互斥项目提醒:两项展示出来二选一,确定后替换已选项 -->
<div class="modal-overlay hidden" id="mutexModal">
<div class="confirm-modal">
<div class="confirm-modal__title">⚠️ 互斥项目提醒</div>
<div class="confirm-modal__desc" id="mutexModalDesc"></div>
<div class="mutex-options" id="mutexOptions"></div>
<div class="confirm-modal__actions">
<button class="confirm-modal__btn confirm-modal__btn--ghost" onclick="closeMutexModal()">取消</button>
<button class="confirm-modal__btn confirm-modal__btn--primary" onclick="confirmMutexSelect()">确定</button>
</div>
</div>
</div>
<!-- [交互] 额度不足提醒 -->
<div class="modal-overlay hidden" id="exceedModal">
<div class="confirm-modal">
<div class="confirm-modal__title">💰 额度不足提醒</div>
<div class="confirm-modal__desc" id="exceedModalDesc"></div>
<div class="confirm-modal__actions">
<button class="confirm-modal__btn confirm-modal__btn--primary" onclick="closeExceedModal()">知道了</button>
</div>
</div>
</div>
</div>
<script>
const annualLimit = 1500;
// 年度体检总额度(可用额度 = 总额度 − 已选项目费用,原型模拟数据)
const annualLimit = 1600;
// AI 推荐等级:3 强烈推荐(既往异常直接相关)> 2 建议加做(存在风险因素)> 1 可选参考(一般性补充筛查)
const AI_LEVELS = {
3: { label: '强烈推荐', cls: 'high', icon: '🔥', desc: '与您的既往异常发现直接相关,建议本年度务必完成。' },
2: { label: '建议加做', cls: 'mid', icon: '✨', desc: '健康画像显示存在相关风险因素,建议在本年度体检中加做。' },
1: { label: '可选参考', cls: 'low', icon: '✨', desc: '按年龄段与常见风险的一般性补充筛查,可结合剩余额度自行选择。' }
};
const projects = [
{ id: 'general', name: '一般检查', desc: '身高、体重、血压、视力', price: 120, type: 'required', level: 'normal' },
{ id: 'blood', name: '血常规', desc: '血红蛋白、白细胞、血小板等', price: 80, type: 'required', level: 'normal' },
{ id: 'urine', name: '尿常规', desc: '尿蛋白、尿糖、尿潜血等', price: 45, type: 'required', level: 'normal' },
{ id: 'liver', name: '肝功能', desc: 'ALT、AST、总胆红素等', price: 160, type: 'required', level: 'key' },
{ id: 'kidney', name: '肾功能', desc: '肌酐、尿素氮、尿酸等', price: 110, type: 'required', level: 'key' },
{ id: 'lipid', name: '血脂四项', desc: '总胆固醇、甘油三酯、HDL、LDL', price: 90, type: 'required', level: 'normal' },
{ id: 'glucose', name: '空腹血糖', desc: '空腹静脉血糖检测', price: 40, type: 'required', level: 'normal' },
{ id: 'ecg', name: '心电图', desc: '十二导联心电图检查', price: 75, type: 'required', level: 'key' },
{ id: 'xray', name: '胸部X光', desc: '胸部正位X线摄影', price: 140, type: 'optional', level: 'normal' },
{ id: 'ultrasound', name: '腹部B超', desc: '肝胆脾胰肾超声检查', price: 180, type: 'optional', level: 'key' },
{ id: 'thyroid', name: '甲状腺彩超', desc: '甲状腺结构与结节筛查', price: 160, type: 'optional', level: 'key' },
{ id: 'tumor', name: '肿瘤标志物', desc: '基础肿瘤风险筛查', price: 220, type: 'optional', level: 'key' }
{ id: 'general', name: '一般检查', icon: '📏', price: 120, type: 'required', level: 'normal' },
{ id: 'blood', name: '血常规', icon: '🩸', price: 80, type: 'required', level: 'normal' },
{ id: 'urine', name: '尿常规', icon: '🧪', price: 45, type: 'required', level: 'normal' },
{ id: 'liver', name: '肝功能', icon: '⚗️', price: 160, type: 'required', level: 'key' },
{ id: 'kidney', name: '肾功能', icon: '🧫', price: 110, type: 'required', level: 'key' },
{ id: 'lipid', name: '血脂四项', icon: '🧬', price: 90, type: 'required', level: 'normal' },
{ id: 'glucose', name: '空腹血糖', icon: '🍬', price: 40, type: 'required', level: 'normal' },
{ id: 'ecg', name: '心电图', icon: '📈', price: 75, type: 'required', level: 'key' },
{ id: 'xray', name: '胸部X光', icon: '🦴', price: 140, type: 'optional', level: 'normal' },
{ id: 'ultrasound', name: '腹部B超', icon: '🔬', price: 180, type: 'optional', level: 'key', aiLevel: 3, linkId: 'thyroid', aiReason: '健康画像显示 BMI 26.8(超重),且近年血脂持续偏高,属于脂肪肝与胆囊病变的高风险人群,建议本年度务必完成腹部超声重点筛查。预计费用 ¥180,在本年度体检额度内。' },
{ id: 'thyroid', name: '甲状腺彩超', icon: '🦋', price: 160, type: 'optional', level: 'key', aiLevel: 2, linkId: 'ultrasound', aiReason: '既往史显示 2024 年体检发现甲状腺结节(TI-RADS 2 类),建议每年做一次超声随访复查,观察结节变化。预计费用 ¥160,在本年度体检额度内。' },
{ id: 'tumor', name: '肿瘤标志物', icon: '🎗️', price: 220, type: 'optional', level: 'key', aiLevel: 2, mutexId: 'echo', aiReason: '家族史提示直系亲属有肿瘤病史,结合您所处年龄段(45 岁以上),建议今年纳入基础肿瘤标志物筛查,做到早发现早干预。预计费用 ¥220,在本年度体检额度内。' },
{ id: 'echo', name: '心脏彩超', icon: '🫀', price: 360, type: 'optional', level: 'key', aiLevel: 1, mutexId: 'tumor', aiReason: '近两次体检血压处于正常高值,可完成一次心脏彩超,建立心脏结构与瓣膜功能的基线资料,便于日后对比随访。预计费用 ¥360,请注意控制项目总额。' },
{ id: 'gastro', name: '胃幽门螺杆菌检测', icon: '🦠', price: 480, type: 'optional', level: 'normal', aiLevel: 1, aiReason: '幽门螺杆菌是胃癌明确的危险因素,国内感染率较高。如从未检测过,建议先做一次 C13 呼气试验筛查,阳性可规范四联根除治疗。预计费用 ¥480,可结合剩余额度自行选择。' }
];
let activeTab = 'optional';
@@ -335,9 +490,11 @@
const requiredFeeEl = document.getElementById('requiredFee');
const optionalFeeEl = document.getElementById('optionalFee');
const availableFeeEl = document.getElementById('availableFee');
const quotaBarFillEl = document.getElementById('quotaBarFill');
const annualLimitTextEl = document.getElementById('annualLimitText');
function formatPrice(value) {
return `¥${value.toFixed(2)}`;
return `¥${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function getRequiredProjects() {
@@ -345,7 +502,11 @@
}
function getOptionalProjects() {
return projects.filter(project => project.type === 'optional');
// [交互] AI 推荐项目按推荐等级从高到低排在列表前面,未推荐项目保持原顺序
return projects
.filter(project => project.type === 'optional')
.slice()
.sort((a, b) => (b.aiLevel || 0) - (a.aiLevel || 0));
}
function getRequiredFee() {
@@ -358,17 +519,26 @@
.reduce((sum, project) => sum + project.price, 0);
}
function getAvailableFee() {
return annualLimit - getRequiredFee() - getOptionalFee();
}
function updateSummary() {
const requiredFee = getRequiredFee();
const optionalFee = getOptionalFee();
const totalFee = requiredFee + optionalFee;
const availableFee = annualLimit - totalFee;
const overLimit = availableFee < 0;
requiredFeeEl.textContent = formatPrice(requiredFee);
optionalFeeEl.textContent = formatPrice(optionalFee);
totalFeeEl.textContent = formatPrice(totalFee);
availableFeeEl.textContent = formatPrice(Math.max(availableFee, 0));
availableFeeEl.classList.toggle('summary-row__value--danger', availableFee < 0);
availableFeeEl.classList.toggle('quota-cell__value--danger', overLimit);
const ratio = Math.min(totalFee / annualLimit, 1);
quotaBarFillEl.style.width = (ratio * 100).toFixed(1) + '%';
quotaBarFillEl.classList.toggle('quota-card__bar-fill--over', overLimit);
}
function renderProjects() {
@@ -379,21 +549,25 @@
const card = document.createElement('div');
const isReadonly = project.type === 'required';
const isSelected = selectedOptionalIds.has(project.id);
// 额度已满时,未选中的可选项目置灰不可再选
const isDisabled = !isReadonly && !isSelected && getAvailableFee() <= 0;
const projectLevelText = project.level === 'key' ? '重点项目' : '一般项目';
card.className = `project-card${isReadonly ? ' project-card--readonly' : ''}${isSelected ? ' selected' : ''}`;
const aiLevel = AI_LEVELS[project.aiLevel];
card.className = `project-card${isReadonly ? ' project-card--readonly' : ''}${isSelected ? ' selected' : ''}${isDisabled ? ' project-card--disabled' : ''}`;
card.innerHTML = `
<div class="project-card__check${isReadonly ? ' project-card__check--hidden' : ''}"></div>
<div class="project-card__img">${project.icon}</div>
<div class="project-card__info">
<div class="project-card__name">${project.name}</div>
<div class="project-card__desc">${project.desc}</div>
</div>
<div class="project-card__side">
<div class="project-card__tags">
<span class="project-card__price">${formatPrice(project.price)}</span>
<div class="project-card__meta">
<span class="project-card__badge${project.level === 'key' ? ' project-card__badge--key' : ''}">${projectLevelText}</span>
</div>
</div>
<div class="project-card__side">
<span class="project-card__price">${formatPrice(project.price)}</span>
<span class="project-card__arrow" data-detail="${project.id}">&#8250;</span>
</div>
${aiLevel ? `<span class="project-card__badge project-card__badge--ai project-card__badge--ai-${aiLevel.cls} project-card__badge--ai-corner">${aiLevel.icon} ${aiLevel.label}</span>` : ''}
`;
// [交互] 箭头点击跳转项目详情
@@ -402,22 +576,143 @@
window.location.href = `project-detail.html?id=${project.id}`;
});
if (!isReadonly) {
card.addEventListener('click', () => {
if (selectedOptionalIds.has(project.id)) {
selectedOptionalIds.delete(project.id);
} else {
selectedOptionalIds.add(project.id);
}
renderProjects();
updateSummary();
// [交互] 轻触 AI 推荐标签查看推荐理由(不触发卡片勾选)
const aiTag = card.querySelector('.project-card__badge--ai');
if (aiTag) {
aiTag.addEventListener('click', (e) => {
e.stopPropagation();
openAiReason(project);
});
}
if (!isReadonly) {
card.addEventListener('click', () => trySelectProject(project));
}
grid.appendChild(card);
});
}
// ===== 选择流程:关联项目同步选中 / 互斥项目二选一 / 额度校验 =====
// 关联:ultrasound ↔ thyroid;互斥:tumor ↔ echo
let pendingLinkProject = null;
let pendingMutex = null;
function trySelectProject(project) {
// 已选中 → 取消勾选,关联项目同步取消
if (selectedOptionalIds.has(project.id)) {
selectedOptionalIds.delete(project.id);
const linked = project.linkId && projects.find(p => p.id === project.linkId);
if (linked && selectedOptionalIds.has(linked.id)) selectedOptionalIds.delete(linked.id);
renderProjects();
updateSummary();
return;
}
// 额度已满:剩余项目置灰,点击不响应
if (getAvailableFee() <= 0) return;
// 关联项目:对方未选中 → 弹窗提醒同步选中
const linked = project.linkId && projects.find(p => p.id === project.linkId);
if (linked && !selectedOptionalIds.has(linked.id)) {
openLinkModal(project, linked);
return;
}
// 互斥项目:对方已选中 → 弹窗二选一
const mutex = project.mutexId && projects.find(p => p.id === project.mutexId);
if (mutex && selectedOptionalIds.has(mutex.id)) {
openMutexModal(project, mutex);
return;
}
// 单选超额 → 弹窗提示,不生效
if (getAvailableFee() < project.price) {
openExceedModal(`「${project.name}」预计费用 ${formatPrice(project.price)},选择后项目总额将超出年度体检额度,请调整已选项目后再试`);
return;
}
selectedOptionalIds.add(project.id);
renderProjects();
updateSummary();
}
// 关联项目弹窗
function openLinkModal(project, linked) {
pendingLinkProject = project;
document.getElementById('linkModalDesc').innerHTML =
`「${project.name}」与「${linked.name}」为关联项目,<br>选中其中一项将同步选中另一项`;
document.getElementById('linkModalInfo').innerHTML = [project, linked].map(p =>
`<div class="confirm-modal__row"><span>${p.name}</span><span class="confirm-modal__value">${formatPrice(p.price)}</span></div>`
).join('');
document.getElementById('linkModal').classList.remove('hidden');
}
function closeLinkModal() {
document.getElementById('linkModal').classList.add('hidden');
}
function confirmLinkSelect() {
const project = pendingLinkProject;
const linked = projects.find(p => p.id === project.linkId);
const cost = project.price + linked.price;
// 两项合计超出剩余额度 → 转额度不足提醒
if (getAvailableFee() < cost) {
closeLinkModal();
openExceedModal(`「${project.name}」与「${linked.name}」需同步选中,合计 ${formatPrice(cost)},选择后项目总额将超出年度体检额度,请调整已选项目后再试`);
return;
}
selectedOptionalIds.add(project.id);
selectedOptionalIds.add(linked.id);
closeLinkModal();
renderProjects();
updateSummary();
}
// 互斥项目弹窗:展示两项做单选,确定后替换已选项
function openMutexModal(clicked, other) {
pendingMutex = { clicked, other, chosenId: clicked.id };
document.getElementById('mutexModalDesc').innerHTML =
`「${clicked.name}」与「${other.name}」互斥,只能同时体检其中一个项目,请选择本次要做的项目`;
renderMutexOptions();
document.getElementById('mutexModal').classList.remove('hidden');
}
function renderMutexOptions() {
document.getElementById('mutexOptions').innerHTML = [pendingMutex.clicked, pendingMutex.other].map(p => `
<div class="mutex-option${pendingMutex.chosenId === p.id ? ' active' : ''}" onclick="chooseMutexOption('${p.id}')">
<span class="mutex-option__radio"></span>
<span class="mutex-option__name">${p.name}</span>
<span class="mutex-option__price">${formatPrice(p.price)}</span>
</div>
`).join('');
}
function chooseMutexOption(id) {
pendingMutex.chosenId = id;
renderMutexOptions();
}
function closeMutexModal() {
document.getElementById('mutexModal').classList.add('hidden');
}
function confirmMutexSelect() {
const { clicked, other, chosenId } = pendingMutex;
const chosen = chosenId === clicked.id ? clicked : other;
const dropped = chosenId === clicked.id ? other : clicked;
// 替换后的可用额度 = 当前可用 + 被替换项目释放额度 - 新选项目费用
if (getAvailableFee() + dropped.price - chosen.price < 0) {
closeMutexModal();
openExceedModal(`选择「${chosen.name}」后项目总额将超出年度体检额度,请调整已选项目后再试`);
return;
}
selectedOptionalIds.delete(clicked.id);
selectedOptionalIds.delete(other.id);
selectedOptionalIds.add(chosen.id);
closeMutexModal();
renderProjects();
updateSummary();
}
// 额度不足提醒弹窗
function openExceedModal(desc) {
document.getElementById('exceedModalDesc').innerHTML = desc;
document.getElementById('exceedModal').classList.remove('hidden');
}
function closeExceedModal() {
document.getElementById('exceedModal').classList.add('hidden');
}
tabButtons.forEach(button => {
button.addEventListener('click', () => {
activeTab = button.dataset.tab;
@@ -427,16 +722,18 @@
});
});
annualLimitTextEl.textContent = formatPrice(annualLimit);
renderProjects();
updateSummary();
function openSubmitConfirm() {
const requiredCount = getRequiredProjects().length;
const optionalCount = selectedOptionalIds.size;
const totalCount = requiredCount + optionalCount;
const totalFee = getRequiredFee() + getOptionalFee();
const totalCount = requiredCount + selectedOptionalIds.size;
document.getElementById('confirmCount').textContent = totalCount + ' 项';
document.getElementById('confirmFee').textContent = formatPrice(totalFee);
document.getElementById('confirmTotalFee').textContent = formatPrice(getRequiredFee() + getOptionalFee());
document.getElementById('confirmRequiredFee').textContent = formatPrice(getRequiredFee());
document.getElementById('confirmAnnualLimit').textContent = '≤ ' + formatPrice(annualLimit);
document.getElementById('confirmAvailableFee').textContent = formatPrice(Math.max(getAvailableFee(), 0));
document.getElementById('submitModal').classList.remove('hidden');
}
function closeSubmitConfirm() {
@@ -445,6 +742,19 @@
function goPrevStep() {
window.location.href = 'booking-hospital.html';
}
function openAiReason(project) {
const level = AI_LEVELS[project.aiLevel];
document.getElementById('aiModalProject').textContent = project.name;
const levelEl = document.getElementById('aiModalLevel');
levelEl.textContent = level.icon + ' ' + level.label;
levelEl.className = 'project-card__badge project-card__badge--ai project-card__badge--ai-' + level.cls;
document.getElementById('aiModalLevelDesc').textContent = level.desc;
document.getElementById('aiModalReason').textContent = project.aiReason;
document.getElementById('aiModal').classList.remove('hidden');
}
function closeAiReason() {
document.getElementById('aiModal').classList.add('hidden');
}
function confirmSubmit() {
window.location.href = 'booking-project-selected.html';
}

Some files were not shown because too many files have changed in this diff Show More