Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b0c78bdd | ||
|
|
4767c24d3d | ||
|
|
44d7d0b219 | ||
|
|
8e6c10f0bc | ||
|
|
a900886061 | ||
|
|
71ddc7fa0a | ||
|
|
63407a20c2 | ||
|
|
5e479bc6fe | ||
|
|
b0479ee9d1 | ||
|
|
c3c51978de | ||
|
|
3ae4e98f9a | ||
|
|
cbf21d1817 | ||
|
|
f1f412d897 | ||
|
|
05f83885ac | ||
|
|
9cd970a31e | ||
|
|
a54d933841 | ||
|
|
489a72838e | ||
|
|
2dfc61bedf | ||
|
|
c85d9c124e | ||
|
|
3e7cd268fa | ||
|
|
d5756021b6 | ||
|
|
bdc44daf24 | ||
|
|
dcacbe289b | ||
|
|
39547a8bb6 | ||
|
|
bf08a9614b | ||
|
|
17595d5fb2 | ||
|
|
3800631e88 | ||
|
|
18dcb35851 | ||
|
|
b5a8b83e6d | ||
|
|
256b741c8f | ||
|
|
97652d797f | ||
|
|
9434681a80 | ||
|
|
982cb1c068 | ||
|
|
8a3a116501 | ||
|
|
03a186fc7d | ||
|
|
91efe28d41 | ||
|
|
bc77229275 | ||
|
|
26419bc1de | ||
|
|
60b4e38166 | ||
|
|
55819e077a | ||
|
|
0bc9a33aec | ||
|
|
47ab38e28d | ||
|
|
b4f3d5cb8c | ||
|
|
d76285aa4a | ||
|
|
fbe7defafa | ||
|
|
708f1c29e2 | ||
|
|
891678d97e | ||
|
|
7f184709f3 | ||
|
|
15ca37b892 | ||
|
|
29a09e9402 | ||
|
|
05e157e667 | ||
|
|
118d143767 | ||
|
|
2213106961 | ||
|
|
3a838d04a2 | ||
|
|
37ae0543a1 | ||
|
|
a443c61907 | ||
|
|
0d1c770ee9 | ||
|
|
3ae6cd855a | ||
|
|
12f54cdbd9 | ||
|
|
db8c2d1d08 | ||
|
|
deb366e19c | ||
|
|
2996e835c7 | ||
|
|
7333189799 | ||
|
|
ab27a394f3 | ||
|
|
61103fc4e7 | ||
|
|
1689707ab2 | ||
|
|
1d31d1c53c | ||
|
|
f177c05119 | ||
|
|
b96b936604 | ||
|
|
0af98f6144 | ||
|
|
539a2ba232 | ||
|
|
f90f260769 | ||
|
|
76dbca9fb7 | ||
|
|
173e849b5e | ||
|
|
11415da744 | ||
|
|
81cddba76e | ||
|
|
40e0b311c4 | ||
|
|
059d1c82ab | ||
|
|
e76ca89ae6 | ||
|
|
9f8a80eae9 | ||
|
|
52aca0a74b | ||
|
|
da410f71b0 | ||
|
|
e663982b77 | ||
|
|
c4ca35ec60 | ||
|
|
e698c874e8 | ||
|
|
00cd56530c | ||
|
|
2922834e9d | ||
|
|
ef2dc7dfb4 | ||
|
|
564dd1c1c5 | ||
|
|
d13eb783ad | ||
|
|
5d5b61e965 | ||
|
|
b62d1830b7 | ||
|
|
a723eba52e | ||
|
|
2d8b9e1b23 | ||
|
|
3bdc47629c | ||
|
|
a155c0d122 | ||
|
|
1e98d476f7 | ||
|
|
dd0331bb98 | ||
|
|
ee6041257f | ||
|
|
d52b23d546 | ||
|
|
c8322529cb | ||
|
|
d20f3913f6 | ||
|
|
585e5fc24c | ||
|
|
6d18fc4718 | ||
|
|
792567ac03 | ||
|
|
b4dfe31b25 | ||
|
|
44e7f322ff | ||
|
|
6dacd91ca5 | ||
|
|
2346465c90 | ||
|
|
a16500068c | ||
|
|
5761ce7399 | ||
|
|
55b88b2bad | ||
|
|
b522ce527d | ||
|
|
dbdc1891e1 | ||
|
|
6dd8e3e03a | ||
|
|
39998f4de6 | ||
|
|
b07dd055b4 | ||
|
|
ff48f38dbf | ||
|
|
ae9acce200 | ||
|
|
32bd018c4e | ||
|
|
deb475222c | ||
|
|
fdeb8a2d09 | ||
|
|
d28f71ef9f | ||
|
|
d49235f92a | ||
|
|
cfb30a45ee | ||
|
|
56d5aa29ba | ||
|
|
7187a9cfd7 | ||
|
|
51661ab127 | ||
|
|
a7e0e02945 | ||
|
|
259eab3701 | ||
|
|
a261e0c91b | ||
|
|
5fbf1af12b | ||
|
|
445d824848 | ||
|
|
6fc7b12d65 | ||
|
|
d00208a33d | ||
|
|
6c812f8e64 | ||
|
|
4fd44f1005 | ||
|
|
2b8a35d5a3 | ||
|
|
aaad019628 | ||
|
|
6a505f2d84 | ||
|
|
459cd40de2 | ||
|
|
558ff9f8dd | ||
|
|
c8606b37f5 | ||
|
|
d7ca18766e | ||
|
|
e83b0f0697 | ||
|
|
09f1080730 | ||
|
|
a131d5545e | ||
|
|
1404c38497 | ||
|
|
799be643b2 | ||
|
|
836b05383d | ||
|
|
e157638168 | ||
|
|
d32141e6ba | ||
|
|
8e49ebc020 | ||
|
|
cedbe03134 | ||
|
|
9a2277a756 | ||
|
|
08cc7b44c6 | ||
|
|
3f64691732 | ||
|
|
b19d24bef8 | ||
|
|
aa3d439471 | ||
|
|
de20b8693c | ||
|
|
e8fd8fea5b | ||
|
|
a1be3ce138 | ||
|
|
0f0756c448 | ||
|
|
656814d4fe | ||
|
|
ba33400fcd | ||
|
|
3159df1bf5 | ||
|
|
eff33506d2 | ||
|
|
a4f61cc65c | ||
|
|
7b77e5b1cb | ||
|
|
2a1e15d928 | ||
|
|
8108f5520e | ||
|
|
91621bc48b | ||
|
|
bb83ecdb37 | ||
|
|
1e7c5a1f19 | ||
|
|
6bbdbabc7b | ||
|
|
c9e59f8630 | ||
|
|
6a87800b95 | ||
|
|
9cd6cdce60 | ||
|
|
ea8239debf | ||
|
|
bab86f72f3 | ||
|
|
4b7946d012 | ||
|
|
f67176459a | ||
|
|
a4697b0fb9 | ||
|
|
4e2cd9aff7 | ||
|
|
0603113596 | ||
|
|
5c518cb457 | ||
|
|
e725e69726 | ||
|
|
486b7880c2 | ||
|
|
8a96ead2c8 | ||
|
|
7236967b16 | ||
|
|
6836a0794b | ||
|
|
6c6e92219a | ||
|
|
d5b3e52ed4 | ||
|
|
c7096947e0 | ||
|
|
ff11ee0e1c | ||
|
|
039a86697e | ||
|
|
80722a05e0 | ||
|
|
a11702f727 | ||
|
|
adc2fb7c3e | ||
|
|
9b3fed3fc1 | ||
|
|
42af90e39f | ||
|
|
281e756a78 | ||
|
|
559eee84d1 | ||
|
|
10a4a4bf7b | ||
|
|
a55a918239 | ||
|
|
e197cf8eb3 | ||
|
|
8e2737be4a | ||
|
|
0f28887062 | ||
|
|
0088235446 | ||
|
|
5d02e675ae | ||
|
|
eb762a0435 | ||
|
|
eae40fb37a | ||
|
|
6646cbdfac | ||
|
|
1c1469402a | ||
|
|
5592230aa3 | ||
|
|
1d43d9d78c | ||
|
|
ac13e0b6f2 | ||
|
|
019922fd8f | ||
|
|
4f0246ab04 | ||
|
|
a51723cd76 | ||
|
|
ada9d81017 | ||
|
|
328c6f0aaf | ||
|
|
7f4f655099 | ||
|
|
618a94bbf3 | ||
|
|
c843027967 | ||
|
|
5002dc38ad | ||
|
|
d525cf0eb3 | ||
|
|
29eebdf40a | ||
|
|
9093c4bd7f | ||
|
|
33dcbf88c1 | ||
|
|
a8e3515a05 | ||
|
|
723b5cef02 | ||
|
|
63940f4c49 | ||
|
|
c0ae45d3c3 | ||
|
|
23205ddd1b | ||
|
|
65372009fe | ||
|
|
da06b4fa94 | ||
|
|
c3cca6127a | ||
|
|
ece5afbc74 | ||
|
|
239fd01229 | ||
|
|
ebc6a8204d | ||
|
|
f20cea53b3 | ||
|
|
aa93392610 | ||
|
|
b86ce4320e | ||
|
|
750543ee14 | ||
|
|
c62a1b1a31 | ||
|
|
3c9d581ba0 | ||
|
|
dbf1920d18 | ||
|
|
7f1efbdb81 | ||
|
|
04b34c106e | ||
|
|
7d7488737b | ||
|
|
fbc9fdbddf | ||
|
|
d13843d3e6 | ||
|
|
06587d20a3 | ||
|
|
7b05763222 | ||
|
|
14a80cb167 | ||
|
|
e663b7e827 | ||
|
|
8effdb2e15 | ||
|
|
0f3566f990 | ||
|
|
c94658f083 | ||
|
|
1120bac840 | ||
|
|
c74c573a97 | ||
|
|
0dbeab8192 | ||
|
|
143b38ffca | ||
|
|
15ce29e7a3 | ||
|
|
c30805c103 | ||
|
|
ae12cd3710 | ||
|
|
80a81175c9 | ||
|
|
f3a3701397 | ||
|
|
700495771b | ||
|
|
6256f7b9fa | ||
|
|
769fcd5721 | ||
|
|
d03f845622 | ||
|
|
23388a8e9a | ||
|
|
4dd064dd36 | ||
|
|
c33e0c52d1 | ||
|
|
1a3cd3334e | ||
|
|
d08ae27779 | ||
|
|
7d64cc0dd7 | ||
|
|
dd0477f110 | ||
|
|
dc45b51430 | ||
|
|
801a75d7f3 | ||
|
|
2465e051dd | ||
|
|
07be7fb86e | ||
|
|
810a817e47 | ||
|
|
aafecd719e | ||
|
|
0da2f87505 |
@@ -1,11 +1,15 @@
|
||||
_Last synced: 2026-06-12 | Base commit: `2373704`_
|
||||
# Memory Index
|
||||
> **单一基准**: 此目录是唯一权威来源,其他位置的记忆以此为准同步。
|
||||
> _Last synced: 2026-09-10 | Base commit: `b5a8b83`_
|
||||
|
||||
# 记忆索引
|
||||
|
||||
| 文件 | 描述 | 类型 | 最后更新 |
|
||||
|------|------|------|----------|
|
||||
| project_overview.md | 项目技术栈、架构与模块结构 | project | 2026-05-14 |
|
||||
| project_progress.md | 各模块原型开发进度 | project | 2026-06-12 |
|
||||
| decisions.md | 关键设计决策与架构约定 | project | 2026-06-12 |
|
||||
| feedback.md | 协作规范与用户纠正记录 | feedback | 2026-06-12 |
|
||||
| user_profile.md | 用户角色与偏好 | user | 2026-04-21 |
|
||||
| 文件 | 描述 | 类型 | 更新日期 | 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 | — |
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
---
|
||||
name: 关键决策
|
||||
description: 关键设计决策与架构约定
|
||||
type: project
|
||||
last_updated: 2026-09-07
|
||||
commit: 891678d
|
||||
---
|
||||
|
||||
# 关键决策
|
||||
|
||||
## 卫生供应仓储页面区域选择层级约定
|
||||
- **决策**:`web-admin/nutrition` 的仓储相关页面统一采用三层规则:列表展示到格口明细、搜索区筛选停留在柜子/仓区级别、新增/编辑弹窗在柜类场景下下钻到格口级别。
|
||||
- **原因**:兼顾检索效率与录入精度,避免筛选项过细影响检索体验,同时保证录入时能准确落到具体格口。
|
||||
- **适用范围**:`sup-warehouse.html`、`sup-in-raw.html`、`sup-in-cleanin.html` 及后续同类仓储页面。
|
||||
- **日期**:2026-06-15
|
||||
|
||||
## 原型技术方案
|
||||
- **决策**:采用纯 HTML 自包含文件,不使用前端框架
|
||||
- **原因**:需兼容墨刀导入,禁止外部引用
|
||||
@@ -18,6 +32,14 @@
|
||||
- **决策**:每个模块维护 `doc/CHANGELOG.md` + `doc/PRD.md` + `doc/screenshots/`
|
||||
- **原因**:规范化模块文档管理,便于团队协作和评审
|
||||
|
||||
## 单位部门筛选控件
|
||||
- **决策**:所有筛选条件中的"单位部门"必须使用双列面板样式(左单位→右部门联动)
|
||||
- **原因**:用户明确要求统一该交互模式
|
||||
- **参考实现**:`web-admin/health-consult/history-expert.html` → `web-admin/weight-management/registration-tracking.html`
|
||||
- **结构**:`org-select-wrap` 容器包裹 `org-trigger`(触发器) + `org-panel`(双列面板),左侧 `orgUnitCol` 列单位,右侧 `orgDeptCol` 列部门
|
||||
- **交互**:点击触发器展开面板;选择左侧单位后右侧部门动态加载;选中部门后触发器文本更新为"单位 / 部门"
|
||||
- **日期**:2026-06-16
|
||||
|
||||
## 导航页设计状态标识
|
||||
- **决策**:SITE_MAP 中通过 `done: true` 字段标记模块设计完成状态
|
||||
- **三态**:待设计(无页面)/ 设计中(有页面无done)/ 已完成(有页面+done:true)
|
||||
@@ -33,12 +55,13 @@
|
||||
```css
|
||||
body {
|
||||
background: #05080f; /* 全屏深色画布 */
|
||||
min-height: 100vh; min-width: 1280px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 760px; min-width: 1320px; /* PAD(720+40padding)×(1280+40padding) */
|
||||
display: grid; place-items: center; /* 用 grid 居中,不用 flex(见下文 Why) */
|
||||
padding: 20px;
|
||||
}
|
||||
.device-frame {
|
||||
width: 1280px; height: 720px; /* PAD 横屏逻辑画布 */
|
||||
flex-shrink: 0; /* 兜底保护:即使在 flex 容器中也禁止被压缩 */
|
||||
border-radius: 24px; /* 硬件外壳圆角 */
|
||||
box-shadow: 0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04);
|
||||
overflow: hidden; /* 防止内容溢出滚动条 */
|
||||
@@ -53,10 +76,181 @@ body {
|
||||
- 所有 1280×720 主屏页面(餐线营养秤、档口营养秤、配比秤、净菜柜、毛菜秤等)均按此模板改造
|
||||
- 副屏(如 480×320 双屏小屏)复用现有 `.screen-frame` 并升级阴影/圆角即可,无需新增 `.device-frame`
|
||||
- 内部 `height: calc(100vh - X)` 必须改为 `height: calc(100% - X)`,避免视口与外壳尺寸耦合
|
||||
- **body 用 `display: grid; place-items: center`,不要用 flex**。2026-08-14 毛菜秤改造严重踩坑:`body { display:flex; justify-content:center; align-items:center; min-width:1280px; padding:20px }` 配合 `.device-frame { width:1280px; flex-shrink:0 }` 时,flex 算法在主轴计算上把 device-frame 的 hypothetical main size 算成 1648px(1280 + 神秘 368px),导致 `justify-content:center` 把 device-frame 反向定位到 offsetLeft=-48,横向滚动条出现,视觉上"完全偏左不居中"。即使 `flex-shrink:0` 也救不了——这只防止压缩,不防止 flex 容器对 item 尺寸的错误估算。改用 grid 后,scrollWidth=1600=viewport、offsetLeft=160 完美居中。**这跟之前 commit 2373704 餐线营养秤的 flex 方案表现不同**,怀疑与 Chromium 版本/页面其他元素 min-content 计算有关,但 grid 方案在所有页面都稳定。
|
||||
- **`.device-frame` 仍要加 `flex-shrink: 0`** 作为兜底(虽然 body 已改 grid,但留作子项保护不会出错)
|
||||
- **body `min-width` / `min-height` 必须把 padding 算进去**:`min-width: 1320px` (= 1280 + 40 padding),`min-height: 760px` (= 720 + 40 padding)。注意 `* { box-sizing: border-box }` 下 min-width 作用在 border-box,所以 padding 已包含在 1320 内,不会重复加。
|
||||
- PAD 内禁止可见滚动条(见 [[feedback.md#PAD 终端页面内禁止可见滚动条]])
|
||||
- **PAD 外壳改造的强制验证步骤**:改造完成后必须用浏览器实测
|
||||
1. `document.querySelector('.device-frame').clientWidth === 1280`
|
||||
2. `document.querySelector('.device-frame').getBoundingClientRect().left === (window.innerWidth - 1280) / 2`(居中验证)
|
||||
3. `document.body.scrollWidth === window.innerWidth`(无横向溢出)
|
||||
不能只靠代码审查。CSS flex/grid 子项宽度行为反直觉,代码看着对但实际塌缩/偏移很常见。
|
||||
|
||||
**See Also**:
|
||||
- [[feedback.md#PAD 终端页面内禁止可见滚动条]]
|
||||
- [[feedback_medical_pad_migration.md]](医疗点 PAD 沿用本外壳模式,尺寸变体 1280×800,壳内浅色背景)
|
||||
|
||||
- **日期**:2026-06-12
|
||||
- **首次落地**:餐线营养秤 9 页 + canteen-device/index.html(commit 2373704)
|
||||
|
||||
## 规格体系统一化改造(跨设备端 + Web 端 + 辅助页)
|
||||
|
||||
**结论**:设备端重量字段统一「克存储 + 展示层兼容显示斤」;切配规格 / 包装规格多形态字符串统一为结构化对象;建立跨页共享 `localStorage['dish_specs_v1']` + `nutri_sync_log_v1` 机制;分析页与字典页字段语义对齐权威字典。
|
||||
|
||||
**Why**:原状态字段散落各页面,重量单位(斤/克)混用、`cutSpec` / `packSpec` 既有 `"20mm×1mm×1mm · 细丝 · 平刀"` 字符串又有 `"500g"` `"大份800g"` 等多形态字符串,导致:
|
||||
- 数据沉淀(localStorage)口径不一致,跨页读写不可靠
|
||||
- 设备端复用同一食材数据需要重复换算
|
||||
- 营养质控同步逻辑只能"toast 假落地",无法真正写回餐品总库
|
||||
- 分析页字典语义与权威字典 (`spec_cut_shape` / `pack_method`) 错位
|
||||
|
||||
**How to apply**:
|
||||
- **存储层**:所有重量字段统一存「克」,展示层通过 `formatGram(g)` / `formatGramForCanteen(g)` 自动还原"斤"显示(如 2000g → "4 斤")
|
||||
- **字段结构化**:
|
||||
- `cutSpec`:字符串 → `{length, width, height, shape, technique}` 对象
|
||||
- `packSpec`:字符串 → `{baseGram, unit, portion?}` 对象
|
||||
- 兼容老数据:`parseLegacyCutSpec()` / `parseLegacyPackSpec()` 解析旧字符串
|
||||
- 渲染统一走 `formatSpec(specObj)` / `formatCutSpec(cutObj)`
|
||||
- **跨页共享**:
|
||||
- 餐品总库 `localStorage['dish_specs_v1']` 作为权威源
|
||||
- 营养质控同步日志 `localStorage['nutri_sync_log_v1']`(按时间倒序最多 100 条)
|
||||
- 缺省回退:`FALLBACK_DISH_DATA` / `FALLBACK_SYNC_DATA`
|
||||
- **字典语义对齐**:`spec_cut_shape`(丝/片/丁/块/段/末/泥/条 — 形状名词而非"切丝/切片"动作);`pack_method`(真空/气调/普通/冷鲜/保鲜盒/散装托盘)独立于 `packSpec`
|
||||
- **设备端墨刀兼容**:工具函数内联到每个 HTML 页面(不引入外部 JS)
|
||||
- **权威源参考**:`web-admin/nutrition/doc/spec-utils-reference.md`
|
||||
|
||||
**改造分阶段记录**(详见 `web-admin/nutrition/doc/规格体系统一化改造任务清单.md`):
|
||||
- 阶段一~三:Web 端档案/字典/数据沉淀类页面(已先期完成)
|
||||
- 阶段四(commit 4fd44f1):设备端 9 任务 14 文件全部完成
|
||||
- 阶段五(commit 4fd44f1):辅助页 3 任务 6 文件全部完成(质控真同步落地 + 字典语义修正 + 净菜进柜 spec 多形态结构化)
|
||||
|
||||
**See Also**:
|
||||
- [[feedback.md]](暂无直接关联条目,后续若新增规格体系相关规范在此补引用)
|
||||
|
||||
- **日期**:2026-08-05
|
||||
- **首次全阶段落地**:commit 4fd44f1
|
||||
|
||||
## 收银设备用户类型徽标三态体系
|
||||
|
||||
**结论**:收银设备(canteen-device/checkout-device)全端用户身份徽标统一为三态:会员(`member`)、临时(`temp`)、内部员工(`staff`),使用统一的 CSS 类名与配色,跨主屏(03-checkout/04-pay-success/05-orders/06-order-detail/07-statistics)与副屏(user-02-checkout-sync/user-03-paying/user-04-success)严格一致。
|
||||
|
||||
**Why**:原各页面只在 03-checkout 显示一个金色的"会员等级"标签,无法区分临时用户与内部员工,订单列表/详情层面看不到顾客构成不利于营收细分;且各页面分别实现样式,跨页视觉漂移。统一三态徽标后,订单列表可增加用户类型列、订单详情可独立呈现顾客信息卡,整套配色与文案可复用。
|
||||
|
||||
**How to apply**:
|
||||
- CSS 类名:`.user-type` 基类 + 修饰符 `.user-type--member` / `.user-type--temp` / `.user-type--staff`
|
||||
- 配色规则:
|
||||
- 会员:`linear-gradient(135deg, rgba(96,165,250,.28), rgba(251,191,36,.32))` + 金色 1px 边框 + `#fbbf24` 文字(可在后追加 `<small class="level">` 显示会员等级)
|
||||
- 临时:`rgba(148,163,184,.18)` 灰底 + 灰边框 + `#cbd5e1` 文字(无渐变、无阴影)
|
||||
- 内部员工:蓝渐变 `linear-gradient(135deg, rgba(96,165,250,.22), rgba(74,144,217,.14))` + 蓝边框 + 蓝文字
|
||||
- 数据约定:每条订单/顾客对象携带 `userType: 'member' | 'temp' | 'staff'`,跨页通过 URL 参数 `userType` / `userName` / `userCode` 传递(中文用户名 `encodeURIComponent`)
|
||||
- 配置表:每个页面保留本地 `USER_TYPE_CONFIG` 常量(文案 + className),保证跨页一致
|
||||
|
||||
**See Also**:
|
||||
- [[project_progress.md]] — 2026-08-10 里程碑条目记录落地范围
|
||||
|
||||
- **日期**:2026-08-10
|
||||
- **首次落地**:commit deb4752
|
||||
|
||||
## 补齐明细页业务路径修正:少记来源 = 未识别取餐记录
|
||||
|
||||
**结论**:补齐明细页(`08-reconcile.html`)的"少记加入"来源是**当前餐次中未匹配到人脸/账户的取餐记录**(营养秤/档口秤有重量但识别不到人的记录),而不是菜品库。多记处理维持"手动移除"。
|
||||
|
||||
**Why**:原方案弹出菜品库让收银员凭空选菜品并手填重量加入,违背真实业务路径——少记的真相是"取餐时秤具有数据但识别不到人",这些数据已经躺在系统的未识别池里,应该被作为可信来源直接挑选加入。让收银员从菜品库重新挑菜反而会引入"凭空捏造"风险且增加录入成本。
|
||||
|
||||
**How to apply**:
|
||||
- 「补齐未识别」Tab 顶部用黄色提示条说明用途:"未匹配到顾客的取餐记录,属漏记则点 + 加入"
|
||||
- 按来源分组(营养秤·餐线取餐 / 档口秤·档口取餐),每条记录携带 `recordId` / `time` / `source`,点击 `+ 加入` 直接进入左侧订单标记"新加"
|
||||
- 用 `addedRecordIds: Set` 跟踪已加入记录避免重复,加入后按钮变 `✓ 已加`;左侧订单删除该新加项时自动恢复未识别列表的 `+` 按钮
|
||||
- **不要**让收银员从菜品库选菜——重量来自秤具本身,不需要重新输入
|
||||
- 多记处理仍走"左侧订单表格化展示 + 移除/恢复/删除三按钮"模式
|
||||
|
||||
**See Also**:
|
||||
- [[project_progress.md]] — 2026-08-10 里程碑条目记录补齐明细页重构细节
|
||||
|
||||
- **日期**:2026-08-10
|
||||
- **首次落地**:commit deb4752
|
||||
|
||||
## 待机页副屏人脸识别监控区(收银员侧实时反馈)
|
||||
|
||||
**结论**:收银员屏待机页(`02-standby.html`)左下区域由原「开始结算」按钮改为「副屏人脸识别监控区」,展示副屏识别状态(三态:成功/光线不足/位置不对)+ 模拟副屏画面(人形剪影 + 检测框 + LIVE 实时指示)。结算流程入口由副屏识别事件自动驱动,不再由收银员手动点击触发。
|
||||
|
||||
**Why**:
|
||||
- 原「开始结算」按钮与"副屏识别→自动加载订单"的实际业务路径重复,容易让收银员误以为需要主动触发结算
|
||||
- 副屏识别成功率受光线/站位影响,但收银员屏此前没有对应的实时反馈区,无法主动提示顾客"请正对摄像头""请补光"等引导
|
||||
- 收银员是识别失败的最终处理人(决定何时提示顾客调整),必须在收银员屏有显式的状态可视化
|
||||
|
||||
**How to apply**:
|
||||
- 三态统一用 `face-monitor--{success|warning|info}` 修饰符 + `border-color` + `box-shadow` 联动
|
||||
- `success` 识别成功:绿框 + ✅ 徽标 + 脱敏用户名(如"张*明")+ 会员等级/工号
|
||||
- `warning` 光线不足:黄框 + 💡 徽标 + "环境亮度低于识别阈值,请补光" + `faceWarnBlink` 闪烁动画
|
||||
- `info` 位置不对:蓝框 + 🎯 徽标 + "请顾客正对摄像头并保持 50-80cm 距离" + 扫描线动画(`scanMove` 2.2s 循环)
|
||||
- 模拟副屏画面固定 200×140px 黑色摄像头视角块:用 `radial-gradient` 实现人形剪影 + `border-dashed` + 四角 `.face-preview__corner` 强调检测框
|
||||
- 原型演示用 4 秒轮播;**后端真实场景下应替换为副屏识别事件驱动**(状态变化由 WebSocket/SDK 推送,而非定时器)
|
||||
- 时钟区相应精简:年月日已由右上角 `topMeta` 承担,左下时钟区只显示「时分秒 + 餐次标签」,餐次按当前小时自动切换(早餐 6-10 / 午餐 10-14 / 晚餐 16-21 / 其他时段"欢迎光临")
|
||||
- 底部入口区原「🖨️ 小票打印」改为「🍽️ 用餐管理」(功能待开发,占位 `javascript:void(0)`)
|
||||
|
||||
**See Also**:
|
||||
- [[project_progress.md]] — 2026-08-10 第二批次里程碑条目记录待机页重构范围
|
||||
|
||||
- **日期**:2026-08-10
|
||||
- **首次落地**:commit 32bd018
|
||||
|
||||
## 餐次管理模块业务路径与交互约定
|
||||
|
||||
**结论**:结算设备新增餐次管理子模块(10-13 四页),覆盖开餐→上餐/加餐/闭餐→结束用餐→余量处置→历史回溯的完整业务闭环。余量处置四选项(员工福利/捐赠/废弃/加工再利用)严格对齐后台营养管理 `prod-proc-surplus.html` 的处置方式体系;余量阈值三态(≤10% 红/≤25% 金/>25% 绿)驱动补餐预警;时间轴节点六类(开餐/上餐/加餐/闭餐/结束/处置)统一色编码。
|
||||
|
||||
**Why**:
|
||||
- 原待机页底部「餐次管理」按钮长期为 `javascript:void(0)` 占位,食堂现场人员缺少对餐次生命周期的管理工具
|
||||
- 餐次开关决定结算设备是否接受新订单,直接影响收银流程
|
||||
- 余量处置是食品安全与成本控制的关键环节,需与后台营养管理系统的处置方式体系统一,避免前后台数据口径不一致
|
||||
- 历史记录为食堂经营分析(餐次维度的人均消费/订单趋势/余量率)提供数据基础
|
||||
|
||||
**How to apply**:
|
||||
- **入口**:`02-standby.html` → `10-meal-center.html`(开餐开关 + 历史入口)
|
||||
- **开餐中**:`11-meal-active.html?meal=lunch` 支持餐线Tab切换(A/B/C)、餐品图文卡含余量进度条、通知补餐按钮(toast反馈+按钮态切换)、结束用餐二次确认弹窗
|
||||
- **余量处置**:`12-meal-end-surplus.html?meal=lunch` 复选框+批量处置方式下拉+处置方式变更联动下拉配色(福利蓝/捐赠绿/废弃红/再利用紫)+提交二次确认弹窗(显示处置汇总)→跳回入口页并触发Toast
|
||||
- **历史记录**:`13-meal-history.html?date=2026-08-09&meal=lunch` 左30%餐次列表(按日期分组+状态徽标)+右70%流程时间轴(渐变竖线串联六类节点)+底部统计(订单/营收/客单价/余量)
|
||||
- **URL参数驱动**:`meal`(breakfast/lunch/dinner)、`toast`(submitted)、`date`(历史跳转)
|
||||
- **餐次状态**:进行中(绿色脉冲)/已结束(灰色)/未开餐(占位)
|
||||
- **时间轴色编码**:开餐(绿▶)/上餐(蓝🍽)/加餐(金➕)/闭餐(灰✕)/结束(红⏹)/处置(紫♻)
|
||||
|
||||
**See Also**:
|
||||
- [[project_progress.md]] — 2026-08-10 第三批次里程碑条目记录餐次管理落地范围
|
||||
|
||||
- **日期**: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
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
name: 协作规范
|
||||
description: 用户纠正与确认的协作规范(含 Why + How to apply)
|
||||
type: feedback
|
||||
last_updated: 2026-09-10
|
||||
commit: b5a8b83
|
||||
---
|
||||
|
||||
# 协作规范
|
||||
|
||||
## Agent 行为约束
|
||||
@@ -10,6 +18,14 @@
|
||||
|
||||
## 用户纠正记录
|
||||
|
||||
### 2026-06-15 | 仓储页面区域筛选层级约定
|
||||
- **纠正**:在 `web-admin/nutrition` 仓储相关页面中,用户明确要求列表可以展示到格口明细,但搜索区筛选不能下钻到格口;只有新增/编辑弹窗在柜类场景下才下钻到格口级别。
|
||||
- **强制规则**:
|
||||
1. 列表展示:柜类存储方式使用「柜号-格口」明细格式,仓库类使用仓区格式
|
||||
2. 搜索区筛选:`所在区域` 只联动到柜子/仓区级别,不展示格口
|
||||
3. 新增/编辑弹窗:柜类场景下 `所在区域` 下钻到格口级别,仓库场景下选择仓区
|
||||
4. 适用页面:`sup-warehouse.html`、`sup-in-raw.html`、`sup-in-cleanin.html` 及后续同类仓储页面
|
||||
|
||||
### 2026-06-05 | 禁止自动提交代码
|
||||
- **纠正**:Agent 在完成任务后自动执行 `git commit`,未经用户允许。
|
||||
- **强制规则**:**禁止自动提交(commit)代码,只有在用户明确说"提交"或"commit"时才能执行 git commit。** Agent 完成文件修改后应报告结果,等待用户指令再提交。
|
||||
@@ -42,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×800(16:10)**,替代项目背景.md 原规定的 1024×768(用户认为 4:3 比例"矮胖"不协调)。深色画布 + 居中 device-frame(grid),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]]。
|
||||
@@ -2,13 +2,12 @@
|
||||
name: 记忆健康检查报告
|
||||
description: memory-lint 最新一次执行的检查结果与待处理项
|
||||
type: lint
|
||||
last_updated: 2026-06-12
|
||||
commit: 2373704
|
||||
last_updated: 2026-09-10
|
||||
---
|
||||
|
||||
# 记忆健康检查报告
|
||||
|
||||
> _执行时间: 2026-06-12 | Base commit: `2373704` | Last synced: 2026-06-12_
|
||||
> _执行时间: 2026-09-10 | Base commit: `b5a8b83` | Last synced: 2026-09-10_
|
||||
|
||||
## 健康概览
|
||||
|
||||
@@ -16,31 +15,42 @@ commit: 2373704
|
||||
|--------|------|----------|------------|
|
||||
| 孤儿检测 | ✅ | — | — |
|
||||
| 幽灵检测 | ✅ | — | — |
|
||||
| 引用存在性检测(3A) | ✅ | — | — |
|
||||
| 双链对称性检测(3B) | ✅ | — | — |
|
||||
| 引用存在性检测(3A) | ✅ | 1 已修复 | — |
|
||||
| 双链对称性检测(3B) | ⚠ | — | 1 待处理(遗留) |
|
||||
| 内容矛盾检测 | ✅ | — | — |
|
||||
| 过期检测 | ⚠ | — | 1 待处理 |
|
||||
| 可推断内容污染 | ✅ | — | — |
|
||||
| 过期检测 | ✅ | 2 已刷新 | — |
|
||||
| 可推断内容污染 | ⚠ | — | 1 待处理(遗留) |
|
||||
| frontmatter 完整性 | ✅ | 2 已补齐 | — |
|
||||
|
||||
**AUTO-FIX 已执行 0 项 | NEED-HUMAN 待处理 1 项**
|
||||
**AUTO-FIX 已执行 5 项 | NEED-HUMAN 待处理 2 项(均为前期遗留)**
|
||||
|
||||
---
|
||||
> 本次为 2026-09-10 sync(891678d → 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`(补 frontmatter,last_updated 刷新为 2026-09-10,消除 116 天过期 ERROR)
|
||||
- [x] frontmatter 补齐 + 过期刷新:`user_profile.md`(补 frontmatter,last_updated 刷新为 2026-09-10,消除 139 天过期 WARN)
|
||||
|
||||
## NEED-HUMAN 待处理清单
|
||||
|
||||
### [WARN] 过期记忆
|
||||
### [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 不自动修改」
|
||||
- **建议**:下次人工编辑该章节时顺手补一行回引
|
||||
|
||||
- **文件**:`user_profile.md`(last_updated: 2026-04-21,已过期 52 天)
|
||||
- **建议**:核对用户角色与偏好是否仍准确;本次会话未涉及用户画像变化,可在下次会话有相关线索时更新
|
||||
### [WARN] 可推断内容污染 — project_progress.md(遗留)
|
||||
- **文件**:`project_progress.md`「近期里程碑」区段
|
||||
- **问题**:里程碑条目含具体文件路径与 commit hash,按「Git 历史可查不应写入记忆」规则属边缘污染
|
||||
- **本次执行**:新增条目延续仓库既有的详细里程碑格式(属变更范围说明,且为跨会话恢复工作的关键索引),历史条目未清理
|
||||
- **建议**:若嫌冗长可人工压缩 2026-08 月以前的早期条目为一句话结论,近期条目保留细节
|
||||
|
||||
---
|
||||
|
||||
## 本次执行新增的双链
|
||||
## 交叉引用验证清单(本次新增/受影响部分)
|
||||
|
||||
- `decisions.md#餐厅终端设备 PAD 居中外壳设计模式` ↔ `feedback.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。
|
||||
@@ -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端 |
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: 项目进度
|
||||
description: 各模块原型开发进度与近期里程碑
|
||||
description: 各模块原型开发进度与里程碑
|
||||
type: project
|
||||
last_updated: 2026-06-12
|
||||
commit: 2373704
|
||||
last_updated: 2026-09-10
|
||||
commit: b5a8b83
|
||||
---
|
||||
|
||||
# 项目进度
|
||||
@@ -16,15 +16,20 @@ commit: 2373704
|
||||
- `operator/` — 操作人员端(22 页:工作台15 + 知识库2 + 个人中心5)✅
|
||||
- `professional/` — 专业人员端(13 页:工作台7 + 知识库2 + 个人中心4)✅
|
||||
- `onsite/` — 驻场人员端(12 页:工作台6 + 知识库2 + 个人中心4)✅
|
||||
- `admin/` — 管理员端(18 页:工作台13 + 知识库2 + 个人中心3)🔨 设计中
|
||||
- `message/` — 消息/即时通讯(3 页,共享模块)✅
|
||||
- `ai-assistant/` — AI 助手(1 页,共享模块)✅
|
||||
|
||||
### 员工端 APP(employee-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/` — 体重管理(10 页)✅
|
||||
- `weight-main.html` — 体重管理主页(英雄区/三态卡片/吃动平衡/体重记录/排行榜/推荐视频)
|
||||
- `weight-management/` — 体重管理(29 页)✅
|
||||
- `weight-main.html` — 体重管理主页(三态切换/英雄区/三态卡片/吃动平衡/体重记录/排行榜/推荐视频)
|
||||
- `goal-commitment.html` — 目标承诺签订(活动/个人双类型+URL参数状态同步)
|
||||
- `goal-management.html` — 目标管理(7种状态多卡片显隐+自我/活动目标切换)
|
||||
- `basic-info.html` — 基本信息
|
||||
@@ -34,6 +39,23 @@ commit: 2373704
|
||||
- `promise-signing.html` — 承诺签订(横屏)
|
||||
- `my-promise.html` — 我的承诺书(证书风格)
|
||||
- `balance-history.html` — 吃动平衡历史(月视图日历+柱状图)
|
||||
- 新增 13 页:`diet-advice.html`、`eat-checkin.html`、`eat-history.html`、`exercise-advice.html`、`exercise-checkin.html`、`exercise-history.html`、`history.html`、`index.html`、`report-detail.html`、`weight-checkin.html`、`weight-history.html`、`weight-ranking.html`、`weight-report.html`
|
||||
- `diet-advice.html` — 膳食建议(今日推荐摄入)
|
||||
- `eat-checkin.html` — 饮食打卡
|
||||
- `eat-history.html` — 摄入记录历史
|
||||
- `exercise-advice.html` — 运动建议(今日推荐运动量)
|
||||
- `exercise-checkin.html` — 运动打卡
|
||||
- `exercise-history.html` — 消耗记录历史
|
||||
- `weight-checkin.html` — 体重打卡
|
||||
- `weight-history.html` — 体重记录历史
|
||||
- `weight-ranking.html` — 排行榜
|
||||
- `weight-report.html` — 体重报告
|
||||
- `eat-checkin-v2.html` — 饮食打卡v2(重构版)
|
||||
- `weight-main-v2.html` — 体重管理主页v2(重构版)
|
||||
- `diet-advice-v2.html` — 膳食建议v2(重构版)
|
||||
- `eat-detail-v2.html` — 饮食详情v2(重构版)
|
||||
- `scan-checkin.html` — 扫码打卡
|
||||
- `weight-detail-v2.html` — 体重详情v2(重构版)
|
||||
- `health-monitor/` — 健康监测(20+ 页)🔨 设计中
|
||||
- `wearable/` — 穿戴设备监测(16 页:血氧/心率/压力/体温/步数/运动/睡眠/血压/心电+告警)
|
||||
- `weight/` — 体重监测(3 页,已独立为 `weight-management/` 模块)
|
||||
@@ -45,32 +67,140 @@ commit: 2373704
|
||||
- `health-consult/` — 专家咨询管理(50+ 页)✅
|
||||
- `emergency-dispatch/` — 应急就医管理(含 admin + operator 子模块,70+ 页)✅
|
||||
- `health-checkup/` — 健康体检管理 ✅
|
||||
- `nutrition/` — 营养管理(70+ 页:溯源双菜单、档案、种采、供应、生产、用餐、异常、食堂、数据、模型、终端、字典)🔨 设计中
|
||||
- `nutrition/` — 营养管理(75+ 页:溯源双菜单、档案、种采、供应、生产、用餐、异常、食堂、数据、模型、终端、字典、数据分析)🔨 设计中
|
||||
- 溯源模块重构:食材溯源/餐品溯源双菜单分离(trace-timeline 双维度搜索区独立)
|
||||
- `health-monitor/` — 健康监测管理(83 页)🔨 设计中
|
||||
- 数据分析子菜单组:`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`
|
||||
- 过程管理:`action-record.html`、`action-overview.html`、`action-overview-detail.html`、`activity-statistics.html`、`personnel-measure-record.html`(人员测量记录,2026-08-07 新增)、`personnel-overview-record.html`(人员概览记录,原 `history-personnel-list.html` 重命名)
|
||||
- 历史干预:`history-plan-list.html`、`history-plan-detail.html`、`history-personnel-detail.html`
|
||||
- 个人体重管理:`personal-target-data.html`、`personal-target-detail.html`
|
||||
- 设备管理:体重秤(`scale-*.html`×4)、蓝牙秤(`bluetooth-*.html`×4)、体脂仪(`fat-*.html`×4)、运动器械(`sport-*.html`×4)
|
||||
- 数据统计:`employee-data.html`/`employee-data-record.html`、`dept-stat.html`、`employee-stat.html`、`detail-data.html`、`weight-ranking.html`
|
||||
- 异常事件:`exception-system-dirty.html`、`exception-result-abnormal.html`
|
||||
- `health-monitor/` — 健康监测管理(40+ 页)🔨 设计中
|
||||
- `wearable/` — 穿戴监测(8 页:设备管理/绑定记录/用户数据/部门报告/部门统计/个人统计/异常事件/通知配置)
|
||||
- `weight/` — 体重监测(20 页:体重秤/蓝牙秤/体脂仪/运动器械各含设备管理+监测+明细+新增,员工数据/部门统计/员工统计)
|
||||
- `environment/` — 环境监测(25 页:室内/室外/花粉/水质各含设备类型+设备列表+数据监测+历史数据+APP记录+异常告警,花粉知识)
|
||||
- `health-checkup/` — 健康排查(7 页:问卷分类/模板/题目/评分/格言/部门记录/用户记录)
|
||||
- 根级页面(5 页:档案管理个人/单位/算法、异常事件脏数据/结果)
|
||||
- `frontline-medical/` — 一线医疗管理端 🔨 设计中(47 页,覆盖档案/基础配置/日常业务/床头呼叫/数据统计/健康小屋/异常事件七大分区;侧栏二级分组可展开收起;床头呼叫楼栋-宿舍-床位-设备四级管理;救助物品统计图表/报表双视图)
|
||||
|
||||
### 其他终端(other-module)
|
||||
- `mini-program/nutrition/` — 微信小程序营养管理用户端(27 页)✅
|
||||
- `canteen-device/` — 餐厅终端设备(20 设备类型 + 营养秤9页含双屏)🔨 设计中
|
||||
- `canteen-nutrition-scale/` — 餐线营养秤(9 页:导航/开餐/餐品展示/取餐单屏/取餐双屏大屏/取餐双屏小屏/设置/采样/历史)+ PRD + FLOW + CHANGELOG,已统一 PAD 居中外壳改造并挂载至根 SITE_MAP(done:true)
|
||||
- 父级 `canteen-device/index.html` 同步 PAD 居中外壳改造
|
||||
- 18 种其他终端设备原型页(结算终端/充值屏/净菜柜/食品安全监测/营养分屏/餐盘机/加工提示屏/比例秤/卫生检测/检测秤/原料柜/加工秤/清洗秤/档口营养秤/水电表等)⏳ 仅有占位
|
||||
- `emergency-screen/` — 应急大屏(西安/标准版/九厂)⏳ 待开发
|
||||
- `medical-pad/` — 医疗点PAD端 ⏳ 待开发
|
||||
- `mini-program/nutrition/` — 微信小程序营养管理用户端(41 页)✅
|
||||
- `meal-delivery-h5/` — 营养管理餐品配送 H5(配送员端 11 页:登录/工作台/派单来电/抢单池/订单详情[含自助取货/到店取货双弹窗]/配送中/完成签收/历史/个人中心)✅ + CHANGELOG;称谓统一"配送员",派单不设倒计时
|
||||
- `canteen-device/` — 餐厅终端设备(多设备实页持续补齐)🔨 设计中
|
||||
- `canteen-nutrition-scale/` — 餐线营养秤(11 页:含登录页、欢迎页,双屏取餐合并为单页 03-meal-pickup-dual 同页左右双屏布局)+ PRD + FLOW + CHANGELOG
|
||||
- `checkout-device/` — 结算设备(14 页:收银员屏 9 页覆盖登录/待机/收银/支付成功/订单/详情/统计/对账/小票 + 用户屏 4 页覆盖待机/同步/支付/成功 + 双屏导航入口)+ index 左右双屏布局;登录改为用户名密码方式
|
||||
- `clean-veg-packaging-labeler/` — 净菜包装秤(5 页:登录/订单/包装/历史/标签预览)+ CHANGELOG;术语由"贴标"统一为"包装",包装作业按单份拆分
|
||||
- `clean-veg-ratio-scale/` — 净菜组配秤(原净菜配比秤重构,纵向 3:4 布局 + 调料组配区 + 调料位置设置页)
|
||||
- `ratio-scale/` — 配比秤(含 04-seasoning-settings.html 调料位置设置页;调料组配区支持展开/折叠,应称/已称 X/Yg 格式)
|
||||
- `raw-veg-inspection-scale/` — 毛菜验收秤(8 页:登录/欢迎/验收/设置/历史/采样等)+ PRD + UI-SPEC + FLOW;2026-08-14 全页面完成 PAD 居中外壳改造(grid 方案),验收列表动态列(有订单 10 列/无订单 8 列),导航统一经登录页进入
|
||||
- `raw-veg-processing-scale/` — 加工毛菜秤(10 页:登录/欢迎/清洗任务/批次称重/批次清洗/历史/设置等);清洗任务支持新增弹窗与补录功能
|
||||
- `processing-prompt-screen/` — 后厨补餐屏(原"加工提示屏"更名,1 页首版:16 格菜品余量监控四态看板 + 补餐/食谱来源标签)
|
||||
- 父级 `canteen-device/index.html` 已恢复员工登录入口卡片并同步菜单状态;模块级 `doc/CHANGELOG.md` 已建立
|
||||
- 其余设备页仍有部分占位,待继续补齐
|
||||
- `emergency-screen/` — 应急大屏(西安/标准版/九厂)🔨 设计中
|
||||
- `xian-center/index.html` — 西安应急中心大屏(应急指挥大屏,含 KPI 指标/穿戴告警/心脑血管 AI 预警/地理地图/资源调度/急救路线;新增穿戴告警/心脑血管预警/应急求助三个业务记录弹窗含筛选与分页,地图资源标记弹窗,告警弹窗红色闪烁边框)
|
||||
- `medical-pad/` — 医疗点PAD端 ✅ 19 页(1280×800 横屏:登录/任务中心/日常诊疗/随访/慢病干预/健康宣教/员工档案/健康监测/床头呼叫/报告解读,抽屉交互页内实现;随访抽屉携带员工信息跳转 goFollow 带参回显、巡诊任务重构、删除五类人群登记页)+ CHANGELOG
|
||||
|
||||
## 待开发模块
|
||||
Web 管理端和员工端 APP 的其余模块(系统管理、运动管理、健康评估、知识普及、健康数据、心脑血管/糖尿病/癌症预防等)均为待开发状态。
|
||||
|
||||
## 总计
|
||||
- 已完成/设计中:550+ 个 HTML 原型页面
|
||||
- 覆盖模块:18 个(含三端+其他终端)
|
||||
- 已完成/设计中:900+ 个 HTML 原型页面
|
||||
- 覆盖模块:20 个(含三端+其他终端)
|
||||
|
||||
## 近期里程碑
|
||||
- 2026-09-10:文档技能包与模板规范完善(commit b5a8b83)——①截图规范统一:JPEG 质量 85% + 分终端目录(screenshots/{终端目录}/)+ 只截主界面/重要表单详情页(弹窗/筛选下拉不单独截图)+ Web 端全屏视口 1440×900(android-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` 同步支持身高秤设备切换。
|
||||
- 2026-08-20:体重管理新增体脂仪独立报告页并统一指标术语(commit 1e98d47)——新增 `body-fat-report.html` 体脂仪独立报告页,`report-detail.html` 统一指标术语,体重管理三设备(体重秤/蓝牙秤/体脂仪)报告指标命名对齐。
|
||||
- 2026-08-20:结算设备复秤对账 + 营养管理端报表中心两页面补齐(commit dd0331b)——①结算设备 `03-checkout.html` 新增复秤对账条(读秤对比记录/实际净重,超阈值警示 + 三态演示切换),`08-reconcile.html` 新增复秤对账横条 + 智能推荐卡片(漏记补录/多记移除组合方案 + 一键应用);②营养管理端补齐 `din-place-biz-analysis.html`(场所经营分析)与 `din-place-goal.html`(场所目标考核)两个报表页。
|
||||
- 2026-08-20:营养管理端营养用餐菜单新增报表中心子菜单(commit ee60412)——在「数据分析」下新增「报表中心」子菜单组(场所经营分析/场所目标考核),同步更新侧边栏与 SITE_MAP。
|
||||
- 2026-08-20:配送H5取货方式拆分 + 营养餐线管理重构 + 餐盘柜餐盘编码(commit c832252)——①`04-order-detail.html` 底部按钮拆为「📦 自助取货/🏪 到店取货」双按钮,新增 `04-order-detail-selfpickup-dialog.html`(智能货柜凭取货码开柜自取+拍照确认)与 `04-order-detail-storepickup-dialog.html`(门店现场取货+拍照留存)两个弹窗页,订单信息新增「取货方式」行,`01-home.html` 派单横幅文案「倒计时 25s」→「已等待 5 分钟」;②`din-canteen-line.html` 餐线管理重构(结算方式/状态列 + 收费模式按周 7×4 餐次矩阵设置 + 批量设置 + 餐线详情/设备概况弹窗);③`din-terminal.html` 餐盘柜格口新增餐盘编码字段(格口卡片/详情展示,格口分配原位改造+编码输入,解绑清空);④`din-user-mgmt.html` 会员权益弹窗新增餐盘编号列 + 手动绑定/解绑专属餐盘柜。
|
||||
- 2026-08-19:体重管理员工端/管理端全量弹窗截图重拍与 PRD 弹窗文档补全(commit d20f391)——员工端与管理端体重管理模块全部弹窗截图重拍,PRD 弹窗说明补齐。
|
||||
- 2026-08-18:体检管理端完善(commit 585e5fc)——列表新增序号/导出,检前专项配置页新增筛选/搜索/弹窗及人员选择功能。
|
||||
- 2026-08-18:订餐终端设备概览弹窗 + 会员权益/采购库存抵扣/追溯多锅次 + 设备端统计与验收改造(commit 792567a)——①`din-terminal.html` 新增设备概览大弹窗(Tab 按设备类型切换,智能货柜格口四态可视化 + 格口历史弹窗 + 分配/解绑);②`din-user-mgmt.html` 新增会员权益弹窗(等级权益概要 + 专属餐盘柜信息);③`farm-purchase.html` 待确认采购单库存列 + 一键抵扣(充足移除/部分抵扣);④`din-dish-own.html` 食材选择器改可搜索下拉面板;⑤`din-order-pickup.html` 重量偏差范围筛选 + 取餐详情字段补全;⑥`ano-result.html` 异常详情新增人脸识别失败/设备告警信息;⑦追溯双页(dish/ingredient)支持多锅次烹制分支(COOK-001/009/015 三分支并行)与净菜全流程节点操作人展示;⑧结算设备 `07-statistics.html` 新增收餐记录与余量处置记录弹窗;⑨毛菜验收秤 `01-verify-inspection.html` 实收规格拆分克重+单位可编辑。
|
||||
- 2026-08-14:毛菜验收秤 PAD 外壳改造 + 后厨补餐屏首版 + 配送H5流程降噪 + 营养档案检索录入强化(commit b4dfe31)——①毛菜验收秤全部 9 页统一 PAD 居中外壳(body grid 居中方案,详见 [[decisions.md#餐厅终端设备 PAD 居中外壳设计模式]]),验收页食材列表按"有订单 10 列/无订单 8 列"动态渲染(采购单价只读),返回导航统一 referer 参数"从哪里来回哪里去",导航页卡片统一指向登录页;②加工提示屏更名「后厨补餐屏」并完成首版设计(16 格菜品余量监控四态看板 + 补餐/食谱来源标签 + 餐次时段栏);③配送H5 移除派单倒计时与异常按钮、配送中页新增商家联系条/备注展示/清单卡片化;④营养管理餐品库高级搜索弹窗 + 筛选汇总条 + 操作历史 Tab、食材录入改行内选择器 + 净菜包毛利率字段、食材详情操作历史、蔬菜净菜类型结构化表格、进货明细补金额/单价列。新建 canteen-device 模块级 CHANGELOG。
|
||||
- 2026-08-14:体检模块完善 — 员工端胃肠镜流程 + 管理端配置页(commit 44e7f32,远程合入)——员工端新增 `my-appointments-detail-gastro.html` 与 `my-appointments-detail-gastro-done.html`(胃肠镜体检流程详情与完成态);管理端 `pre-hospital-scope`/`pre-item-combo-template`/`pre-item-exception`/`pre-item-hospital-fee`/`pre-item-unit-fee`/`pre-plan`/`pre-quota-group`/`pre-quota-personal` 各页新增筛选/弹窗等功能。
|
||||
- 2026-08-13:体检模块完善 — 员工端操作流程讲解页 + 管理端配置页(commit 6dacd91)——员工端新增 `process-guide.html`/`process-guide-detail.html`(体检操作流程讲解);管理端 `config-*` 系列配置页新增/完善功能;`index.html` 导航同步。
|
||||
- 2026-08-12:配送H5 新模块 + 净菜三页物流改造 + 餐次历史内聚(commit 2346465)——①新建 `other-module/meal-delivery-h5/`(配送员端 9 页:登录/工作台/派单来电/抢单池/订单详情/配送中/完成签收/历史/个人中心 + CHANGELOG);②净菜三页(`prod-clean-order`/`sup-clean-internal`/`sup-clean-external`)物流列简化为徽标 + 物流详情弹窗(4 节点配送时间轴),收货码仅买家视角可见;③结算台 `07-statistics.html` 将餐次历史内聚为统计页 Tab。
|
||||
- 2026-08-12:健康画像与历史咨询摘要 + 专家统计明细(commit 5761ce7、a165000)——①员工端新增 `profile/health-profile.html`(健康画像);②应急专家端新增 `history-summary.html`(历史咨询摘要),咨询结束页增加 AI 摘要卡片;③西安应急大屏 `help-emergency.html` 增加生活习惯画像;④Web 管理端 `expert-statistics.html` 增加咨询记录明细弹窗。
|
||||
- 2026-08-12:毛菜加工秤与配比秤迭代(commit dbdc189、b522ce5、55b88b2)——①加工毛菜秤 `01-washing-tasks.html` 新增任务新增弹窗(重新排版)与补录功能;②配比秤调料组配区增加展开/折叠开关,应称/已称改为 X/Yg 格式,`04-seasoning-settings.html` 调料位置设置交互优化。
|
||||
- 2026-08-12:营养管理杂项 + 结算台登录改版(commit 39998f4、b07dd05)——①消费管理支持退款(`din-data-consume.html`);②净菜订单支持选择内配调料;③采供管理增加待确认状态(`farm-purchase.html`);④结算台登录改为用户名密码方式(`01-login.html`)。
|
||||
- 2026-08-10:结算设备餐次管理模块新增 — 开餐/补餐/闭餐/余量处置/历史四页联动(commit ff48f38)——①新增 4 个页面:`10-meal-center.html`(餐次管理入口,左右双卡/开餐开关+历史入口)、`11-meal-active.html`(开餐中详情,餐线Tab+餐品图文卡+余量阈值三态+通知补餐+结束用餐二次确认)、`12-meal-end-surplus.html`(余量处置引导,全选+批量处置方式下拉+表格化余量列表+提交二次确认,四选项对齐后台营养管理余量处置体系)、`13-meal-history.html`(历史记录,左餐次列表+右流程时间轴六类节点+底部统计);②`02-standby.html` 底部入口卡片由「用餐管理」改名为「餐次管理」并跳转 `10-meal-center.html`;③`index.html` 收银员屏区追加 4 张导航卡片。(相关决策:[[decisions.md#餐次管理模块业务路径与交互约定]])
|
||||
- 2026-08-10:结算设备待机页新增副屏人脸识别监控区 + 多页面冗余元素精简(commit 32bd018)——①`02-standby.html` 左下「开始结算」按钮区改为「副屏人脸识别监控区」三态轮播演示(成功/光线不足/位置不对),含模拟副屏画面(人形剪影+检测框+LIVE 指示)与时钟精简、底部「小票打印」入口改名为「用餐管理」(功能待开发占位);②`03-checkout.html` 删除顶栏「本单应付」、余额信息卡「会员等级」、清空按钮及相关 CSS/JS;③`07-statistics.html` KPI 对比文案统一为「较昨日午餐」、支付方式柱状图标签语义化(余额支付/现金支付/餐券抵扣);④`08-reconcile.html` 删除差额汇总「企业补贴优惠」行;⑤`user-02-checkout-sync.html` 删除热量环「略少 76%」提示;⑥`user-04-success.html` 删除二维码「今日已有 1,284 人查看」营销文案。(相关决策:[[decisions.md#待机页副屏人脸识别监控区(收银员侧实时反馈)]])
|
||||
- 2026-08-10:收银设备新增用户类型徽标体系 + 补齐明细页重构(commit deb4752)——①主屏(03/04/05/06/07)与副屏(user-02/03/04)统一用户类型徽标三态配色(会员=蓝金渐变金边 / 临时=灰底 / 内部员工=蓝底),订单列表(05)新增「用户类型」列、订单详情(06)新增独立顾客信息卡片并清理语义陈旧的"顾客类型=企业员工"项;②订单详情(06)移除「补齐明细」操作入口(支付后改走退款流程,避免操作歧义);③补齐明细页(08)重构:左侧订单由卡片堆叠改为表格化(对齐 03-checkout),「添加商品」Tab 重构为「补齐未识别」从未匹配取餐记录直接加入(业务路径修正:少记来源=未识别记录而非菜品库);④导航页"结算设备"改名为"结算台"。(相关决策:[[decisions.md#收银设备用户类型徽标三态体系]]、[[decisions.md#补齐明细页业务路径修正:少记来源 = 未识别取餐记录]])
|
||||
- 2026-08-07:体重管理 Web 端新增人员测量记录页 + 多页面同步调整(commit fdeb8a2)——新增 `personnel-measure-record.html`(人员测量记录);原 `history-personnel-list.html` 重命名为 `personnel-overview-record.html`(人员概览记录);过程管理多页面(行动记录/总览/总览详情/活动统计)字段与交互调整;档案/设备/异常等多页同步微调。
|
||||
- 2026-08-06:结算设备全流程原型补齐 + 餐线营养秤双屏取餐合并(commit 51661ab)——结算设备新增收银员屏与用户屏两组共 13 个页面,覆盖登录→待机→收银→支付→订单/详情/统计/对账/小票 完整业务闭环与对应的用户侧待机/同步/支付/成功状态;导航页重构为左右双屏(用户屏/收银员屏)布局。餐线营养秤将原双屏大屏页与双屏小屏页合并为单页双屏布局(同页左右两区)。营养秤 5 个二级页统一返回按钮 history.back() 兜底逻辑、菜品展示与单屏取餐页样式微调。
|
||||
- 2026-08-06:食材档案新增「基础定价」字段 + 净菜订单按餐次核算人数与「生成中」状态(commit 445d824)——食材档案新增"基础定价(元/100g)"字段,列表/详情/新增/编辑/复制三形态同步;净菜订单由食谱快速创建订单弹窗重构(每餐次独立人数输入/预定配送时间字段/份数按餐次累加/校验逻辑)、由食谱创建的订单初始状态置为「生成中」并完成全链路(青色脉冲动效/聚合规则/筛选/统计卡片/流转入口);餐品档案定价区继续微调对齐。
|
||||
- 2026-08-05:餐线营养秤菜品展示页新增视频演示模式 + 净菜订单/申领三页统一订单金额字段(commit d00208a)——①`canteen-nutrition-scale/02-dish-display.html` 新增演示模式切换 pill(常规/视频)与视频模式布局(上视频+下信息卡,680px 16:9 视频播放器含播放按钮/控制条,下方菜品名+营养指标信息卡),通过 `main-area.mode-video` 状态类控制 CSS 布局切换;②`web-admin/nutrition` 净菜三页(`prod-clean-order`/`sup-clean-external`/`sup-clean-internal`)统一增加订单金额字段:订单明细 `lines[]` 每项新增 `price` 与 `subtotal` 字段、列表表头新增「订单金额」列、弹窗移除「汇总预览」表格改为底部灰底「订单总金额」栏(`#orderTotalAmount`/`#extTotalAmount`/`#claimTotalAmount`)、新增 `calcOrderTotal(o)` 与 `formatMoney(n)` 工具函数(`¥1,234.5` 千分位 + 最多 2 位小数)。
|
||||
- 2026-08-05:规格体系统一化改造阶段四(设备端)+ 阶段五(辅助页)全部完成(commit 4fd44f1)——阶段四覆盖设备端 9 任务 14 文件:①毛菜验收秤 `verifyType` 字段(weight/count)+ UNIT_OPTIONS 分类;②毛菜加工秤 4 页存储层斤→克、`formatGram` 展示层兼容、`yieldRate` 字段、04-history 语法错误修复;③净菜加工秤设置页删除"重量单位"select、滚刀块 cm→mm;④净菜包装贴标秤 3 页(最复杂)5 个工具函数内联、`cutSpec`/`packSpec` 全部结构化、`portions`→`packSize` 重命名;⑤净菜组配秤 3 页工具函数内联 + `localStorage['dish_specs_v1']` 跨页共享 + `resolvePackSpec` 兜底。阶段五覆盖辅助页 3 任务 6 文件:①营养质控 3 页(confirmSync 真落地写入餐品总库、菜品下拉跨页共享、同步日志驱动);②分析页字典语义修正(`sup-process-analysis` 切配线"切丝"→"丝"形状名词、新增独立 `pack_method` 字典;`sup-biz-analysis` 投料/产出吨→kg 对齐);③净菜进柜 `prod-proc-cabinet` 的 `spec` 多形态字符串结构化(含决策点 #10:`'6份/箱'` 理解为 6 个份型单位装一箱)。新增权威源 `doc/spec-utils-reference.md`、改造任务清单与研发同步说明。阶段四+五完成标志着规格体系统一化五个阶段全部完成。
|
||||
- 2026-08-05:体重管理新增 `eat-exercise-detail-v2.html`(饮食运动详情 v2);西安中心应急大屏迭代至 `index-v4.html`。
|
||||
- 2026-08-06:营养管理后台「批量充值」从 `din-user-mgmt.html` 弹窗抽离为独立原型页 `din-user-recharge.html`(用户筛选/候选用户表/充值配置区/汇总栏),原页面清理相关 CSS/JS/HTML 共 ≈800 行,操作栏「批量充值」按钮改为 `<a href>` 跳转;`index.html` SITE_MAP 在「用户管理」后追加入口。同日 `din-user-account.html` 充值弹窗支付方式精简为微信/现金/支付宝三种;小程序 `me-account.html` 姓名行增加可编辑态(与手机号行一致)。
|
||||
- 2026-08-05/06:营养管理后台餐品档案定价区与成本核算交互改造——`din-dish-add/own/all.html` 三页同步:餐品定价区去特殊化(原蓝色渐变卡片改为普通 `.info-section`)、定价字段精简为「称重基础价/利润率/食材成本/成本核算按钮」、新增「🧮 成本核算」按钮 + `calcCostPrice()` 计算「每百克成本 = 食材成本 ÷ 熟重 × 100」+ 小问号明细浮窗 + 「应用到称重基础价」按钮;四大项营养指标样式精简(四色渐变改为统一浅灰);移除「营养等级」字段。`din-dish-add.html` 修复「成本核算」按钮无响应问题(增加演示数据 fallback)。
|
||||
- 2026-08-04:营养管理后台餐品档案 4 项核心改造同步落地——①宣传视频多视频上传(`promoVideoEnabled` boolean 字段废弃,改用 `promoVideos: [{name,size,enabled}]` 数组);②营养指标自动计算(由食材清单累加,新增 `recalcAddNutri()`,4 大卡 + 16 项折叠);③烹饪工艺精简(删除「用油量/用盐量/出品量」3 字段,步骤数 9→6);④餐品定价独立分区(DISHES 数据新增 `pricing: {basePrice, ingredientCost, per100gCost, profitRate}` 嵌套对象)。
|
||||
- 2026-08-04:营养管理 + 卫生供应「数据分析」板块新增——营养用餐侧边栏新增「数据分析」子菜单组(经营分析 `din-biz-analysis.html` + 餐品分析 `din-meal-analysis.html`);卫生供应侧边栏同步新增「数据分析」子菜单组(加工经营分析 `sup-biz-analysis.html` + 加工分析 `sup-process-analysis.html`)。同步更新全部 29 个营养用餐 + 8 个卫生供应分组页面侧边栏及 `index.html` SITE_MAP。
|
||||
- 2026-08-03:营养管理后台新增「自有餐品库」二级菜单——在营养用餐侧边栏"餐品总库"与"算法标准"之间新增 `din-dish-own.html`(自有餐品库),页面内容暂复制自餐品总库后续将做差异化,同步更新全部 26 个营养用餐分组页面的侧边栏导航及 `index.html` SITE_MAP。
|
||||
- 2026-07-24:营养管理后台主子表重构——净菜订单/外售/内供三页面数据模型全面重构为主子表(一单多品),表格展开显示菜品明细,弹窗重构为动态明细行构建器(支持多菜品添加/删除/联动/汇总),确认收货改为行级逐条确认;采购管理新增验收数量列及验收明细弹窗(逐项校验);净菜进柜/组配/烹制新增关联订单列与订单进度展示;净菜加工移除质检结果列。小程序端订单详情新增支付信息卡片(含支付方式/流水号/时间)与餐线订单渲染;我的订单新增餐线订单卡片与Tab切换过滤(全部/待取餐/待配送/已完成)。体重管理新增 diet-advice-v2 膳食建议v2、eat-detail-v2 饮食详情v2、scan-checkin 扫码打卡、weight-detail-v2 体重详情v2 四个重构版页面。
|
||||
- 2026-07-23:营养管理后台与小程序多项增强——后台:场所新增/编辑页重构(营业时段/场所图片/资质证照 5 卡片区)、食材与餐品总库新增分类管理弹窗(树形增删改查)、食材新增/编辑页支持从已有食材复制;小程序:净菜包订单按状态差异化展示(待配送/配送中/已完成)、履约信息时间选择器重构为日期+时段联动下拉面板、新增收货地址管理页 me-address.html、健康档案移除营养目标分区;同步更新 CHANGELOG 及导航页。
|
||||
- 2026-07-21:导航页 popover 事件冒泡回归再次修复(同一根因第二次出现)——0603113 提交将 popover 重构为延迟渲染模式(首次点击"X 页"才动态构建 DOM),但 `togglePop` 函数内 `insertAdjacentHTML` 模板漏加 `onclick="event.stopPropagation()"`,导致同样的双 tab 打开回归。修复方式:在延迟构建模板中补上 stopPropagation(commit f671764)。系统性教训:嵌套 onclick 结构中子容器的 stopPropagation 在代码重构时极易遗漏,后续涉及 popover/弹层 DOM 创建位置变更时需检查事件冒泡拦截。
|
||||
- 2026-07-20:导航页 popover 事件冒泡回归修复——8a96ead 提交重构 popover 关闭逻辑时误删 `<div class="page-popover">` 上的 `onclick="event.stopPropagation()"`,导致 popover 内 `<a>` 点击事件冒泡到父级 `module-card` 触发 `window.open` 打开模块入口页,与 `<a>` 默认跳转叠加造成点击"页面总览"等列表项时同时打开两个 tab。修复方式:恢复 popover 容器上的 stopPropagation,与新版本 document click 监听器(靠 `closest` 判断)兼容(commit 5c518cb)。
|
||||
- 2026-07:本月重要变更摘要——新增蓝牙体重秤小程序模块 `other-module/bluetooth-scale-miniprogram/`(11 页:登录/搜索设备/wifi连接/添加成员/设备成员/主页/数据详情/资料完善/帮助中心/帮助详情/资料);西安应急大屏 v3 系列重构与扩展(主屏 v3 重构+路径规划纵向逐步导航+体检雷达/穿戴统计数值带单位+体重管理今日维度、cv 副屏重构为穿戴告警监测+新增告警趋势/心脑血管预警/告警时段分布、新增 help-monitor 应急求助副屏、新增多个业务记录弹窗含筛选与分页、站点入口指向 v3);营养管理后台菜单用户操作权限划分;营养管理小程序原型页面调整(小程序购买净菜包+健康档案+加餐数据展示);体重管理 web-admin 多页面更新(archive-unit-report-detail、history-personnel-list/detail、plan-edit、action-record 等);健康监测模块更新。
|
||||
- 2026-06-26:餐厅终端设备净菜包装秤与组配秤迭代——净菜包装秤全模块完成“贴标”到“包装”的术语统一,包装作业页清单按 `packSize` 展开为单份、移除称重弹窗并改为直接读秤,新增搜索框与 `label-preview.html` 标签预览页;净菜组配秤任务/历史页按单份拆分展示,食材名称去除丝/段/片/块/液等后缀,组配作业页移除确认弹窗并优化底部按钮布局;同步删除根目录个人化全平台进度生成脚本 `scripts/generate-overview.js`。
|
||||
- 2026-06-25:营养边缘服务原型迭代 + 西安应急大屏 v2/v3 双版本——营养侧新增《缓存中转服务器产品需求说明书》及 `index.html` 导航入口;重构 `edge-dispatch-detail.html`(单卡片+接收轨迹抽屉+终端应用明细弹窗,含 3 台服务器与按服务器动态渲染的终端列表),`edge-alert-center.html`/`edge-dispatch-management.html`/`edge-report-management.html`/`edge-server-detail.html`/`edge-server-management.html` 统一补齐分页组件与列调整。应急大屏侧新增 `xian-center/index-v2.html`(路径规划 Tab + 统计胶囊悬浮化 + 图层位置调整)与 `xian-center/index-v3.html`(顶部 KPI 条 + 24h 趋势折线 + 横向条形图 + 柱状/玫瑰环形组合图 + 中央环形/类型列表组合图等图表化重构,全部纯内联 SVG);`emergency-screen/doc/CHANGELOG.md` 同步追加 3 条变更记录。
|
||||
- 2026-06-24:餐厅终端设备多模块迭代——净菜配比秤重构为净菜组配秤(纵向 3:4 布局 + 调料组配区 + 调料位置设置页 `04-seasoning-settings.html`);新增净菜贴标秤模块 `clean-veg-packaging-labeler/`(登录/订单/贴标/历史 4 页),术语由"吐标"统一为"打印",贴标秤与组配秤均补齐净菜类型字段;毛菜验收秤 PRD 更新。
|
||||
- 2026-06-24:穿戴监测与应急大屏修正——`wearable/exception-event.html` 告警记录页字段优化 & 事件监听菜单展开箭头修复;西安应急大屏地图右下角接听状态标签修正。
|
||||
- 2026-06-24:营养管理边缘服务原型补齐——新增 `edge-server-management.html`、`edge-server-detail.html`、`edge-report-management.html`、`edge-dispatch-management.html`、`edge-dispatch-detail.html`、`edge-alert-center.html` 6 个页面;`index.html` SITE_MAP 同步新增边缘服务入口并增加项目资料入口;`_templates/md-viewer.html` 支持 Mermaid 文档预览;`web-admin/nutrition/doc/CHANGELOG.md` 已补充记录。
|
||||
- 2026-06-24:西安应急中心大屏业务弹窗与地图交互增强——三个业务记录弹窗(穿戴告警/心脑血管预警/应急求助)各含6项筛选条件(类型/日期/姓名/编号等)与分页列表(28条Mock数据);右上角告警弹窗红色脉冲闪烁边框动画;地图资源标记点击弹窗(11个资源信息卡片),普通Tab模式与急救路线模式点击分发;`emergency-screen/doc/CHANGELOG.md` 同步更新
|
||||
- 2026-06-22:营养小程序就餐记录与 web-admin 营养模块字段完善——小程序 `eat-home.html` 今日就餐记录列表移除食物图标、加序号徽标、副信息改为重量/热量、新增就餐时间;数据来源扩展为员工食堂/补充记录/拍照记录三种;底部"自记餐品"按钮拆为"手动补录"+"拍照记录"双按钮;新增 `eat-photo-record.html` 拍照记录页(餐品名称 + 拍照上传多条)。Web 管理端:`din-dish-add/din-dish-all` 食材"是否主料"字段改为"食材类型"(主材/辅材/调料);`prod-proc-combo/sup-in-cleanin/sup-in-raw/sup-pkg-label` 新增"终端设备"列展示设备名与编号;`din-recipe-mgmt` 新增"同步至生产任务"按钮与"已同步"标签。
|
||||
- 2026-06-18:营养模块净菜联动与小程序登录优化——小程序新增 `me-login.html` 微信授权登录页,并将“我”入口统一先跳转登录页;Web 管理端营养模块补强餐品/食材、净菜订单、净菜内供/外售、净菜入库、净菜包装等页面的“食材名称 + 净菜类型”联动与包装规格整数输入,对应模块 CHANGELOG 已补充记录。
|
||||
- 2026-06-23:应急大屏模块启动——新增西安应急中心大屏原型 `xian-center/index.html`(应急指挥大屏,深色科技风,含 KPI 指标卡/穿戴告警分布与事件滚动/心脑血管 AI 预警/地理地图多资源标记/急救路线与资源调度/急救提示书/应急求助事件滚动列表),`index.html` SITE_MAP 同步更新,`emergency-screen/doc/CHANGELOG.md` 初始化
|
||||
- 2026-06-18:体重管理 Web管理端异常事件页面补全 + 历史干预模块 + 个人目标详情——补全 `exception-system-dirty.html`(系统脏数据:筛选栏/操作栏/9列表格/分页/10条Mock数据)和 `exception-result-abnormal.html`(结果异常:员工维度筛选含单位部门联动/9列表格/分页/10条Mock数据),参考健康监测模块对应页面字段设计;新增 `history-plan-list.html`(历史干预计划列表)、`history-plan-detail.html`(计划详情)、`personal-target-detail.html`(个人目标详情);`activity-statistics.html`、`weight-ranking.html`、`detail-data.html`、`personal-target-data.html` 等页面大幅重构;全部 39 个页面顶部导航新增「历史干预」菜单;`index.html` SITE_MAP 同步更新;`health-monitor/exception-result.html` 移除异常等级筛选及列
|
||||
- 2026-06-18:营养模块净菜联动与小程序登录优化——小程序新增 `me-login.html` 微信授权登录页,并将"我"入口统一先跳转登录页;Web 管理端营养模块补强餐品/食材、净菜订单、净菜内供/外售、净菜入库、净菜包装等页面的"食材名称 + 净菜类型"联动与包装规格整数输入,对应模块 CHANGELOG 已补充记录。
|
||||
- 2026-06-17:餐厅终端设备净菜加工与配比秤流程迭代——净菜加工秤从验收语义调整为净菜加工,新增任务获取/自动轮询、未完成净菜卡片、累计加工、重量确认弹窗、任务截止/到达时间展示;配比秤详情增加订单类型标签并统一年月日时间格式。
|
||||
- 2026-06-16:体重管理 Web 管理端活动计划重构——活动计划列表、计划编辑、报名追踪、行动记录、行动总览/详情、目标体重审核等页面完成大幅重构,补充单位部门双列联动筛选并更新 `web-admin/weight-management/doc/CHANGELOG.md`。
|
||||
- 2026-06-15:卫生供应仓储管理联动增强——`sup-warehouse.html`、`sup-in-raw.html`、`sup-in-cleanin.html` 新增存储方式列;搜索区新增存储方式筛选,所在区域按柜子/仓区级别联动;新增弹窗在柜类场景下下钻到格口级别,仓库场景下选择仓区;对应 `web-admin/nutrition/doc/CHANGELOG.md` 已补充记录
|
||||
- 2026-06-15:体重管理模块扩展——新增饮食建议/打卡/历史、运动建议/打卡/历史、体重打卡/历史、排行榜、报告、报告详情、模块入口与通用历史页,模块页面数由 10 页扩展至 23 页
|
||||
- 2026-06-15:餐厅终端设备多模块补齐——新增或完善毛菜验收秤、加工毛菜秤、净菜配比秤、配比秤、餐线营养秤登录/欢迎/任务/历史/设置等页面,`canteen-device/index.html` 恢复员工登录入口卡片
|
||||
- 2026-06-12:餐线营养秤 + 餐厅终端导航页 PAD 居中外壳改造——9 个营养秤页面(含双屏大/小)统一引入 `.device-frame` 1280×720 PAD 外壳(圆角 24px + 阴影),body 改全屏深色画布 + flex 居中;副屏 480×320 复用并升级 `.screen-frame`;修正 `100vh` → `100%` 避免视口耦合;父级 `canteen-device/index.html` 同步改造并紧凑化内容(gap/padding/icon 缩减);根 SITE_MAP「其他终端 → 餐厅终端设备」标记 done:true 并挂载 9 页;新增 canteen-nutrition-scale/doc/CHANGELOG.md
|
||||
- 2026-06-12:体重管理模块独立——weight-main(主页/吃动平衡/体重记录/排行榜/推荐视频)、balance-history(月视图日历+柱状图)、promise-signing/my-promise、goal-management 七种状态卡片显隐控制完善、goal-commitment URL参数状态同步、SITE_MAP补全
|
||||
- 2026-06-11:餐厅终端设备模块新增——20种设备终端原型(营养称6页含PRD+FLOW、结算终端/充值屏/净菜柜/食品安全监测/营养分屏/餐盘机/比例秤/卫生检测/水电表等)、canteen-device/index.html导航页
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
name: 用户画像
|
||||
description: 用户角色与偏好
|
||||
type: user
|
||||
last_updated: 2026-09-10
|
||||
commit: b5a8b83
|
||||
---
|
||||
|
||||
# 用户画像
|
||||
|
||||
## 角色
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: android-capture
|
||||
description: 通过 Android 真机 + adb 无线调试截取原生 APP 实际页面截图(用于操作手册)。当用户要求「截取 APP 实际页面」「真机调试截图」「生成 XX 端 APP 操作手册截图」时使用。
|
||||
---
|
||||
|
||||
# Skill:Android 真机 APP 截图
|
||||
|
||||
## 触发方式
|
||||
|
||||
用户说「截取 XX APP 的实际页面截图」「真机调试截图」时触发。
|
||||
用于原生 Android / Flutter 混开 APP(iOS 同款只做 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. 安装 adb(Windows,curl 一键)
|
||||
|
||||
```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: 生成模块设计总览页
|
||||
|
||||
## 触发方式
|
||||
@@ -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. 文档中引用 ``、``
|
||||
|
||||
**界面原型截图**:
|
||||
- 沿用各端 `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
|
||||
- 员工端 APP:390×844(iPhone 14)
|
||||
- 医疗点 PAD:1280×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 导出规范」三原则,避免图文割裂、表行切割
|
||||
- 文档全部使用简体中文
|
||||
@@ -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)」**
|
||||
- 正文中「跳转到 XX(filename.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 合并表尤需保证合并单元整体不跨页切割
|
||||
- 文档全部使用简体中文
|
||||
@@ -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/` 存放
|
||||
- 截图文件名用中文页面名,与操作手册章节一一对应
|
||||
@@ -1,4 +1,16 @@
|
||||
node_modules/
|
||||
.idea/
|
||||
.idea/claudeCodeTabState.xml
|
||||
AGENTS.md
|
||||
AGENTS.md
|
||||
.claude/settings.local.json
|
||||
|
||||
# ZCode 本地工作目录
|
||||
.zcode/
|
||||
|
||||
# 临时文件(工具自动生成,下划线开头)
|
||||
_*.png
|
||||
_*.jpg
|
||||
_*.jpeg
|
||||
_*.gif
|
||||
_*.webp
|
||||
/交付文档/
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ClaudeCodeTabState">
|
||||
<option name="tabNames">
|
||||
<map>
|
||||
<entry key="1" value="RR" />
|
||||
</map>
|
||||
</option>
|
||||
<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-sonnet-4-6" />
|
||||
<option name="model" value="claude-opus-4-7[1m]" />
|
||||
<option name="permissionMode" value="bypassPermissions" />
|
||||
<option name="reasoningEffort" value="medium" />
|
||||
<option name="reasoningEffort" value="high" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
@@ -23,11 +19,11 @@
|
||||
<value>
|
||||
<TabSessionState>
|
||||
<option name="provider" value="claude" />
|
||||
<option name="sessionId" value="138df05e-0994-43f4-815e-ae3e2bce9d58" />
|
||||
<option name="sessionId" value="2b914133-b4ca-4fe5-8982-8bd381b51120" />
|
||||
<option name="cwd" value="$PROJECT_DIR$" />
|
||||
<option name="model" value="claude-opus-4-6" />
|
||||
<option name="model" value="claude-opus-4-7[1m]" />
|
||||
<option name="permissionMode" value="bypassPermissions" />
|
||||
<option name="reasoningEffort" value="medium" />
|
||||
<option name="reasoningEffort" value="high" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
@@ -35,28 +31,16 @@
|
||||
<value>
|
||||
<TabSessionState>
|
||||
<option name="provider" value="claude" />
|
||||
<option name="sessionId" value="147fc6bc-9ed3-4b0d-970d-0087ec03558c" />
|
||||
<option name="sessionId" value="9db55ee0-b91b-4abe-9d2f-5655a168223d" />
|
||||
<option name="cwd" value="$PROJECT_DIR$" />
|
||||
<option name="model" value="claude-opus-4-6" />
|
||||
<option name="model" value="claude-opus-4-7[1m]" />
|
||||
<option name="permissionMode" value="bypassPermissions" />
|
||||
<option name="reasoningEffort" value="medium" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="3">
|
||||
<value>
|
||||
<TabSessionState>
|
||||
<option name="provider" value="claude" />
|
||||
<option name="sessionId" value="56449601-4de2-4e73-b2ca-6b3ad0b2c6e0" />
|
||||
<option name="cwd" value="$PROJECT_DIR$" />
|
||||
<option name="model" value="claude-opus-4-6" />
|
||||
<option name="permissionMode" value="bypassPermissions" />
|
||||
<option name="reasoningEffort" value="medium" />
|
||||
<option name="reasoningEffort" value="high" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="tabCount" value="4" />
|
||||
<option name="tabCount" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,5 +1,4 @@
|
||||
<!-- Last updated: 2026-06-12 | Commit: 2373704 -->
|
||||
|
||||
<!-- Last updated: 2026-09-10 | Commit: b5a8b83 -->
|
||||
# 项目约束
|
||||
|
||||
本项目所有工作必须严格遵守 `项目背景.md` 中定义的全部规范,包括:
|
||||
@@ -52,9 +51,12 @@
|
||||
- `decisions.md` — 关键决策与架构约定,避免重复讨论已决事项
|
||||
- `feedback.md` — 协作规范与用户纠正,避免重犯已纠正的错误
|
||||
- `project_progress.md` — 当前阶段进度与待办,避免重复已完成工作
|
||||
- `project_overview.md` — 项目技术栈与三端架构,避免对项目结构产生误解
|
||||
3. **按需加载**(与当前任务相关时加载):
|
||||
- `project_overview.md` — 技术栈与架构(涉及架构讨论时)
|
||||
- `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` — 记忆健康检查报告(发现记忆体系存在待处理问题时)
|
||||
|
||||
### 记忆体系的更新与同步
|
||||
@@ -65,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 的默认推断。若记忆内容与当前代码状态冲突,以代码为准并更新记忆。
|
||||
|
||||
@@ -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>
|
||||
@@ -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 微前端(Monorepo:Turbo + 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>
|
||||
@@ -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 助手
|
||||
|
||||
### 功能说明
|
||||
|
||||
提供智能问答与健康建议能力,支持文本输入、常见问题推送、数据统计。
|
||||
|
||||
## 四、个人中心
|
||||
|
||||
### 功能说明
|
||||
|
||||
提供当前登录用户的基本信息查看与维护、账号设置等能力。
|
||||
|
||||
## 五、组织架构与权限
|
||||
|
||||
### 功能说明
|
||||
|
||||
平台统一维护单位/部门/人员组织架构,各子系统共享组织架构数据,按角色控制功能访问权限。
|
||||
@@ -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 | {各取值含义} |
|
||||
@@ -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 业务架构设计
|
||||
|
||||
{分层描述业务架构,自上而下四层:用户交互层 → 应用与服务层 → 数据整合层 → 数据服务层}
|
||||
|
||||
**业务架构图**:
|
||||
|
||||

|
||||
|
||||
### 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 Alibaba(Nacos 注册配置中心);MyBatis-Plus 作为 ORM 框架;Sa-Token 统一认证授权;服务间调用采用 Feign(FallbackFactory 降级兜底);熔断限流采用 Sentinel;PostgreSQL 15 数据存储;Redis 缓存与分布式锁;RocketMQ 消息队列;XXL-Job 定时任务;MinIO 文件存储。
|
||||
- **前端**:Qiankun 微前端 + Monorepo(Turbo + npm workspaces);Vue 3 + TypeScript + Vite;Ant Design Vue 组件库;Pinia 状态管理;secure-ls(AES)与 jsencrypt(RSA)安全存储与传输。
|
||||
|
||||
**技术架构图**:
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
- 日志分析:ELK(Elasticsearch + 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 技术支持响应承诺
|
||||
@@ -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}
|
||||
|
||||
**界面截图**:
|
||||
|
||||

|
||||
|
||||
**注意事项**:
|
||||
- {注意点1}
|
||||
- {注意点2}
|
||||
|
||||
### 3.2 {一级功能2}
|
||||
|
||||
{同上结构}
|
||||
|
||||
## 4. 常见问题与故障处理
|
||||
|
||||
| 序号 | 问题现象 | 可能原因 | 处理方法 |
|
||||
|------|---------|---------|---------|
|
||||
| 1 | {问题} | {原因} | {处理} |
|
||||
|
||||
## 5. 附录
|
||||
|
||||
### 5.1 术语表
|
||||
|
||||
| 术语 | 说明 |
|
||||
|------|------|
|
||||
|
||||
### 5.2 快捷操作
|
||||
|
||||
{键盘快捷键 / 常用入口 汇总}
|
||||
@@ -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}`);
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* HTML 宣传册转 PDF(横版 A4,保留背景)
|
||||
* 用法:node pdf-export.js <html文件路径>
|
||||
* 输出:与 HTML 同目录同名 .pdf
|
||||
* 依赖:playwright(chromium),无则用系统 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);
|
||||
})();
|
||||
@@ -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 存在独立「表单字段表」段时才合并生成,否则本部分省略}
|
||||
@@ -6,6 +6,7 @@
|
||||
<base href="../">
|
||||
<title>文档预览</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1677FF;
|
||||
@@ -50,6 +51,9 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica
|
||||
.content hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
|
||||
.content img { max-width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border); margin: 8px 0; cursor: pointer; transition: opacity .15s; }
|
||||
.content img:hover { opacity: .85; }
|
||||
.content .mermaid { margin: 16px 0; padding: 16px; background: #fff; border: 1px solid var(--border); border-radius: 8px; overflow-x: auto; text-align: center; }
|
||||
.content .mermaid svg { max-width: 100%; height: auto; }
|
||||
.content .mermaid-error { color: #ff4d4f; text-align: left; white-space: pre-wrap; }
|
||||
|
||||
/* Lightbox 图片放大遮罩 */
|
||||
.lightbox { display: none; position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,.75); align-items: center; justify-content: center; cursor: zoom-out; }
|
||||
@@ -75,17 +79,60 @@ const params = new URLSearchParams(location.search);
|
||||
const filePath = params.get('file');
|
||||
const title = params.get('title');
|
||||
|
||||
function renderMermaidBlocks(root) {
|
||||
if (typeof mermaid === 'undefined') return;
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
securityLevel: 'strict',
|
||||
fontFamily: '-apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue","Segoe UI",sans-serif'
|
||||
});
|
||||
root.querySelectorAll('pre code.language-mermaid, pre code.mermaid').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
if (!pre) return;
|
||||
const diagram = document.createElement('div');
|
||||
diagram.className = 'mermaid';
|
||||
diagram.textContent = code.textContent.trim();
|
||||
pre.replaceWith(diagram);
|
||||
});
|
||||
const nodes = root.querySelectorAll('.mermaid');
|
||||
if (!nodes.length) return;
|
||||
mermaid.run({ nodes }).catch(err => {
|
||||
nodes.forEach(node => {
|
||||
if (!node.querySelector('svg')) {
|
||||
node.classList.add('mermaid-error');
|
||||
node.textContent = 'Mermaid 图形渲染失败:' + (err && err.message ? err.message : err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (title) {
|
||||
document.getElementById('doc-title').textContent = title;
|
||||
document.title = title + ' - 文档预览';
|
||||
}
|
||||
if (filePath) {
|
||||
document.getElementById('doc-path').textContent = filePath;
|
||||
fetch(filePath)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('文件未找到 (' + res.status + ')');
|
||||
/* 用绝对路径(/ 开头),避免 <base href="../"> 在不同浏览器中解析不一致 */
|
||||
var fetchUrl = '/' + filePath;
|
||||
|
||||
function tryFetch(url, fallbackUrl) {
|
||||
return fetch(url).then(function(res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return res.text();
|
||||
})
|
||||
}).catch(function(err) {
|
||||
/* 主路径失败 → 尝试去掉开头的 / 重新请求 */
|
||||
if (fallbackUrl && url !== fallbackUrl) {
|
||||
return fetch(fallbackUrl).then(function(res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status + ' (fallback)');
|
||||
return res.text();
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
tryFetch(fetchUrl, filePath)
|
||||
.then(md => {
|
||||
/* 检测是否为自带 HTML 结构的文件(如共性需求说明.md) */
|
||||
const isRawHtml = /^\s*<(?:style|div|!doctype|html)/i.test(md);
|
||||
@@ -121,6 +168,9 @@ if (filePath) {
|
||||
+ '#main-content p{font-size:14px;line-height:1.8;margin:8px 0}'
|
||||
+ '#main-content ul,#main-content ol{padding-left:20px;margin:8px 0;font-size:14px;line-height:1.8}'
|
||||
+ '#main-content code{background:#f5f5f5;padding:2px 6px;border-radius:4px;font-size:13px}'
|
||||
+ '#main-content .mermaid{margin:16px 0;padding:16px;background:#fff;border:1px solid #e8ecf1;border-radius:8px;overflow-x:auto;text-align:center}'
|
||||
+ '#main-content .mermaid svg{max-width:100%;height:auto}'
|
||||
+ '#main-content .mermaid-error{color:#ff4d4f;text-align:left;white-space:pre-wrap}'
|
||||
+ '#main-content blockquote{border-left:4px solid #1677FF;padding:8px 16px;margin:12px 0;background:#E6F7FF;border-radius:0 4px 4px 0}'
|
||||
+ '#toc-container a.active{color:#1677FF;font-weight:600}'
|
||||
+ '</style>';
|
||||
@@ -136,13 +186,25 @@ if (filePath) {
|
||||
+ 'links.forEach(function(a){a.classList.remove("active")});if(cur)cur.link.classList.add("active")}'
|
||||
+ 'window.addEventListener("scroll",update);update()});'
|
||||
+ '<\/script>';
|
||||
/* iframe 内注入:将 mermaid 代码块转换为图形 */
|
||||
const mermaidScript = '<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"><\/script>';
|
||||
const diagramScript = '<script>'
|
||||
+ 'document.addEventListener("DOMContentLoaded",function(){'
|
||||
+ 'if(typeof mermaid==="undefined")return;'
|
||||
+ 'mermaid.initialize({startOnLoad:false,theme:"default",securityLevel:"strict",fontFamily:"-apple-system,BlinkMacSystemFont,\\"PingFang SC\\",\\"Helvetica Neue\\",\\"Segoe UI\\",sans-serif"});'
|
||||
+ 'document.querySelectorAll("pre code.language-mermaid,pre code.mermaid").forEach(function(code){'
|
||||
+ 'var pre=code.closest("pre");if(!pre)return;var div=document.createElement("div");div.className="mermaid";div.textContent=code.textContent.trim();pre.replaceWith(div)});'
|
||||
+ 'var nodes=document.querySelectorAll(".mermaid");if(!nodes.length)return;'
|
||||
+ 'mermaid.run({nodes:nodes}).catch(function(err){nodes.forEach(function(node){if(!node.querySelector("svg")){node.classList.add("mermaid-error");node.textContent="Mermaid 图形渲染失败:"+(err&&err.message?err.message:err)}})})'
|
||||
+ '});'
|
||||
+ '<\/script>';
|
||||
/* 用 iframe 隔离渲染 */
|
||||
document.querySelector('.topbar').style.display = 'none';
|
||||
const wrap = document.getElementById('content');
|
||||
wrap.style.cssText = 'max-width:none;margin:0;padding:0;min-height:100vh';
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.cssText = 'width:100%;height:100vh;border:none';
|
||||
iframe.srcdoc = '<!DOCTYPE html><html><head><meta charset="UTF-8">' + extraCss + '</head><body>' + processed + tocScript + '</body></html>';
|
||||
iframe.srcdoc = '<!DOCTYPE html><html><head><meta charset="UTF-8">' + extraCss + mermaidScript + '</head><body>' + processed + tocScript + diagramScript + '</body></html>';
|
||||
wrap.innerHTML = '';
|
||||
wrap.appendChild(iframe);
|
||||
} else {
|
||||
@@ -151,11 +213,12 @@ if (filePath) {
|
||||
const content = document.getElementById('content');
|
||||
if (typeof marked !== 'undefined') {
|
||||
content.innerHTML = marked.parse(md);
|
||||
renderMermaidBlocks(content);
|
||||
/* 将图片相对路径补全为基于 Markdown 文件目录的路径 */
|
||||
content.querySelectorAll('img').forEach(img => {
|
||||
const src = img.getAttribute('src');
|
||||
if (src && !src.startsWith('http') && !src.startsWith('/') && !src.startsWith('data:')) {
|
||||
img.src = mdBase + src;
|
||||
img.src = '/' + mdBase + src;
|
||||
}
|
||||
});
|
||||
/* 绑定图片点击放大 */
|
||||
@@ -170,9 +233,9 @@ if (filePath) {
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('content').innerHTML = '<div class="loading error">加载失败:' + err.message + '<br><small style="color:var(--text-placeholder);margin-top:8px;display:block">请确认文件路径:' + filePath + '</small></div>';
|
||||
});
|
||||
.catch(err => {
|
||||
document.getElementById('content').innerHTML = '<div class="loading error">❌ 加载失败:' + err.message + '<br><small style="color:var(--text-placeholder);margin-top:8px;display:block">📁 文件路径:' + filePath + '<br>🔗 请求地址:' + fetchUrl + '</small></div>';
|
||||
});
|
||||
} else {
|
||||
document.getElementById('content').innerHTML = '<div class="loading">未指定文件路径</div>';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,809 @@
|
||||
# 异常数据分析
|
||||
|
||||
> **文档目的**:根据业务表结构,预先梳理各模块可能产生脏数据的数据库表及对应的脏数据类型,供后续脏数据检测开发使用。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [一、脏数据类型汇总](#一脏数据类型汇总)
|
||||
- [二、健康评估](#二健康评估)
|
||||
- [2.1 涉及数据库表](#21-涉及数据库表)
|
||||
- [2.2 系统脏数据检测规则](#22-系统脏数据检测规则)
|
||||
- [2.3 检测频率建议](#23-检测频率建议)
|
||||
- [三、健康咨询](#三健康咨询)
|
||||
- [3.1 涉及数据库表](#31-涉及数据库表)
|
||||
- [3.2 系统脏数据检测规则](#32-系统脏数据检测规则)
|
||||
- [3.3 现有后端实现评估](#33-现有后端实现评估)
|
||||
- [3.4 检测频率建议](#34-检测频率建议)
|
||||
- [四、应急就医](#四应急就医)
|
||||
- [4.1 涉及数据库表](#41-涉及数据库表)
|
||||
- [4.2 系统脏数据检测规则](#42-系统脏数据检测规则)
|
||||
- [4.3 检测频率建议](#43-检测频率建议)
|
||||
- [五、营养管理](#五营养管理)
|
||||
- [5.1 涉及数据库表](#51-涉及数据库表)
|
||||
- [5.2 系统脏数据检测规则](#52-系统脏数据检测规则)
|
||||
- [5.3 现有后端实现评估](#53-现有后端实现评估)
|
||||
- [5.4 检测频率建议](#54-检测频率建议)
|
||||
- [六、健康监测](#六健康监测)
|
||||
- [6.1 涉及数据库表](#61-涉及数据库表)
|
||||
- [6.2 系统脏数据检测规则](#62-系统脏数据检测规则)
|
||||
- [6.3 检测频率建议](#63-检测频率建议)
|
||||
|
||||
---
|
||||
|
||||
## 一、脏数据类型汇总
|
||||
|
||||
| 脏数据类型 | 健康评估 | 健康咨询 | 应急就医 | 营养管理 | 健康监测 |
|
||||
|------------|:--:|:--:|:--:|:--:|:--:|
|
||||
| 数据缺失 | ✅ | ✅ | — | ✅ | ✅ |
|
||||
| 格式错误 | ✅ | ✅ | — | ✅ | ✅ |
|
||||
| 逻辑矛盾 | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 重复数据 | ✅ | — | — | ✅ | ✅ |
|
||||
| 范围越界 | ✅ | ✅ | — | ✅ | ✅ |
|
||||
| 关联缺失 | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 状态异常 | ✅ | ✅ | ✅ | — | — |
|
||||
| 非法数据 | — | — | ✅ | — | — |
|
||||
| 异常坐标 | — | — | ✅ | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 二、健康评估
|
||||
|
||||
> **后端服务**:`platform-assessment`(端口 24803)| **原型参考**:评估模块原型使用摹刀独立设计
|
||||
>
|
||||
> **开发状态**:⚠️ 后端已有 `AssDataAbnormalController` 占位(返回硬编码 mock 数据)。
|
||||
|
||||
健康评估模块对员工进行多维度健康评估,包括风险评估、心理评估、规律起居评估、人群分类等。
|
||||
|
||||
### 2.1 涉及数据库表
|
||||
|
||||
评估模块共 21 张业务表,按子域分类如下。
|
||||
|
||||
#### 风险评估(7+2 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `ass_risk_evaluation` | 风险评估主表(问卷+自动双模式) | 高 — `eval_source`(评估来源:1=问卷/2=自动)、`status`(任务状态:1=成功/2=处理中/3=失败)、`is_exception`(是否异常:0=否/1=是)、`user_id`(用户ID) |
|
||||
| `ass_risk_result` | 风险评估结果(疾病等级、BMI、生活习惯标记) | 高 — `bmi`(BMI值)、`sbp`(收缩压)/`dbp`(舒张压)、`fasting_bg`(空腹血糖)、`diabetes_level`(糖尿病等级)、`bmi_mark`(BMI标记格式) |
|
||||
| `ass_risk_evaluation_statistics` | 风险评估统计(按用户/年份汇总) | 中 — `statics_num`(统计评估次数)、`exception_num`(异常评估次数) |
|
||||
| `ass_risk_factor` | 风险因素明细 | 中 — `current_value`(当前检测值)、`reference_range`(参考范围)、`is_abnormal`(是否异常) |
|
||||
| `ass_risk_questionnaire_info` | 问卷基础信息(病史、生活方式) | 高 — `height`(身高cm)/`weight`(体重kg)/`waist_line`(腰围cm)、`smoking_status`(吸烟状态:1-3) |
|
||||
| `ass_risk_diet_sports_prescription` | 饮食运动处方 | 中 — `f_elements`(食物元素格式)、`aerobic_*`(有氧运动)、`strength_*`(力量训练)、`flexibility_*`(柔韧训练) |
|
||||
| `ass_risk_hms_log` | 第三方HMS接口调用日志 | 低 — XML 格式完整性 |
|
||||
| `ass_risk_request_cache` | MD5请求去重缓存 | 低 — 缓存过期/碰撞 |
|
||||
| `ass_risk_food_element` | 食物营养元素字典 | 低 — 字典数据完整性 |
|
||||
|
||||
#### 心理评估(3 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `ass_psychology` | 心理评估主表(综合/压力/焦虑/抑郁) | 高 — `type`(评估类型:1=综合/2=压力/3=焦虑/4=抑郁)、`grade`(评估得分)、`phy_grade_level`(健康等级)、`is_exception`(是否异常)、`status`(任务状态) |
|
||||
| `ass_psychology_statistics` | 心理评估统计 | 中 — 统计值与实际记录数一致性 |
|
||||
| `ass_influence_factor` | 心理影响因素评估 | 中 — 因子数据完整性 |
|
||||
|
||||
#### 规律起居(2 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `ass_daily_routine_online` | 在线穿戴数据评估(睡眠/运动/心率/血氧/压力) | 高 — `sleep_hour`(睡眠时长分钟)、`sport_minute`(运动时长分钟)、`resting_heart_rate`(静息心率bpm)、`oxygen_saturation`(血氧饱和度%)、`week_start_date`(周开始日期)/`week_end_date`(周结束日期) |
|
||||
| `ass_daily_routine_paper` | 问卷规律起居(体力活动/睡眠/上网) | 中 — `sport_value`(运动值)、`sleep_value`(睡眠值)、`time_of_net_value`(上网时长)、`questions_id`(关联问卷ID) |
|
||||
|
||||
#### 人群分类(4 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `ass_group_user` | 人群分类主表(A/B/C/D/E五类) | 高 — `user_group`(人群分类枚举:A/B/C/D/E)、`audit_status`(审核状态:0-2)、`is_archived`(是否归档)、`latest_assessment_date`(最近评估日期) |
|
||||
| `ass_group_user_detail` | 人群分类明细(疾病/体检项) | 中 — `detail_type`(明细类型:A/B/C/D)、`item_code`(项目编码)、`risk_level_flag`(风险等级标记) |
|
||||
| `ass_group_user_change_his` | 分类变更历史 | 低 — 变更记录完整性 |
|
||||
| `ass_group_user_audit_his` | 分类审核历史 | 低 — 审核记录完整性 |
|
||||
|
||||
#### 共享/基础(3 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `ass_questions` | 问卷答案(JSON) | 中 — `questions_json`(问卷JSON)、`status`(状态:卡处理中) |
|
||||
| `ass_algorithm_model` | 算法模型文档 | 低 |
|
||||
| `ass_trigger_config` | 评估触发器配置 | 低 |
|
||||
|
||||
### 2.2 系统脏数据检测规则
|
||||
|
||||
> 参照营养模块 `nut_ano_dirty` 模式。mock 数据中定义了四种异常类型:缺失值、超范围值、重复数据、格式错误。
|
||||
|
||||
#### 风险评估
|
||||
|
||||
**评估主表(`ass_risk_evaluation`)**
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及字段 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| 数据缺失 | `user_id` 为 NULL | `user_id`(用户ID) | 评估记录无归属人 |
|
||||
| 范围越界 | `eval_source` 不在 1-2 范围内 | `eval_source`(评估来源) | 评估来源异常(1=问卷/2=自动) |
|
||||
| 状态异常 | `status` = 2(处理中)或 3(失败)超过 24 小时 | `status`(任务状态) | 评估任务卡住未完成 |
|
||||
| 逻辑矛盾 | `is_exception` = 1 但 `ass_risk_result` 中所有疾病等级均为正常 | `is_exception`(是否异常) ↔ `ass_risk_result.*_level`(疾病等级) | 异常标记与结果数据不一致 |
|
||||
|
||||
**评估结果(`ass_risk_result`)**
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及字段 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| 范围越界 | BMI 值 < 10 或 > 80 | `bmi`(BMI值) | 超出人体可能范围 |
|
||||
| 范围越界 | 收缩压 < 50 或 > 300 mmHg | `sbp`(收缩压) | 血压值异常 |
|
||||
| 范围越界 | 舒张压 < 30 或 > 200 mmHg | `dbp`(舒张压) | 血压值异常 |
|
||||
| 逻辑矛盾 | `sbp` < `dbp` | `sbp`(收缩压)、`dbp`(舒张压) | 收缩压低于舒张压 |
|
||||
| 范围越界 | `fasting_bg`(空腹血糖)< 0 或 > 50 mmol/L | `fasting_bg`(空腹血糖) | 血糖值异常 |
|
||||
| 范围越界 | `health_age` / `potential_health_age` 计算为负数或 > 150 | `health_age`(健康年龄)、`potential_health_age`(潜在健康年龄) | 健康年龄异常 |
|
||||
| 格式错误 | `bmi_mark` / `smoke_mark` / `sleep_mark` 等不符合 `state,score,fullmark` 格式 | `*_mark` 字段(生活习惯标记) | 生活习惯标记格式异常 |
|
||||
| 范围越界 | 疾病等级(`diabetes_level`、`hypertension_level` 等)不在预期枚举范围内 | `*_level` 字段(疾病等级) | 等级编码越界 |
|
||||
| 逻辑矛盾 | `cholesterol`、`triglycerin`、`hdlc`、`ldlc` 均为 0 或 NULL 但疾病等级非空 | 血脂四项 + `*_level`(疾病等级) | 血脂数据缺失但给出了疾病评级 |
|
||||
|
||||
**问卷信息(`ass_risk_questionnaire_info`)**
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及字段 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| 范围越界 | 身高 < 50cm 或 > 250cm | `height`(身高cm) | 录入异常 |
|
||||
| 范围越界 | 体重 < 20kg 或 > 300kg | `weight`(体重kg) | 录入异常 |
|
||||
| 范围越界 | 腰围 < 30cm 或 > 200cm | `waist_line`(腰围cm) | 录入异常 |
|
||||
| 范围越界 | `smoking_status` 不在 1-3 | `smoking_status`(吸烟状态) | 吸烟状态枚举越界 |
|
||||
| 范围越界 | 个人病史字段(`personal_diabetes` 等)不为 0 或 1 | `personal_*` 字段(个人病史) | 病史枚举越界 |
|
||||
| 数据缺失 | `height`、`weight`、`waist_line` 均为 NULL | `height`(身高)、`weight`(体重)、`waist_line`(腰围) | 基础体测数据完全缺失 |
|
||||
| 格式错误 | 问卷 JSON 原始数据格式异常 | `questions_json`(问卷JSON,通过 `ass_questions`) | JSON 解析失败 |
|
||||
|
||||
**评估统计(`ass_risk_evaluation_statistics`)**
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及字段 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| 逻辑矛盾 | `statics_num` 与 `ass_risk_evaluation` 中实际评估次数不一致 | `statics_num`(统计评估次数) | 统计数据过期 |
|
||||
| 逻辑矛盾 | `is_bmi_abnormal` 等疾病异常标记与 `last_evaluation_id` 对应结果数据不一致 | `is_*_abnormal` 字段(疾病异常标记) | 统计标记与源数据脱节 |
|
||||
| 逻辑矛盾 | `exception_num` 与实际 `is_exception=1` 的记录数不一致 | `exception_num`(异常计数) | 异常计数错误 |
|
||||
|
||||
#### 心理评估
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 范围越界 | `type` 不在 1-4(综合/压力/焦虑/抑郁) | `ass_psychology` | `type`(评估类型) |
|
||||
| 范围越界 | `grade` 超出该量表合理分值范围 | `ass_psychology` | `grade`(评估得分) |
|
||||
| 数据缺失 | `grade` 或 `phy_grade_level` 为 NULL | `ass_psychology` | `grade`(评估得分)、`phy_grade_level`(健康等级) |
|
||||
| 格式错误 | `phy_grade_level` 编码不在数据字典范围内 | `ass_psychology` | `phy_grade_level`(健康等级编码) |
|
||||
| 状态异常 | `status` = 2(分析中)超过 24 小时 | `ass_psychology` | `status`(任务状态) |
|
||||
| 逻辑矛盾 | `is_exception` = 1 但 `grade` 在正常范围内 | `ass_psychology` | `is_exception`(是否异常) ↔ `grade`(评估得分) |
|
||||
| 关联缺失 | `questions_id` 在 `ass_questions` 中不存在 | `ass_psychology` | `questions_id`(关联问卷ID) |
|
||||
| 逻辑矛盾 | `ass_psychology_statistics` 统计值与实际评估记录数不一致 | `ass_psychology_statistics` | — |
|
||||
|
||||
#### 规律起居
|
||||
|
||||
**在线穿戴评估(`ass_daily_routine_online`)**
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及字段 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| 范围越界 | `sleep_hour` < 0 或 > 1440 分钟 | `sleep_hour`(睡眠时长分钟) | 睡眠时长不合理 |
|
||||
| 范围越界 | `sport_minute` < 0 或 > 1440 分钟 | `sport_minute`(运动时长分钟) | 运动时长不合理 |
|
||||
| 范围越界 | `resting_heart_rate` < 30 或 > 220 bpm | `resting_heart_rate`(静息心率bpm) | 静息心率异常 |
|
||||
| 范围越界 | `oxygen_saturation` < 60% 或 > 100% | `oxygen_saturation`(血氧饱和度%) | 血氧值异常 |
|
||||
| 范围越界 | `pressure_value` 超出传感器量程 | `pressure_value`(压力值) | 压力值异常 |
|
||||
| 逻辑矛盾 | `week_end_date` < `week_start_date` | `week_end_date`(周结束日期)、`week_start_date`(周开始日期) | 周起止日期倒置 |
|
||||
| 逻辑矛盾 | `sleep_hour` + `sport_minute` > 1440 | `sleep_hour`(睡眠时长)、`sport_minute`(运动时长) | 睡眠+运动超过24小时 |
|
||||
| 状态异常 | `status` = 2(分析中)超过 24 小时 | `status`(任务状态) | 任务卡住 |
|
||||
| 数据缺失 | `is_exception` 为 NULL | `is_exception`(是否异常) | 异常标记缺失 |
|
||||
|
||||
**问卷评估(`ass_daily_routine_paper`)**
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及字段 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| 范围越界 | `sport_value` / `sleep_value` / `time_of_net_value` 为负值 | `sport_value`(运动值)、`sleep_value`(睡眠值)、`time_of_net_value`(上网时长) | 负数无意义 |
|
||||
| 范围越界 | `time_of_net_value` > 24 小时 | `time_of_net_value`(上网时长) | 上网时长不合理 |
|
||||
| 范围越界 | `sport_level` / `sleep_type` / `time_of_net_level` 不在枚举范围 | `sport_level`(运动等级)、`sleep_type`(睡眠类型)、`time_of_net_level`(上网等级) | 等级编码越界 |
|
||||
| 关联缺失 | `questions_id` 在 `ass_questions` 中不存在 | `questions_id`(关联问卷ID) | 孤立问卷关联 |
|
||||
| 数据缺失 | `is_exception` 为 NULL | `is_exception`(是否异常) | 异常标记缺失 |
|
||||
|
||||
#### 人群分类
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 范围越界 | `user_group` 不在 A/B/C/D/E 范围内 | `ass_group_user` | `user_group`(人群分类) |
|
||||
| 范围越界 | `group_change_type` 不在 1-3 | `ass_group_user` | `group_change_type`(变更类型) |
|
||||
| 范围越界 | `audit_status` 不在 0-2 | `ass_group_user` | `audit_status`(审核状态) |
|
||||
| 逻辑矛盾 | `is_archived` = 1 且 `archived_time` 为 NULL | `ass_group_user` | `is_archived`(是否归档)、`archived_time`(归档时间) |
|
||||
| 范围越界 | `date_year` 为未来年份或早于 2020 | `ass_group_user` | `date_year`(归属年份) |
|
||||
| 范围越界 | `latest_assessment_date` 为未来日期 | `ass_group_user` | `latest_assessment_date`(最近评估日期) |
|
||||
| 关联缺失 | `group_user_id` 在 `ass_group_user` 中不存在 | `ass_group_user_detail` | `group_user_id`(人群分类ID) |
|
||||
| 范围越界 | `detail_type` 不在 A/B/C/D | `ass_group_user_detail` | `detail_type`(明细类型) |
|
||||
| 范围越界 | `data_type` 不在 1-2 | `ass_group_user_detail` | `data_type`(数据类型) |
|
||||
| 重复数据 | 同一用户同一年度存在多条活跃分类记录 | `ass_group_user` | `user_id`(用户ID) + `date_year`(归属年份) |
|
||||
|
||||
#### 问卷与处方
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 格式错误 | `questions_json` 格式非法(JSON 解析失败) | `ass_questions` | `questions_json`(问卷JSON) |
|
||||
| 状态异常 | `status` = 2(分析中)超过 24 小时 | `ass_questions` | `status`(任务状态) |
|
||||
| 格式错误 | `f_elements` 不符合 `meal_foodCode_amount;...` 格式 | `ass_risk_diet_sports_prescription` | `f_elements`(食物元素格式) |
|
||||
| 范围越界 | `energy_level` 为负值或 > 10000 kcal | `ass_risk_diet_sports_prescription` | `energy_level`(能量等级) |
|
||||
| 数据缺失 | 评估正常但处方中所有运动字段为 NULL | `ass_risk_diet_sports_prescription` | `aerobic_*`(有氧运动)、`strength_*`(力量训练)、`flexibility_*`(柔韧训练) |
|
||||
|
||||
### 2.3 检测频率建议
|
||||
|
||||
| 检测类型 | 建议频率 | 说明 |
|
||||
|----------|----------|------|
|
||||
| 评估任务卡住检测 | 每小时 | `status=2` 超过 24 小时的记录 |
|
||||
| 数值范围越界检测 | 每日凌晨(评估任务完成后) | 全量扫描 BMI/血压/心率等 |
|
||||
| 统计一致性校验 | 每日凌晨 | `statics_num` vs 实际记录数 |
|
||||
| 格式校验(JSON/mark) | 数据入库时实时 | 问卷JSON、`f_elements`、`*_mark` |
|
||||
|
||||
---
|
||||
|
||||
## 三、健康咨询
|
||||
|
||||
> **后端服务**:`platform-consult`(端口 24806)| **原型参考**:无独立原型页面
|
||||
>
|
||||
> **开发状态**:✅ 已实现 `con_exception_dirty` 表 + 3 种异常类型定时检测,管理端分页查询/忽略/导出功能完整。
|
||||
|
||||
健康咨询模块管理员工在线图文/视频咨询全流程,涵盖咨询会话、专家管理、病历档案、IM 消息、评价结算等。
|
||||
|
||||
### 3.1 涉及数据库表
|
||||
|
||||
#### 咨询核心(4 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `con_consult` | 咨询会话主表(图文/视频统一) | 高 — `consult_no`(咨询单号)、`content_status`(预约/咨询状态:1-8)、`session_status`(会话状态:1=进行中/2=已结束)、`start_time`(开始时间)/`end_time`(结束时间)、`duration_minutes`(咨询时长秒)、`score`(用户评分1-5)、`cost`(咨询费用) |
|
||||
| `con_evaluate` | 咨询评价表 | 高 — `score`(评分1-5)、`consult_id`(关联咨询ID)、`expert_id`(被评价专家ID) |
|
||||
| `con_medical_record` | 病历档案表 | 中 — `medical_describe`(病情描述)、`duration_type`(持续时间:1-4)、`images`(检查资料图片URL) |
|
||||
| `con_family_member` | 家庭成员/咨询人表 | 中 — `member_name`(成员姓名)、`birthday`(出生日期)、`height`(身高cm)、`weight`(体重kg)、`relation`(家庭关系) |
|
||||
|
||||
#### 专家与排班(2 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `con_expert` | 专家信息表 | 高 — `expert_no`(专家编号)、`expert_type`(专家类型:1=收费/2=免费/3=其他)、`overall_merit`(综合评分)、`response_rate`(24h回复率)、`hospital_id`(所属医院ID)、`dept_id`(所属科室ID)、`reg_date`(入职日期)/`off_date`(离职日期) |
|
||||
| `con_expert_schedule_rule` | 专家排班规则表(按周设置) | 中 — `week_day`(周几:1-7)、`period`(时段:1-4)、`start_time`(出诊开始时间)/`end_time`(出诊结束时间)、`capacity`(接诊人数上限) |
|
||||
|
||||
#### IM 消息(2 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `con_im_message` | IM消息记录表 | 中 — `msg_key`(腾讯IM消息唯一标识)、`msg_type`(消息类型:1=单聊/2=群聊/3=应急群聊/4=音视频)、`msg_content`(消息内容)、`send_time`(发送时间)、`consult_id`(关联咨询ID) |
|
||||
| `con_im_chat` | IM聊天会话表(通讯录单聊) | 中 — `session_id`(会话ID)、`pair_id`(会话对唯一标识)、`from_user_id`(发起者)、`to_user_id`(接受者) |
|
||||
|
||||
#### 结算(4 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `con_settle_main` | 结算主表 | 中 — `settle_status`(结算状态:0=待结算/1=结算中/2=已结算/3=异常)、`com_fee_per_month`(每月通讯费)、`error_msg`(异常信息) |
|
||||
| `con_settle_result` | 结算结果 | 中 — 金额一致性 |
|
||||
| `con_settle_result_item` | 结算结果明细 | 中 — 明细与汇总一致性 |
|
||||
| `con_settle_cost_config` | 费用配置 | 低 — 配置值合理性 |
|
||||
|
||||
#### 基础数据(4 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `con_hospital` | 医院关联配置 | 中 — `ref_id`(关联外部医院ID) |
|
||||
| `con_hospital_dept` | 医院-科室关联表 | 低 — `hospital_id`(医院ID)、`dept_id`(科室ID) |
|
||||
| `con_department` | 科室表 | 中 — `dept_name`(科室名称)、`parent_id`(上级科室ID) |
|
||||
| `con_disease` | 疾病信息表 | 低 — `disease_name`(疾病名称)、`dept_id`(所属科室ID) |
|
||||
|
||||
#### 其他(4 张表)
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `con_notice` | 咨询须知表 | 低 |
|
||||
| `con_phrase` | 常用语表 | 低 |
|
||||
| `con_settle_period_config` | 结算周期配置 | 低 |
|
||||
|
||||
### 3.2 系统脏数据检测规则
|
||||
|
||||
> 后端已实现 `con_exception_dirty` 表,目前定义了 3 种异常类型:咨询时长异常(`exception_type=1`)、长时间未回复(`exception_type=2`)、评价分数极低(`exception_type=3`)。
|
||||
>
|
||||
> ⚠️ **已知问题**:`ConConsultTimeoutJob` 每小时自动关闭超时 7 天的会话,因此"咨询时长异常"检测的阈值必须设置在 7 天以内,否则异常记录永远无法生成。
|
||||
>
|
||||
> ⚠️ **已知缺陷**:`ConExceptionDirtyMapper.xml` 第 29 行 `work_no` 被硬编码为 `NULL`,前端展示员工工号列为空。
|
||||
>
|
||||
> ⚠️ **已知缺陷**:`ConExceptionDirtyMapper.xml` 中 `h."name" AS hospital_name` 引用了 `con_hospital` 表中可能不存在的 `name` 列(`con_hospital` 实体仅有 `ref_id` 无 `name`),需确认运行时是否会报错或从外部服务取数。
|
||||
|
||||
#### 咨询会话(已有)
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 状态异常 | 咨询时长异常:会话进行中但 `duration_minutes` 超过合理阈值(如 > 60 分钟) | `con_consult` | `duration_minutes`(咨询时长)、`content_status`(预约/咨询状态) | `exception_type=1`,需在超时 Job(7天)前触发 |
|
||||
| 状态异常 | 长时间未回复:专家超过 N 小时未回复用户消息 | `con_consult` | `last_reply_time`(最后回复时间)、`content_status`(预约/咨询状态) | `exception_type=2` |
|
||||
| 范围越界 | 评价分数极低:`score` < 2 分 | `con_evaluate` / `con_consult` | `score`(评分) | `exception_type=3` |
|
||||
|
||||
#### 咨询会话(建议新增)
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 逻辑矛盾 | `start_time` > `end_time` | `con_consult` | `start_time`(开始时间)、`end_time`(结束时间) | 咨询结束时间早于开始时间 |
|
||||
| 逻辑矛盾 | `session_status` = 进行中(1) 但 `end_time` 不为空 | `con_consult` | `session_status`(会话状态)、`end_time`(结束时间) | 状态与时间矛盾 |
|
||||
| 逻辑矛盾 | `content_status` = 已完成(3) 但 `end_time` 为空 | `con_consult` | `content_status`(预约/咨询状态)、`end_time`(结束时间) | 已完成但无结束时间 |
|
||||
| 逻辑矛盾 | `content_status` = 已拒绝(8) 但 `reject_reason` 为空 | `con_consult` | `content_status`(预约/咨询状态)、`reject_reason`(拒绝原因) | 拒绝但无理由 |
|
||||
| 范围越界 | `duration_minutes` < 0 | `con_consult` | `duration_minutes`(咨询时长) | 咨询时长为负数 |
|
||||
| 范围越界 | `consult_type` 不在 1-2 | `con_consult` | `consult_type`(咨询类型:1=图文/2=视频) | 咨询类型枚举越界 |
|
||||
| 范围越界 | `content_status` 不在 1-8 | `con_consult` | `content_status`(预约/咨询状态) | 内容状态枚举越界 |
|
||||
| 范围越界 | `cost` < 0 | `con_consult` | `cost`(咨询费用) | 咨询费用为负数 |
|
||||
| 关联缺失 | `expert_id` 在 `con_expert` 中不存在 | `con_consult` | `expert_id`(专家ID) | 关联专家已删除 |
|
||||
| 关联缺失 | `member_id` 不为空但在 `con_family_member` 中不存在 | `con_consult` | `member_id`(咨询人ID) | 关联咨询人已删除 |
|
||||
| 数据缺失 | `consult_no` 为空 | `con_consult` | `consult_no`(咨询单号) | 咨询单号缺失 |
|
||||
|
||||
#### 专家信息
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 范围越界 | `overall_merit` < 0 或 > 5 | `con_expert` | `overall_merit`(综合评分) | 综合评分越界 |
|
||||
| 范围越界 | `response_rate` < 0 或 > 100 | `con_expert` | `response_rate`(24h回复率%) | 回复率越界 |
|
||||
| 范围越界 | `expert_type` 不在 1-3 | `con_expert` | `expert_type`(专家类型) | 专家类型枚举越界 |
|
||||
| 数据缺失 | `expert_no` 为空 | `con_expert` | `expert_no`(专家编号) | 专家编号缺失 |
|
||||
| 逻辑矛盾 | `off_date` < `reg_date`(均已填写时) | `con_expert` | `off_date`(离职日期)、`reg_date`(入职日期) | 离职日期早于入职日期 |
|
||||
| 关联缺失 | `hospital_id` 在 `con_hospital` 中不存在 | `con_expert` | `hospital_id`(所属医院ID) | 关联医院不存在 |
|
||||
| 关联缺失 | `dept_id` 在 `con_department` 中不存在 | `con_expert` | `dept_id`(所属科室ID) | 关联科室不存在 |
|
||||
|
||||
#### 咨询评价
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 范围越界 | `score` < 0 或 > 5 | `con_evaluate` | `score`(评分) | 评分越界 |
|
||||
| 关联缺失 | `consult_id` 在 `con_consult` 中不存在 | `con_evaluate` | `consult_id`(关联咨询ID) | 评价关联的咨询不存在 |
|
||||
| 关联缺失 | `expert_id` 在 `con_expert` 中不存在 | `con_evaluate` | `expert_id`(被评价专家ID) | 评价关联的专家不存在 |
|
||||
|
||||
#### 家庭成员
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 范围越界 | `height` < 50 或 > 250 (cm) | `con_family_member` | `height`(身高cm) | 身高越界 |
|
||||
| 范围越界 | `weight` < 20 或 > 300 (kg) | `con_family_member` | `weight`(体重kg) | 体重越界 |
|
||||
| 范围越界 | `sex` 不在 1-2 | `con_family_member` | `sex`(性别:1=男/2=女) | 性别枚举越界 |
|
||||
| 范围越界 | `birthday` 为未来日期 | `con_family_member` | `birthday`(出生日期) | 生日为未来日期 |
|
||||
|
||||
#### 病历档案
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 数据缺失 | `medical_describe` 为空 | `con_medical_record` | `medical_describe`(病情描述) | 病情描述缺失 |
|
||||
| 范围越界 | `duration_type` 不在 1-4 | `con_medical_record` | `duration_type`(持续时间:1=几天内/2=1-2周/3=1-3月/4=3月以上) | 持续时间枚举越界 |
|
||||
|
||||
#### IM 消息
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 数据缺失 | `msg_content` 为空且非撤回消息 | `con_im_message` | `msg_content`(消息内容) | 消息内容缺失 |
|
||||
| 范围越界 | `msg_type` 不在 1-4 | `con_im_message` | `msg_type`(消息类型) | 消息类型枚举越界 |
|
||||
| 关联缺失 | `consult_id` 不为空但在 `con_consult` 中不存在 | `con_im_message` | `consult_id`(关联咨询ID) | 消息关联的咨询不存在 |
|
||||
|
||||
#### 排班规则
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 范围越界 | `week_day` 不在 1-7 | `con_expert_schedule_rule` | `week_day`(星期:1=周一~7=周日) | 星期枚举越界 |
|
||||
| 范围越界 | `period` 不在 1-4 | `con_expert_schedule_rule` | `period`(时段:1-4) | 时段枚举越界 |
|
||||
| 范围越界 | `capacity` < 0 | `con_expert_schedule_rule` | `capacity`(接诊人数上限) | 接诊人数为负 |
|
||||
| 逻辑矛盾 | `end_time` < `start_time` | `con_expert_schedule_rule` | `start_time`(出诊开始时间)、`end_time`(出诊结束时间) | 排班结束时间早于开始时间 |
|
||||
|
||||
### 3.3 现有后端实现评估
|
||||
|
||||
#### 已实现
|
||||
|
||||
| 组件 | 状态 | 说明 |
|
||||
|------|:--:|------|
|
||||
| `con_exception_dirty` 表 | ✅ 已上线 | 5个字段:consult_id、consult_no、exception_type、exception_value、alarm_time |
|
||||
| `ConExceptionDirtyController` | ✅ 已上线 | `/page` 分页查询 + `/ignore` 批量忽略 + `/export` 导出 |
|
||||
| `ConExceptionDirtyService` | ✅ 已上线 | 分页 JOIN 咨询/专家/医院 + Feign 批量查用户名 |
|
||||
| `ExceptionTypeEnum` | ✅ 已上线 | 3 种异常类型:时长异常/长时间未回复/低分评价 |
|
||||
| `ConConsultTimeoutJob` | ✅ 已上线 | 每小时扫描,7 天超时自动关闭会话 |
|
||||
|
||||
#### 待完善
|
||||
|
||||
| 项目 | 优先级 | 说明 |
|
||||
|------|:--:|------|
|
||||
| 异常数据自动检测定时任务 | 高 | 需实现定时扫描生成 `con_exception_dirty` 记录 |
|
||||
| `work_no` 字段补充 | 高 | 当前 Mapper XML 硬编码为 NULL,需 JOIN `con_user_snapshot` 获取 |
|
||||
| 咨询时长异常阈值与超时 Job 的协调 | 高 | 时长异常应在超时 Job 触发前检测(如 > 60 分钟即告警,而非等 7 天) |
|
||||
| 专家/家庭成员/病历/排班等数据校验 | 中 | 当前仅覆盖咨询会话维度的 3 种异常 |
|
||||
| IM 消息数据完整性校验 | 低 | 消息内容、关联关系 |
|
||||
|
||||
### 3.4 检测频率建议
|
||||
|
||||
| 检测类型 | 建议频率 | 说明 |
|
||||
|----------|----------|------|
|
||||
| 咨询时长异常检测 | 每 30 分钟 | 需在超时 Job(7天)前触发,建议 > 60 分钟即告警 |
|
||||
| 长时间未回复检测 | 每小时 | 检查 `last_reply_time` 超过阈值 |
|
||||
| 低分评价检测 | 评价生成时实时 | 评价提交后即时判断 |
|
||||
| 枚举/范围/逻辑矛盾校验 | 每日凌晨 | 全量扫描各表枚举字段和数值范围 |
|
||||
|
||||
---
|
||||
|
||||
## 四、应急就医
|
||||
|
||||
> **后端服务**:`platform-emergency`(端口 24808)| **原型参考**:`web-admin/emergency-dispatch/admin/exception-dirty.html`
|
||||
>
|
||||
> **开发状态**:⚠️ 后端已有 `EmDirtyDataController` 占位(返回空数据),`EmExceptionDictConstant` 定义常量;尚未实现具体检测和存储。
|
||||
|
||||
应急就医管理员工应急工单、云坐席通话、AED 设备部署与管理、救护车调度等。脏数据类型与其他模块差异较大,主要关注 **AED 设备**数据质量和**通话记录**完整性。
|
||||
|
||||
### 4.1 涉及数据库表
|
||||
|
||||
#### 核心应急表
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `em_emergency_center` | 应急中心 | 中 — `longitude`(经度)/`latitude`(纬度)、`center_phone`(中心电话)、`status`(状态) |
|
||||
| `em_user_profile` | 应急人员档案 | 中 — `cert_number`(证书编号)、`license_number`(执照编号)、`station_status`(驻场状态) |
|
||||
| `em_call_record` | 通话记录(七陌云客服) | 高 — `incoming_phone`(来电号码)、`match_status`(匹配状态:1=唯一匹配/2=多重匹配)、`employee_id`(匹配员工ID)、`duration`(通话时长) |
|
||||
| `em_emergency_order` | 应急工单 | 高 — `order_no`(工单编号)、`status`(工单状态)、`location_desc`(位置描述)、`location_lat`(纬度)/`location_lng`(经度)、`closed_at`(关闭时间) |
|
||||
| `em_work_order_dispatch` | 工单调度日志 | 中 — 调度链路完整性 |
|
||||
|
||||
#### AED 设备管理
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `em_aed_device` | AED 设备主表 | 高 — `battery_level`(电池电量百分比)、`battery_status`(电池状态枚举) |
|
||||
| `em_aed_deploy_point` | AED 部署点位 | 高 — `longitude`(经度)、`latitude`(纬度) |
|
||||
| `em_aed_maintenance` | AED 维护记录 | 中 — 维护日期、到期提醒 |
|
||||
| `em_aed_latest` | AED 最新状态 | 高 — `battery_power`(厂商原始电池数据String)、实时状态聚合 |
|
||||
| `em_aed_warn_info` | AED 告警信息 | 中 — 告警记录 |
|
||||
| `em_aed_usage` | AED 使用记录 | 中 — 使用时间、使用人 |
|
||||
|
||||
#### 其他应急表
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `em_ambulance` | 救护车 | 中 — `plate_no`(车牌号)、`driver`(驾驶员)、`status`(状态) |
|
||||
| `em_schedule_record` | 值班排班记录 | 中 — 排班时间冲突 |
|
||||
| `em_emergency_contact` | 紧急联系人 | 中 — 联系方式有效性 |
|
||||
| `em_emergency_center_call_record` | 应急中心通话记录 | 中 — `call_id`(通话ID)、`employee_id`(员工ID)、`begin_time`(开始时间)、`end_time`(结束时间)、`record_file`(录音文件) |
|
||||
| `em_emergency_center_call_join` | 应急中心通话参与人 | 中 — 参与人关联完整性、`employee_id`(员工ID) |
|
||||
| `em_hospital` | 应急合作医院 | 低 |
|
||||
| `em_first_aid_kit` | 急救箱 | 中 — 过期检查 |
|
||||
| `em_drill_record` | 演练记录 | 低 |
|
||||
|
||||
### 4.2 系统脏数据检测规则
|
||||
|
||||
> 应急模块使用独特的异常类型分类:非法数据 / 异常坐标 / 状态异常。
|
||||
>
|
||||
> **注意**:后端 `EmExceptionDictConstant` 当前定义了 `MISSING_FIELD` / `INVALID_FORMAT` / `ORPHAN_RECORD`,与原型中的 `非法数据` / `异常坐标` / `状态异常` 分类不一致,需后续对齐。
|
||||
|
||||
#### 通话记录
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 非法数据 | 电话号码格式不合法(非国内手机号/非座机号) | `em_call_record` | `incoming_phone`(来电号码) | 云客服传入的号码非法 |
|
||||
| 状态异常 | `match_status` 为"多重匹配"(=2),存在多个员工对应同一号码 | `em_call_record` | `match_status`(匹配状态)、`employee_id`(匹配员工ID) | 一个号码匹配到多名员工,需人工确认 |
|
||||
| 关联缺失 | 通话记录关联的 `order_id` 在 `em_emergency_order` 中不存在 | `em_call_record` | `order_id`(关联工单ID) | 孤立的工单关联 |
|
||||
| 逻辑矛盾 | 通话结束时间早于开始时间 | `em_call_record` | `begin_time`(开始时间)、`end_time`(结束时间) | 时间戳异常 |
|
||||
| 逻辑矛盾 | 通话状态为"已接听"但时长为 0 | `em_call_record` | `answer_status`(接听状态) + `duration`(通话时长) | 状态与数据矛盾 |
|
||||
|
||||
#### AED 设备管理
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 | 说明 |
|
||||
|----------|----------|--------|----------|------|
|
||||
| 非法数据 | 电池电量百分比超出 0-100% 范围(如 = 260%) | `em_aed_device` | `battery_level`(电池电量百分比) | 硬件上传了不可能的电量值 |
|
||||
| 非法数据 | 设备上报的原始电池电压/电量(String 类型,厂商原始值) | `em_aed_latest` | `battery_power`(厂商原始电池数据) | 厂商原始电池数据,非 0-100% 百分比 |
|
||||
| 非法数据 | 电池状态值不在预定义枚举范围内 | `em_aed_device` | `battery_status`(电池状态) | 状态码异常,如"未知" |
|
||||
| 异常坐标 | GPS 坐标不在国内范围(经度 73-135°E,纬度 18-54°N) | `em_aed_deploy_point` | `longitude`(经度)、`latitude`(纬度) | 如坐标指向海外(悉尼) |
|
||||
| 异常坐标 | GPS 坐标为 (0,0) 或 NULL(未获取到定位) | `em_aed_deploy_point` | `longitude`(经度)、`latitude`(纬度) | AED 部署点位无有效坐标 |
|
||||
| 关联缺失 | AED 部署点位关联的应急中心不存在 | `em_aed_deploy_point` ↔ `em_emergency_center` | 中心关联字段 | |
|
||||
| 逻辑矛盾 | AED 最近一次维护日期晚于当前日期 | `em_aed_maintenance` | 维护日期 | 未来日期,数据录入错误 |
|
||||
| 数据缺失 | AED 设备缺少维护记录 | `em_aed_device` ↔ `em_aed_maintenance` | | 设备部署后从未维护 |
|
||||
|
||||
> **注意**:AED 设备离线检测(基于心跳时间差)当前暂不可用,因为 `em_aed_latest` 表缺少 `update_time` 字段。需后续新增该字段。
|
||||
|
||||
### 4.3 检测频率建议
|
||||
|
||||
| 检测类型 | 建议频率 | 说明 |
|
||||
|----------|----------|------|
|
||||
| 系统脏数据 — AED 电池/坐标 | 每日凌晨 | AED 设备数据量小 |
|
||||
| 系统脏数据 — 通话记录非法数据 | 每日凌晨 | 通话记录增量扫描 |
|
||||
| AED 设备离线检测 | 每小时 | `em_aed_latest` 心跳超时判断 |
|
||||
|
||||
---
|
||||
|
||||
## 五、营养管理
|
||||
|
||||
> **后端服务**:`platform-nutrition`(端口 24810)| **原型参考**:`web-admin/nutrition/ano-dirty.html`
|
||||
>
|
||||
> **开发状态**:✅ 已实现脏数据存储表 `nut_ano_dirty`,服务已上线。
|
||||
|
||||
营养管理是系统最大的模块(约 130 张表),涵盖菜品/食谱管理、订餐/取餐、农场种植养殖、原材料采购库存、中央厨房生产、营养算法与 AI 推荐等全链路。
|
||||
|
||||
### 5.1 涉及数据库表
|
||||
|
||||
#### 用户与档案
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_user` | 营养用户(用餐者) | 高 — `phone`(手机号)、`bmi`(BMI值)、`health_tags`(健康标签) |
|
||||
| `nut_user_profile` | 用户健康档案 | 高 — `height`(身高)、`weight`(体重)、`target_weight`(目标体重)、`allergies`(过敏原)、`dietary_restrictions`(饮食限制) |
|
||||
| `nut_user_account` | 用户账户/充值 | 中 — 账户余额字段 |
|
||||
| `nut_user_face` | 面部识别数据 | 中 — 面部嵌入向量有效性 |
|
||||
| `nut_member` | 会员记录 | 中 — 会员有效期 |
|
||||
|
||||
#### 食品与食谱
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_food` | 菜品主表 | 高 — `price`(价格)、`nutrition_info`(营养信息)、`status`(状态) |
|
||||
| `nut_food_class` | 菜品分类 | 中 — `parent_id`(上级分类ID) 自引用完整性 |
|
||||
| `nut_food_composition` | 食物成分 | 高 — `value_per_100g`(每100g营养值) |
|
||||
| `nut_food_nutri` | 食物营养数据 | 高 — 详细营养分解 |
|
||||
| `nut_food_spec` | 食物规格/份量 | 中 — `weight`(份量重量) |
|
||||
| `nut_food_check` | 食品检测记录 | 中 — `result`(检测结果)、`passed`(是否通过) |
|
||||
| `nut_recipe` | 食谱 | 中 — `total_calories`(总热量)、`nutrition_summary`(营养汇总JSON) |
|
||||
| `nut_recipe_food` | 食谱-菜品关联 | 中 — `portion`(份数) |
|
||||
| `nut_cookbook` | 菜谱 | 低 — `steps`(步骤) |
|
||||
|
||||
#### 膳食与订餐
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_order` | 订单主表 | 高 — `total_amount`(订单总金额)、`status`(订单状态)、`order_type`(订单类型) |
|
||||
| `nut_order_item` | 订单明细 | 高 — `quantity`(数量)、`price`(单价)、`subtotal`(小计) |
|
||||
| `nut_canteen` | 餐厅/食堂 | 中 — `capacity`(容量)、`status`(状态) |
|
||||
| `nut_canteen_pickup_record` | 取餐记录 | 中 — `portion`(份量)、`calories`(热量) |
|
||||
| `nut_pickup_order` | 取餐订单 | 中 — `nutrition_info`(营养汇总) |
|
||||
| `nut_consume_record` | 消费记录 | 中 — 金额一致性 |
|
||||
| `nut_pay_error` | 支付错误日志 | 低 — 支付异常记录 |
|
||||
|
||||
#### 原材料与供应
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_mater_base` | 原材料主表 | 高 — `price`(价格)、`unit`(单位) |
|
||||
| `nut_raw_inbound` | 原材料入库 | 高 — `quantity`(数量)、`weight`(重量) |
|
||||
| `nut_sup_inventory` | 供应库存 | 高 — `quantity`(库存数量) |
|
||||
| `nut_sup_inventory_flow` | 库存流水 | 中 — `qty`(操作量)、`current_qty_after`(操作后在库量) |
|
||||
| `nut_sup_hygiene_test` | 卫生检测 | 中 — 检测结果 |
|
||||
| `nut_sup_warehouse` | 仓库 | 低 — `location`(位置)、`manager`(管理员) |
|
||||
|
||||
> **注**:`nut_sup_inventory_flow` 的 SQL DDL 文件缺失(`db/nutrition/` 目录下未找到对应建表脚本),需补充。
|
||||
|
||||
#### 生产与中央厨房
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_prod_combo_task` | 组合生产任务 | 中 — `plan_date`(计划日期)、`status`(任务状态) |
|
||||
| `nut_prod_qc_sample` | 质检样品 | 高 — `status`(状态:0=留样中/1=已处置/2=已检验) |
|
||||
| `nut_prod_qc_violation` | 质检违规 | 中 — 违规记录完整性 |
|
||||
| `nut_prod_serve_record` | 供餐记录 | 中 — 实际供餐与实际订单一致性 |
|
||||
| `nut_prod_stat_cost` | 生产成本统计 | 中 — 金额合理性 |
|
||||
|
||||
#### 农场
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_farm_plot` | 地块 | 中 — `area`(面积)、`soil_type`(土壤类型) |
|
||||
| `nut_farm_crop_batch` | 作物批次 | 中 — `plant_date`(种植日期)、`expected_harvest`(预计采收日期) |
|
||||
| `nut_farm_harvest` | 采收记录 | 中 — `quantity`(采收数量) |
|
||||
| `nut_farm_livestock_batch` | 牲畜批次 | 中 — `count`(数量) |
|
||||
| `nut_farm_supplier_rating` | 供应商评级 | 低 — 评分范围 |
|
||||
|
||||
#### 算法与 AI
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_algo_personal` | 个性化算法结果 | 高 — 营养计算结果 |
|
||||
| `nut_algo_mass` | 批量算法结果 | 高 — 批量计算结果 |
|
||||
| `nut_algo_standard` | 算法标准 | 中 — 标准值合理性 |
|
||||
| `nut_model_llm` | LLM 模型配置 | 中 — 配置有效性 |
|
||||
| `nut_model_small_version` | 模型版本 | 低 — 版本管理 |
|
||||
|
||||
#### 设备与终端
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `nut_terminal` | 终端设备 | 中 — 设备状态 |
|
||||
| `nut_terminal_fault` | 终端故障记录 | 低 |
|
||||
| `nut_meter_device` | 计量设备(秤) | 中 — 计量精度 |
|
||||
| `nut_meter_reading` | 计量读数 | 高 — 读数合理性 |
|
||||
|
||||
### 5.2 系统脏数据检测规则
|
||||
|
||||
> 后端已实现 `nut_ano_dirty` 表,以下按原型业务模块分类列出需要检测的规则。
|
||||
|
||||
#### 用户档案
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 逻辑矛盾 | 年龄为负数 | `nut_user_profile` | `birthday`(生日) → 计算年龄 |
|
||||
| 范围越界 | BMI 值超出合理范围 | `nut_user` / `nut_user_profile` | `bmi`(BMI值) |
|
||||
| 数据缺失 | 手机号为空 | `nut_user` | `phone`(手机号) |
|
||||
| 重复数据 | 同一 `user_id` 存在多条档案记录 | `nut_user_profile` | `user_id`(用户ID) |
|
||||
| 逻辑矛盾 | 健康目标与饮食限制冲突(如目标为增重但设置了减脂限制) | `nut_user_profile` | `health_goal`(健康目标) ↔ `dietary_restrictions`(饮食限制) |
|
||||
|
||||
#### 食谱管理
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 数据缺失 | 热量值字段为空 | `nut_recipe` / `nut_food_nutri` | `total_calories`(总热量) |
|
||||
| 关联缺失 | 食谱引用的食材ID在食材库中不存在 | `nut_recipe_food` | `food_id`(食材ID) → `nut_food.id` |
|
||||
| 格式错误 | 食谱营养汇总JSON格式非法 | `nut_recipe` | `nutrition_summary`(营养汇总JSON) |
|
||||
|
||||
#### 订餐管理
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 格式错误 | 订餐日期格式非法 | `nut_order` | 订单日期字段 |
|
||||
| 范围越界 | 单次订餐份数超出系统上限 | `nut_order_item` | `quantity`(数量) |
|
||||
| 逻辑矛盾 | 订单金额与明细小计之和不一致 | `nut_order.total_amount` vs `SUM(nut_order_item.subtotal)` | |
|
||||
|
||||
#### 营养分析
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 重复数据 | 同一用户同日存在多条重复营养分析记录 | `nut_algo_personal` | `user_id`(用户ID) + 日期 |
|
||||
| 范围越界 | 营养素计算结果超出合理范围 | `nut_algo_personal` / `nut_algo_mass` | 营养结果字段 |
|
||||
|
||||
#### 食材库存
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 范围越界 | 库存数量为负值 | `nut_sup_inventory` | `quantity`(库存数量) |
|
||||
| 逻辑矛盾 | 库存流水余额与当前库存不一致 | `nut_sup_inventory_flow` ↔ `nut_sup_inventory` | `current_qty_after`(操作后在库量) vs `quantity`(库存数量) |
|
||||
| 关联缺失 | 库存记录的原材料在 `nut_mater_base` 中不存在 | `nut_sup_inventory` | `mater_id`(原材料ID) |
|
||||
|
||||
#### 健康生产
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 格式错误 | 生产批次号含非法字符 | `nut_prod_combo_task` | `task_no`(任务编号) |
|
||||
| 逻辑矛盾 | 生产任务完成但无供餐记录 | `nut_prod_combo_task` ↔ `nut_prod_serve_record` | |
|
||||
| 数据缺失 | 质检样品无检测结果 | `nut_prod_qc_sample` | `status`(状态:0=留样中/1=已处置/2=已检验) |
|
||||
|
||||
#### 卫生供应
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 数据缺失 | 供应商资质证书编号为空 | `nut_farm_supplier_external` | 证书字段 |
|
||||
| 格式错误 | 卫生检测报告编号格式非法 | `nut_sup_hygiene_test` | 检测编号字段 |
|
||||
| 关联缺失 | 供应记录关联的供应商已删除 | `nut_raw_inbound` | `supplier_id`(供应商ID) |
|
||||
|
||||
### 5.3 现有后端实现评估
|
||||
|
||||
#### 已实现
|
||||
|
||||
| 组件 | 状态 | 说明 |
|
||||
|------|:--:|------|
|
||||
| `nut_ano_dirty` 表 | ✅ 已上线 | 5个索引,支持分页/筛选/导出 |
|
||||
| `NutAnoDirtyService` | ✅ 已上线 | 完整 CRUD + 异步导出 |
|
||||
| `NutAnoDirtyController` | ✅ 已上线 | 与原型一致的查询接口 |
|
||||
|
||||
#### 待完善
|
||||
|
||||
| 项目 | 优先级 | 说明 |
|
||||
|------|:--:|------|
|
||||
| 系统脏数据自动检测定时任务 | 高 | 需实现每日全量扫描 Job |
|
||||
| 订餐管理、食材库存、健康生产、卫生供应的脏数据规则 | 高 | 原型有定义但后端可能未覆盖全 |
|
||||
| 农场子域的脏数据检测 | 中 | 原型未明确涉及,但表结构已存在 |
|
||||
| 设备/终端数据脏数据检测 | 低 | 秤读数、终端故障等 |
|
||||
|
||||
### 5.4 检测频率建议
|
||||
|
||||
| 检测类型 | 建议频率 | 说明 |
|
||||
|----------|----------|------|
|
||||
| 系统脏数据全面扫描 | 每日凌晨 2:00 | 全量扫描所有7个业务模块 |
|
||||
| 库存数量负值检查 | 每日凌晨 + 每次出入库后 | 实时性要求较高 |
|
||||
| 订单金额一致性 | 每日凌晨 | 财务敏感性数据 |
|
||||
|
||||
---
|
||||
|
||||
## 六、健康监测
|
||||
|
||||
> **后端服务**:`platform-monitor`(端口 24805)| **原型参考**:`web-admin/health-monitor/exception-dirty.html`
|
||||
>
|
||||
> **开发状态**:❌ 未实现独立的脏数据检测表,已有 `mon_alert_record`(穿戴告警)和 `mon_env_warn_info`(环境告警)。
|
||||
|
||||
健康监测是系统第二大模块,涵盖可穿戴设备数据采集、环境监测、健康调查问卷等。
|
||||
|
||||
### 6.1 涉及数据库表
|
||||
|
||||
#### 设备与告警
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `mon_device` | 可穿戴设备主表 | 高 — `device_code`(设备编码)、`bind_status`(绑定状态)、`bind_user_id`(绑定用户ID)、`online_status`(在线状态)、`last_data_time`(最后数据上报时间) |
|
||||
| `mon_device_config` | 设备监测配置 | 高 — `threshold_min`(阈值下限)、`threshold_max`(阈值上限)、`upload_frequency`(上报频率) |
|
||||
| `mon_bind_record` | 设备绑定/解绑记录 | 中 — `bind_date`(绑定日期)、`unbind_date`(解绑日期) |
|
||||
| `mon_alert_record` | 告警记录 | 中 — `alert_value`(告警值)、`threshold_range`(阈值范围)、GPS 坐标 |
|
||||
| `mon_notification` | 通知消息 | 低 — `send_time`(发送时间)、`notice_status`(通知状态) |
|
||||
|
||||
#### 每日聚合统计(9张表)
|
||||
|
||||
| 表名 | 关键字段 | 脏数据风险 |
|
||||
|------|----------|:--:|
|
||||
| `mon_daily_stats_summary` | `wear_flag`(佩戴标记)、`last_upload_time`(最后上报时间) | 中 |
|
||||
| `mon_daily_stats_heart_rate` | `hr_max`(最大心率)/`hr_min`(最小心率)/`hr_avg`(平均心率)/`hr_latest`(最新心率)、`silence_hr_*`(静息心率) | 高 |
|
||||
| `mon_daily_stats_spo2` | `spo2_max`(最大血氧)/`spo2_min`(最小血氧)/`spo2_avg`(平均血氧)/`spo2_latest`(最新血氧) | 高 |
|
||||
| `mon_daily_stats_stress` | `stress_max`(最大压力)/`stress_min`(最小压力)/`stress_avg`(平均压力)/`stress_latest`(最新压力) | 中 |
|
||||
| `mon_daily_stats_temperature` | `temp_max`(最高体温)/`temp_min`(最低体温)/`temp_avg`(平均体温)/`temp_latest`(最新体温)、`skin_temp_*`(皮肤温度) | 高 |
|
||||
| `mon_daily_stats_blood_pressure` | `systolic_max`(最高收缩压)/`systolic_min`(最低收缩压)、`diastolic_max`(最高舒张压)/`diastolic_min`(最低舒张压) | 高 |
|
||||
| `mon_daily_stats_sleep` | `total_min`(总睡眠时长分钟)、`deep_min`(深睡时长)、`score`(睡眠评分) | 中 |
|
||||
| `mon_daily_stats_exercise` | `steps`(步数)、`exercise_calorie`(运动消耗卡路里)、`steps_distance`(步行距离)、`exercise_distance`(运动距离) | 中 |
|
||||
| `mon_daily_stats_ecg` | `ecg_count`(心电测量次数)、`latest_value`(最新值) | 低 |
|
||||
|
||||
#### 原始监测数据(12张表)
|
||||
|
||||
| 表名 | 关键字段 | 脏数据风险 |
|
||||
|------|----------|:--:|
|
||||
| `mon_data_heart_rate` | `data_value`(心率值bpm)、`silence_value`(静息心率值) | 高 |
|
||||
| `mon_data_spo2` | `data_value`(血氧值%) | 高 |
|
||||
| `mon_data_stress` | `data_value`(压力值)、`start_time`(测量开始时间)、`end_time`(测量结束时间) | 中 |
|
||||
| `mon_data_temperature` | `data_value`(体温值℃)、`skin_value`(皮肤温度) | 高 |
|
||||
| `mon_data_sleep` | `fall_asleep_time`(入睡时间)、`wake_up_time`(醒来时间)、`sleep_score`(睡眠评分) | 中 |
|
||||
| `mon_data_sleep_stage` | `stage_type`(睡眠阶段类型)、`start_time`(开始时间)、`end_time`(结束时间)、`duration`(持续时间) | 中 |
|
||||
| `mon_data_ecg` | `avg_heart_rate`(平均心率)、`arrhy_type`(心律失常类型)、`data_points_url`(数据点文件URL) | 中 |
|
||||
| `mon_data_exercise` | `calorie`(消耗卡路里)、`distance`(运动距离)、`duration`(运动时长) | 中 |
|
||||
| `mon_data_exercise_trace` | `longitude`(经度)、`latitude`(纬度)、`altitude`(海拔) | 中 |
|
||||
| `mon_data_steps` | `step_count`(步数)、`calorie`(消耗卡路里)、`distance`(距离) | 中 |
|
||||
| `mon_data_blood_pressure` | `systolic_value`(收缩压)、`diastolic_value`(舒张压) | 高 |
|
||||
| `mon_data_gps` | `longitude`(经度)、`latitude`(纬度)、`address`(地址) | 中 |
|
||||
|
||||
#### 环境监测(16张表)
|
||||
|
||||
| 类别 | 数据表 | 日数据表 | 月数据表 | 使用记录表 |
|
||||
|------|--------|----------|----------|-----------|
|
||||
| 室内 | `mon_env_data_indoor` | `mon_env_day_data_indoor` | `mon_env_month_data_indoor` | `mon_env_use_record_indoor` |
|
||||
| 室外 | `mon_env_data_outdoor` | `mon_env_day_data_outdoor` | `mon_env_month_data_outdoor` | `mon_env_use_record_outdoor` |
|
||||
| 花粉 | `mon_env_data_pollen` | `mon_env_day_data_pollen` | `mon_env_month_data_pollen` | `mon_env_use_record_pollen` |
|
||||
| 水质 | `mon_env_data_water` | `mon_env_day_data_water` | `mon_env_month_data_water` | `mon_env_use_record_water` |
|
||||
|
||||
附加表:`mon_env_category_indoor/outdoor/pollen/water`(指标类别定义)、`mon_env_meter_info`(环境仪表元数据)、`mon_env_warn_info`(环境告警记录)、`mon_env_warn_sync_record`(告警同步记录)
|
||||
|
||||
#### 调查问卷
|
||||
|
||||
| 表名 | 说明 | 脏数据风险 |
|
||||
|------|------|:--:|
|
||||
| `mon_survey` | 调查主表 | 低 |
|
||||
| `mon_survey_activity` | 调查活动 | 低 |
|
||||
| `mon_survey_answer_record` | 回答记录 | 中 |
|
||||
| `mon_survey_answer_detail` | 回答详情 | 中 |
|
||||
|
||||
### 6.2 系统脏数据检测规则
|
||||
|
||||
> 按原型业务模块分类:健康排查、穿戴监测、体重监测、环境监测
|
||||
|
||||
#### 健康排查(问卷)
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 数据缺失 | 积分字段异常(`mon_survey_answer_record` 无 score 字段,积分在别处计算) | `mon_survey_answer_record` + 积分相关表 | — |
|
||||
| 逻辑矛盾 | 问卷回答时间早于活动发布时间 | `mon_survey_answer_record` ↔ `mon_survey_activity` | 时间字段 |
|
||||
| 重复数据 | 同一用户同一问卷出现多条回答记录 | `mon_survey_answer_record` | `user_id`(用户ID) + `survey_id`(问卷ID) |
|
||||
|
||||
#### 穿戴监测
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 格式错误 | 设备绑定日期格式非法 | `mon_bind_record` | `bind_date`(绑定日期) |
|
||||
| 重复数据 | 同一设备同一时间段出现重复数据记录 | `mon_data_heart_rate` / `mon_data_spo2` / ... | `device_id`(设备ID) + `data_time`(数据时间) |
|
||||
| 范围越界 | 心率值超出人体可能范围(如 <20 或 >300 bpm) | `mon_data_heart_rate` | `data_value`(心率值bpm) |
|
||||
| 范围越界 | 血氧值超出合理范围(如 <50% 或 >100%) | `mon_data_spo2` | `data_value`(血氧值%) |
|
||||
| 范围越界 | 体温值超出人体可能范围(如 <30℃ 或 >45℃) | `mon_data_temperature` | `data_value`(体温值℃) |
|
||||
| 逻辑矛盾 | 静息心率高于活动心率 | `mon_daily_stats_heart_rate` | `silence_hr_avg`(静息心率均值) > `hr_avg`(心率均值) |
|
||||
| 逻辑矛盾 | 设备在线但超过 N 天无数据上报 | `mon_device` | `online_status`(在线状态) + `last_data_time`(最后上报时间) |
|
||||
| 范围越界 | GPS 坐标超出国内范围 | `mon_data_gps` | `longitude`(经度)、`latitude`(纬度) |
|
||||
| 格式错误 | 穿戴设备原始数据 `data_points_url` 指向的 URL 不可访问 | `mon_data_ecg` | `data_points_url`(数据点文件URL) |
|
||||
|
||||
#### 体重监测
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 逻辑矛盾 | 日统计中的最大心率 < 最小心率 | `mon_daily_stats_heart_rate` | `hr_max`(最大心率) < `hr_min`(最小心率) |
|
||||
| 逻辑矛盾 | 日统计中的收缩压 < 舒张压 | `mon_daily_stats_blood_pressure` | `systolic_*`(收缩压) < `diastolic_*`(舒张压) |
|
||||
| 逻辑矛盾 | 睡眠时长 > 24小时 | `mon_daily_stats_sleep` | `total_min`(总睡眠时长) > 1440 |
|
||||
|
||||
#### 环境监测
|
||||
|
||||
| 异常类型 | 检测规则 | 涉及表 | 涉及字段 |
|
||||
|----------|----------|--------|----------|
|
||||
| 逻辑矛盾 | 室内温度高于室外温度超过10℃(且空调未在制热) | `mon_env_data_indoor` + `mon_env_data_outdoor` | 温度字段 |
|
||||
| 范围越界 | 环境温度超出设备量程 | 各环境数据表 | 温度字段 |
|
||||
| 范围越界 | PM2.5 值为负数 | `mon_env_data_indoor` / `mon_env_data_outdoor` | PM2.5 字段 |
|
||||
| 范围越界 | ph 值超出 0-14 范围 | `mon_env_data_water` | `ph`(酸碱度) |
|
||||
| 数据缺失 | 环境监测设备超过 N 天未上报数据 | 各环境数据表 | `read_time`(读取时间) |
|
||||
| 关联缺失 | 环境数据关联的仪表在 `mon_env_meter_info` 中不存在 | 各环境数据表 | `meter_code`(仪表编码) |
|
||||
|
||||
### 6.3 检测频率建议
|
||||
|
||||
| 检测类型 | 建议频率 | 说明 |
|
||||
|----------|----------|------|
|
||||
| 系统脏数据全面扫描 | 每日凌晨 3:00 | 穿戴/环境数据量大,错峰执行 |
|
||||
| 原始数据范围越界检查 | 数据入库时实时 | 在数据接收层做第一道校验 |
|
||||
| 聚合统计一致性检查 | 每日凌晨(聚合任务完成后) | 校验日统计 = 原始数据汇总 |
|
||||
| 设备在线但无数据告警 | 每小时 | `mon_device.online_status` + `last_data_time` |
|
||||
| GPS 坐标越界检查 | 每日凌晨 | 全量扫描 `mon_data_gps` |
|
||||
@@ -0,0 +1,996 @@
|
||||
# 异常事件数据质量控制规则说明书
|
||||
|
||||
> **文档版本**:v1.1
|
||||
> **编写日期**:2026-07-29
|
||||
> **适用范围**:系统管理、健康咨询、应急就医、健康评估、营养管理、健康监测(共 6 个业务板块)
|
||||
> **执行主体**:数据中台(于数据同步环节内嵌执行质量校验)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [一、文档概述](#一文档概述)
|
||||
- [二、系统脏数据类型定义](#二系统脏数据类型定义)
|
||||
- [三、脏数据与结果异常的边界](#三脏数据与结果异常的边界)
|
||||
- [四、数据中台质量校验流程](#四数据中台质量校验流程)
|
||||
- [五、各模块检测规则](#五各模块检测规则)
|
||||
- [5.1 系统管理](#51-系统管理)
|
||||
- [5.2 健康咨询](#52-健康咨询)
|
||||
- [5.3 应急就医](#53-应急就医)
|
||||
- [5.4 健康评估](#54-健康评估)
|
||||
- [5.5 营养管理](#55-营养管理)
|
||||
- [5.6 健康监测](#56-健康监测)
|
||||
- [六、附录](#六附录)
|
||||
|
||||
---
|
||||
|
||||
## 一、文档概述
|
||||
|
||||
### 1.1 编写目的
|
||||
|
||||
本文档定义平台各业务板块"系统脏数据"的统一质量校验规则,作为数据中台实现自动化数据质量检测(DQC, Data Quality Check)的依据。核心目标:
|
||||
|
||||
1. **统一标准**:收敛各板块原型和研发文档中分散、不一致的脏数据类型定义,建立全平台统一的异常分类体系
|
||||
2. **明确边界**:厘清"系统脏数据"与"结果异常"的判定标准,确保两类异常各归其位
|
||||
3. **指导落地**:为数据中台在 CDC / 增量同步链路中嵌入质量校验提供规则输入
|
||||
|
||||
### 1.2 适用范围
|
||||
|
||||
| 板块 | 管理端模块 | 当前状态 |
|
||||
|------|-----------|---------|
|
||||
| 健康咨询 | `web-admin/health-consult/` | 原型已定稿,后端已部分实现 |
|
||||
| 应急就医 | `web-admin/emergency-dispatch/` | 原型已定稿,后端有占位 |
|
||||
| 健康评估 | `web-admin/health-assessment/` | 原型待开发,后端已部分实现 |
|
||||
| 营养管理 | `web-admin/nutrition/` | 原型已定稿,后端已部分实现 |
|
||||
| 健康监测 | `web-admin/health-monitor/` | 原型已定稿 |
|
||||
| 系统管理 | `platform-java/platform-system/` | 后端已实现 |
|
||||
|
||||
### 1.3 术语定义
|
||||
|
||||
| 术语 | 定义 |
|
||||
|------|------|
|
||||
| **系统脏数据** | 写入数据库后,因其字段值违反数据完整性约束(非空、格式、取值范围、外键关联、唯一性、逻辑一致性)而不可信或不可用的数据记录 |
|
||||
| **结果异常** | 数据本身在存储层面合法、完整,但所反映的业务结果偏离正常范围(如超时未关闭、评分异常偏低、指标超出医学参考范围等),属于业务流程或健康结果的异常,**不属于系统脏数据范畴** |
|
||||
| **数据中台 DQC** | Data Quality Check,在数据同步管道(CDC 或定时增量)中内嵌执行的数据质量校验环节 |
|
||||
| **CDC** | Change Data Capture,基于数据库变更日志的实时增量数据捕获机制 |
|
||||
|
||||
---
|
||||
|
||||
## 二、系统脏数据类型定义
|
||||
|
||||
全平台统一使用以下 6 种脏数据类型。各板块的具体检测规则均以此分类为准。
|
||||
|
||||
### 2.1 类型一览
|
||||
|
||||
| 编码 | 类型名称 | 判定标准 | 严重级别 |
|
||||
|------|----------|----------|:--:|
|
||||
| D01 | **数据缺失** | 业务规则要求非空的字段,其值为 NULL 或空字符串 | 高 |
|
||||
| D02 | **格式违规** | 字段值不符合预定义的格式规范(正则、JSON Schema、日期格式等) | 高 |
|
||||
| D03 | **取值越界** | 字段值超出物理极限或逻辑合理范围(含连续数值越界和离散枚举越界) | 高 |
|
||||
| D04 | **逻辑冲突** | 同一记录内多个字段之间,或存在业务关联的多条记录之间,字段值存在不可调和的逻辑矛盾 | 高 |
|
||||
| D05 | **关联断裂** | 外键字段值非空,但其引用的目标记录在关联表中不存在(孤儿记录) | 中 |
|
||||
| D06 | **数据重复** | 违反业务唯一性约束,存在多条本应唯一的记录 | 中 |
|
||||
|
||||
### 2.2 各类型详细定义
|
||||
|
||||
#### D01 — 数据缺失
|
||||
|
||||
**定义**:业务语义上必须具备值的字段,在数据库层面为 NULL 或等效空值。
|
||||
|
||||
**判定逻辑**:
|
||||
- 单字段规则:`field IS NULL OR field = ''`
|
||||
- 组合规则:同一记录内多个关联字段全部为空(如身高、体重、腰围三者均为空)
|
||||
|
||||
**典型示例**:
|
||||
- 用户档案中姓名为空
|
||||
- 订单主表中订单编号为空
|
||||
- 体检报告中检查结果值为空
|
||||
- 设备编码为空
|
||||
|
||||
**注意事项**:
|
||||
- 允许为空的字段(如选填的备注、非必填的附件URL)不在检测范围
|
||||
- 需结合业务规则确认哪些字段为"必填",不能仅凭数据库 DDL 的 `NOT NULL` 约束判断(有些字段在 DDL 层面允许 NULL,但业务上必须具备值)
|
||||
|
||||
---
|
||||
|
||||
#### D02 — 格式违规
|
||||
|
||||
**定义**:字段值已填充,但其字符串形式不符合约定的格式规范。
|
||||
|
||||
**判定逻辑**:
|
||||
- 正则匹配失败(手机号、身份证号、邮箱等)
|
||||
- JSON/XML 解析失败(如 `questions_json`、`nutrition_summary` 等)
|
||||
- 日期/时间字符串无法按预期格式解析
|
||||
- URL 格式不合法
|
||||
|
||||
**典型示例**:
|
||||
- 手机号字段值为 `"abc123"` 或长度不为 11 位
|
||||
- 常规体检指标数据出现汉字,空腹血糖值 `"无记录"`
|
||||
- 问卷 JSON 字段 `parse()` 抛出异常
|
||||
- 日期字段值为 `"2026-13-40"`(无效日期)
|
||||
- 生活习惯标记字段不符合 `state,score,fullmark` 格式
|
||||
|
||||
**注意事项**:
|
||||
- 格式校验的正则表达式需与业务端输入校验规则保持一致
|
||||
- 对于允许自由文本的字段(如备注、描述),不执行格式校验
|
||||
|
||||
---
|
||||
|
||||
#### D03 — 取值越界
|
||||
|
||||
**定义**:字段值已填充且格式合法,但数值大小或枚举编码超出了物理/逻辑合理范围。
|
||||
|
||||
**判定逻辑**:
|
||||
- 连续数值越界:`value < min OR value > max`(min/max 基于人体生理极限或业务规则)
|
||||
- 离散枚举越界:`value NOT IN (enum_set)`(enum_set 为字段的合法枚举值集合)
|
||||
|
||||
**典型示例**:
|
||||
- BMI 值为 85.3 或 -5(合理范围 10~80)
|
||||
- 收缩压为 350 mmHg(合理范围 50~300)
|
||||
- 库存数量为 -100(不应为负)
|
||||
- 性别字段值为 3(枚举仅 1=男 / 2=女)
|
||||
- 人群分类字段值为 `"X"`(枚举仅 A/B/C/D/E)
|
||||
- 经纬度超出中国版图范围,北纬4°-53°33′,东经135°5′-73°40′
|
||||
|
||||
#### 统一取值越界参考标准
|
||||
|
||||
以下为跨模块通用的数值边界定义。各模块检测规则中的对应阈值统一以此表为准,避免同一业务含义的字段在不同模块中出现不一致的校验边界。
|
||||
|
||||
**人体测量类**
|
||||
|
||||
| 字段 | 单位 | 下限 | 上限 | 说明 |
|
||||
|------|------|:--:|:--:|------|
|
||||
| 身高 | cm | 50 | 250 | 低于50cm不具备成年人体征,高于250cm超出人类身高纪录 |
|
||||
| 体重 | kg | 20 | 300 | 低于20kg不具备成年人体征,300kg为临床极端值上限 |
|
||||
| BMI | — | 10 | 80 | 10以下为生命维持极限,80以上为世界纪录的约3倍 |
|
||||
| 体脂率 | % | 2 | 60 | 2%为男性必需脂肪下限,60%超出临床病态肥胖上限 |
|
||||
| 腰围 | cm | 30 | 200 | 30cm为新生儿水平,200cm超出临床极端值 |
|
||||
| 年龄 | 岁 | 0 | 150 | 0岁为出生当年,150岁超出人类寿命极限 |
|
||||
|
||||
**生命体征类**
|
||||
|
||||
| 字段 | 单位 | 下限 | 上限 | 说明 |
|
||||
|------|------|:--:|:--:|------|
|
||||
| 收缩压 | mmHg | 50 | 300 | 50mmHg为濒危休克值,300mmHg超出血压计量程 |
|
||||
| 舒张压 | mmHg | 30 | 200 | 30mmHg为濒危休克值,200mmHg超出临床极端值 |
|
||||
| 心率 | bpm | 20 | 300 | 20bpm为深度休眠/濒危值,300bpm超出心室颤动极限 |
|
||||
| 血氧饱和度 | % | 50 | 100 | 50%以下为致命性低氧,100%为理论饱和上限 |
|
||||
| 体温 | ℃ | 30 | 45 | 30℃为致命性低温,45℃超出人体存活上限 |
|
||||
| 空腹血糖 | mmol/L | 0 | 50 | 0为理论下界,50mmol/L超出临床极端高值 |
|
||||
|
||||
**时间度量类**
|
||||
|
||||
| 字段 | 单位 | 下限 | 上限 | 说明 |
|
||||
|------|------|:--:|:--:|------|
|
||||
| 睡眠时长 | 分钟 | 0 | 1440 | 0为未睡眠,1440为一天总分钟数 |
|
||||
|
||||
**地理坐标类**
|
||||
|
||||
| 字段 | 单位 | 下限 | 上限 | 说明 |
|
||||
|------|------|:--:|:--:|------|
|
||||
| 经度(中国境内) | 度 | 73 | 136 | 东经73°40′(新疆阿克赛钦)~ 东经135°5′(黑龙江抚远),取整 |
|
||||
| 纬度(中国境内) | 度 | 4 | 54 | 北纬4°(南沙群岛曾母暗沙)~ 北纬53°33′(黑龙江漠河),取整 |
|
||||
|
||||
**评分与占比类**
|
||||
|
||||
| 字段 | 单位 | 下限 | 上限 | 说明 |
|
||||
|------|------|:--:|:--:|------|
|
||||
| 评分(5分制) | 分 | 0 | 5 | 适用于专家评分、咨询评分等五级评分体系 |
|
||||
| 百分比 | % | 0 | 100 | 适用于回复率、电量百分比、完成率等百分比字段 |
|
||||
| pH 值 | — | 0 | 14 | 水溶液酸碱度理论范围 |
|
||||
|
||||
**与格式违规的区别**:
|
||||
- 格式违规关注"值的形态"(如"abc"不是数字)
|
||||
- 取值越界关注"值的量级"(如 999 是合法数字但超出了合理上限)
|
||||
|
||||
---
|
||||
|
||||
#### D04 — 逻辑冲突
|
||||
|
||||
**定义**:单条记录内多个字段之间存在不可调和的值矛盾,或多条关联记录之间的汇总/引用关系不成立。
|
||||
|
||||
**判定逻辑**:
|
||||
- 同一行内两字段比较(如 `end_time < start_time`、`sbp < dbp`)
|
||||
- 一对多汇总不一致(如 `total_amount != SUM(subtotal)`)
|
||||
- 状态与时间戳矛盾(如状态=已完成但 end_time 为空)
|
||||
- 标记与明细不一致(如 is_exception=1 但所有子项指标均正常)
|
||||
|
||||
**典型示例**:
|
||||
- 咨询结束时间早于开始时间
|
||||
- 订单总金额不等于明细小计之和
|
||||
- 会话状态为"进行中"但结束时间已填充
|
||||
- 收缩压(110)低于舒张压(85)
|
||||
- 睡眠时长 + 运动时长 > 1440 分钟(超过一天)
|
||||
|
||||
**注意事项**:
|
||||
- 逻辑冲突规则需要深入理解业务语义,建议由产品经理与研发联合确认
|
||||
- 统计类字段(如 `statics_num`)与源表记录数的一致性校验属于此类,但因实时性要求低,可在每日全量对账时执行
|
||||
|
||||
---
|
||||
|
||||
#### D05 — 关联断裂
|
||||
|
||||
**定义**:记录中的外键字段指向了一个在关联表中已不存在的 ID。
|
||||
|
||||
**判定逻辑**:
|
||||
- `foreign_key IS NOT NULL AND foreign_key NOT IN (SELECT id FROM referenced_table)`
|
||||
|
||||
**典型示例**:
|
||||
- 咨询记录中的 `expert_id` 在专家表中不存在(专家已删除)
|
||||
- 订单中的 `user_id` 在用户表中不存在
|
||||
- 评价记录中的 `consult_id` 在咨询表中不存在
|
||||
- AED 设备部署点位关联的应急中心 ID 无效
|
||||
|
||||
**注意事项**:
|
||||
- 仅在外键值非空时检测(允许 NULL 的外键不触发此规则,因为 NULL 表示"未关联"而非"关联无效")
|
||||
- 软删除场景需确认关联表的过滤条件(如 `del_falg IS NULL`)
|
||||
|
||||
---
|
||||
|
||||
#### D06 — 数据重复
|
||||
|
||||
**定义**:同一业务实体在违反唯一性约束的情况下出现了多条记录。
|
||||
|
||||
**判定逻辑**:
|
||||
- 按业务唯一键分组计数:`COUNT(*) > 1 GROUP BY business_unique_key`
|
||||
|
||||
**典型示例**:
|
||||
- 同一用户同一年度存在多条活跃人群分类记录
|
||||
- 同一员工同一活动重复报名
|
||||
- 同一设备同一时间点存在多条监测数据记录
|
||||
- 同一用户同一天存在多条营养分析结果记录
|
||||
|
||||
**注意事项**:
|
||||
- 业务唯一键 ≠ 数据库唯一索引。需根据业务语义定义去重维度
|
||||
- 区分"合理的多条记录"(如一个用户多次咨询)与"异常的多条记录"(如一个用户同一时刻两条档案)
|
||||
|
||||
---
|
||||
|
||||
## 三、脏数据与结果异常的边界
|
||||
|
||||
### 3.1 判定原则
|
||||
|
||||
> **核心原则**:系统脏数据关注"数据本身是否合法",结果异常关注"业务结果是否正常"。前者是数据质量的范畴,后者是业务监控的范畴。
|
||||
|
||||
### 3.2 边界对照表
|
||||
|
||||
| 场景 | 数据是否合法 | 分类 | 理由 |
|
||||
|------|:--:|------|------|
|
||||
| 血压值为 -50 mmHg | 否 | **脏数据 (D03)** | 数值超出物理极限 |
|
||||
| JSON 字段解析失败 | 否 | **脏数据 (D02)** | 格式不符合规范 |
|
||||
| 员工编号为空 | 否 | **脏数据 (D01)** | 必填字段缺失 |
|
||||
| 订单关联的用户已删除 | 否 | **脏数据 (D05)** | 外键关联失效 |
|
||||
| 同一用户同日两条档案 | 否 | **脏数据 (D06)** | 违反唯一性约束 |
|
||||
| 收缩压 110 < 舒张压 85 | 否 | **脏数据 (D04)** | 字段间逻辑矛盾 |
|
||||
| 血压值 185/110 mmHg | **是** | 结果异常 | 值在物理范围内,但超出医学正常参考范围 |
|
||||
| 工单创建后 7 天未关闭 | **是** | 结果异常 | 状态值和时长均合法,属业务流程超时 |
|
||||
| 专家评分 1.5 分(满分 5) | **是** | 结果异常 | 评分值在合法范围内,属服务质量问题 |
|
||||
| 咨询会话超过 60 分钟 | **是** | 结果异常 | 时长值合法,属服务效率问题 |
|
||||
| 设备超过 3 天未上报数据 | **是** | 结果异常 | 数据本身无异常,属设备离线告警 |
|
||||
| ICU 患者血糖 22 mmol/L | **是** | 结果异常 | 值本身合法,属危急值预警 |
|
||||
|
||||
### 3.3 典型易混淆场景澄清
|
||||
|
||||
**场景一:咨询时长异常**
|
||||
|
||||
研发文档中健康咨询模块的 `exception_type=1`(咨询时长异常 > 60 分钟)被归类为系统脏数据,实为**结果异常**。`duration_minutes` 字段的值 61、120 等在格式和范围内均合法,只是反映了"这个咨询用时很长"这一业务事实——这是服务质量监控指标,不是数据质量问题。
|
||||
|
||||
**场景二:任务状态卡住**
|
||||
|
||||
研发文档中多处出现 `status = 处理中 超过 24 小时` 的规则。若状态字段值本身合法(确实为"处理中"),则属于结果异常(任务卡住);但若状态值与时间戳存在矛盾(如状态=已完成但 end_time 为空),则为脏数据(D04 逻辑冲突)。
|
||||
|
||||
**场景三:评价分数极低**
|
||||
|
||||
评分字段值 1 在 1~5 范围内合法,属于结果异常(服务评价差)。只有当 score 值超出 1~5 范围时,才属于脏数据(D03 取值越界)。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据中台质量校验流程
|
||||
|
||||
### 4.1 整体架构
|
||||
|
||||
系统脏数据的检测由数据中台统一执行,不依赖各业务服务自行实现定时任务。质量校验嵌入在数据中台从业务数据库同步数据的管道中,实现"同步即校验"。
|
||||
|
||||
```
|
||||
┌──────────────┐ CDC/增量同步 ┌────────────────────┐
|
||||
│ 业务数据库 │ ──────────────────> │ 数据中台 │
|
||||
│ (postgreSQL) │ │ │
|
||||
│ │ │ ┌──────────────┐ │
|
||||
│ health_ │ │ │ DQC 规则引擎 │ │
|
||||
│ consult │ │ │ (同步即校验) │ │
|
||||
│ emergency │ │ └──────┬───────┘ │
|
||||
│ exam │ │ │ │
|
||||
│ nutrition │ │ ┌────▼───────┐ │
|
||||
│ monitor │ │ │ 脏数据存储库 │ │
|
||||
│ weight │ │ │ (汇总查询) │ │
|
||||
│ ... │ │ └────────────┘ │
|
||||
└──────────────┘ └────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 校验触发时机
|
||||
|
||||
| 同步模式 | 触发时机 | 适用场景 |
|
||||
|----------|----------|----------|
|
||||
| **CDC 实时同步** | 每捕获一条 INSERT/UPDATE 变更事件,即对该记录执行 DQC 规则校验 | 数据缺失、格式违规、取值越界、逻辑冲突(单行)、枚举非法 |
|
||||
| **增量批次同步** | 每个批次(如每 5 分钟一批)同步完成后,对批次内记录执行跨表校验 | 关联断裂 |
|
||||
| **每日全量对账** | 每日凌晨,对全量/增量数据进行汇总级校验 | 逻辑冲突(汇总不一致)、数据重复 |
|
||||
|
||||
### 4.3 校验管线
|
||||
|
||||
```
|
||||
数据变更事件 (CDC Event)
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 1. 字段映射 │ 将业务表字段映射为标准 DQC 字段模型
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 2. 规则匹配 │ 根据「表名.字段名」匹配对应 DQC 规则集
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 3. 规则执行 │ 逐条执行规则 → 输出 (通过 / 不通过 + 异常详情)
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 4. 结果路由 │ 通过 → 数据入仓
|
||||
│ │ 不通过 → 写入脏数据表 + 同步告警(可选)
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 脏数据记录模型
|
||||
|
||||
校验发现脏数据后,统一写入以下结构的异常记录:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|--------------------|------|------------------------------------------------------------------------|
|
||||
| `id` | bigint | 主键 |
|
||||
| `module` | varchar | 业务板块(health_consult / emergency / exam / nutrition / monitor / weight) |
|
||||
| `dirty_type` | varchar | 脏数据类型编码(D01 ~ D06) |
|
||||
| `table_name` | varchar | 来源表名 |
|
||||
| `table_desc` | varchar | 业务表描述(设备主表、咨询记录表、应急工单表等) |
|
||||
| `field_name` | varchar | 异常字段名(多字段时用逗号分隔) |
|
||||
| `record_id` | varchar | 异常记录的主键/业务编号 |
|
||||
| `abnormal_value` | text | 异常值 |
|
||||
| `rule_description` | text | 触发的规则描述 |
|
||||
| `detected_at` | datetime | 检测时间 |
|
||||
| `data_created_at` | datetime | 异常数据的业务产生时间(来自源表时间戳字段) |
|
||||
| `status` | varchar | 处理状态(待处理 / 已忽略 / 已修复) |
|
||||
|
||||
### 4.5 告警与处置
|
||||
|
||||
- **系统脏数据查询**:在各业务版块下的异常事件-系统脏数据中提供脏查询功能
|
||||
- **实时告警(暂不实现)**:D01~D03 类型(数据缺失、格式违规、取值越界)发现后即时推送告警,通知对应板块的数据负责人
|
||||
- **定时报告(暂不实现)**:D04~D06 类型(逻辑冲突、关联断裂、数据重复)汇总至每日数据质量日报,统一呈现
|
||||
- **处置闭环((暂不实现))**:业务端通过"异常事件 → 系统脏数据"页面查看、筛选、导出,确认后可标记为"已忽略"或修复源数据后自动复检清除
|
||||
|
||||
---
|
||||
|
||||
## 五、各模块检测规则
|
||||
|
||||
> 以下规则按「业务域 → 表 → 规则明细」三层结构组织,每个模块统一格式。
|
||||
|
||||
### 5.1 系统管理
|
||||
|
||||
#### 涉及数据表
|
||||
|
||||
系统管理是平台的基础支撑模块,管理用户、角色、组织架构、字典、配置等核心公共数据。以下仅列出直接影响业务数据完整性的核心校验表。
|
||||
|
||||
| 业务子域 | 核心表 | 关键校验字段 |
|
||||
|----------|--------|-------------|
|
||||
| 用户与角色 | `sys_user`, `sys_user_extend`, `sys_role`, `sys_user_role` | username, workNo, phone, idCard, sex, status, accountStatus, personType, birthday, height, weight, bmi, roleName, roleCode |
|
||||
| 组织机构 | `sys_depart` | departName, orgCode, orgCategory, orgType, status, lng, lat, parentId |
|
||||
| 字典与配置 | `sys_dict`, `sys_dict_item`, `sys_config` | name, code, itemName, itemValue, configKey, configName |
|
||||
|
||||
#### 检测规则
|
||||
|
||||
**sys_user(用户信息)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S01-D01 | 登录账号缺失 | `username` | `IS NULL OR = ''` |
|
||||
| S01-D01 | 工号缺失 | `workNo` | `IS NULL OR = ''` |
|
||||
| S01-D02 | 手机号格式不合法 | `phone` | 不匹配 11 位手机号正则(如 `"888"` 或非数字字符) |
|
||||
| S01-D02 | 身份证号格式不合法 | `idCard` | 长度不为 18 位,或含非法字符,或校验位不通过 |
|
||||
| S01-D03 | 性别枚举越界 | `sex` | `NOT IN (0, 1, 2)`(0=未知, 1=男, 2=女) |
|
||||
| S01-D03 | 员工状态枚举越界 | `status` | `NOT IN (1, 2, 3)`(1=在岗, 2=封存, 3=注销) |
|
||||
| S01-D03 | 账号状态枚举越界 | `accountStatus` | `NOT IN (0, 1)`(0=锁定, 1=激活) |
|
||||
| S01-D03 | 用户类型枚举越界 | `personType` | `NOT IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17)` |
|
||||
| S01-D03 | 生日为未来日期 | `birthday` | `birthday > CURRENT_DATE` |
|
||||
| S01-D06 | 工号重复 | `workNo` | 存在多条记录使用同一工号(排除已逻辑删除记录) |
|
||||
|
||||
**sys_role(角色)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S02-D01 | 角色名称缺失 | `roleName` | `IS NULL OR = ''` |
|
||||
| S02-D01 | 角色编码缺失 | `roleCode` | `IS NULL OR = ''` |
|
||||
| S02-D03 | 角色类型枚举越界 | `roleType` | `NOT IN (0, 1)`(0=自定义, 1=系统内置) |
|
||||
| S02-D06 | 角色编码重复 | `roleCode` | 存在多条记录使用同一角色编码 |
|
||||
|
||||
**sys_user_role(用户角色关联)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S03-D05 | 关联用户不存在 | `userId` | 引用的用户不存在 |
|
||||
| S03-D05 | 关联角色不存在 | `roleId` | 引用的角色不存在 |
|
||||
| S03-D06 | 用户角色重复分配 | `userId`, `roleId` | 同一用户被重复分配同一角色 |
|
||||
|
||||
**sys_depart(组织机构)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S04-D01 | 机构名称缺失 | `departName` | `IS NULL OR = ''` |
|
||||
| S04-D01 | 机构编码缺失 | `orgCode` | `IS NULL OR = ''` |
|
||||
| S04-D03 | 机构类别枚举越界 | `orgCategory` | `NOT IN ('1', '2', '3')`(1=公司, 2=组织机构, 3=岗位) |
|
||||
| S04-D03 | 机构类型枚举越界 | `orgType` | `NOT IN ('1', '2')`(1=一级部门, 2=子部门) |
|
||||
| S04-D03 | 状态枚举越界 | `status` | `NOT IN ('0', '1')`(0=不启用, 1=启用) |
|
||||
| S04-D03 | 经纬度超出国内范围 | `lng`, `lat` | `lng < 73 OR lng > 136 OR lat < 4 OR lat > 54`(含 (0,0) 未初始化场景) |
|
||||
| S04-D04 | 父机构指向自身 | `id`, `parentId` | `id = parentId` |
|
||||
| S04-D05 | 关联父机构不存在 | `parentId` | 引用的父机构不存在 |
|
||||
| S04-D06 | 机构编码重复 | `orgCode` | 存在多条记录使用同一机构编码 |
|
||||
|
||||
**sys_dict(字典主表)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S05-D01 | 字典名称缺失 | `name` | `IS NULL OR = ''` |
|
||||
| S05-D01 | 字典编码缺失 | `code` | `IS NULL OR = ''` |
|
||||
| S05-D06 | 字典编码重复 | `code` | 存在多条记录使用同一字典编码 |
|
||||
|
||||
**sys_dict_item(字典项)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S06-D01 | 字典项名称缺失 | `itemName` | `IS NULL OR = ''` |
|
||||
| S06-D01 | 字典项值缺失 | `itemValue` | `IS NULL OR = ''` |
|
||||
| S06-D03 | 字典项状态枚举越界 | `status` | `NOT IN (0, 1)`(0=不启用, 1=启用) |
|
||||
| S06-D05 | 关联字典不存在 | `dictId` | 引用的字典不存在 |
|
||||
| S06-D06 | 同一字典下字典项值重复 | `dictId`, `itemValue` | 同一字典下存在重复的字典项值 |
|
||||
|
||||
**sys_config(参数配置)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S07-D01 | 配置名称缺失 | `configName` | `IS NULL OR = ''` |
|
||||
| S07-D01 | 配置键缺失 | `configKey` | `IS NULL OR = ''` |
|
||||
| S07-D06 | 配置键重复 | `configKey` | 存在多条记录使用同一配置键 |
|
||||
|
||||
**sys_user_extend(用户扩展信息)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| S08-D03 | 身高越界 | `height` | `< 50 OR > 250` (cm) |
|
||||
| S08-D03 | 体重越界 | `weight` | `< 20 OR > 300` (kg) |
|
||||
| S08-D03 | BMI 越界 | `bmi` | `< 10 OR > 80` |
|
||||
| S08-D05 | 关联用户不存在 | `userId` | 引用的用户不存在 |
|
||||
|
||||
---
|
||||
|
||||
### 5.2 健康咨询
|
||||
|
||||
#### 涉及数据表
|
||||
|
||||
| 表名 | 说明 | 核心校验字段 |
|
||||
|------|------|-------------|
|
||||
| `con_consult` | 咨询会话主表 | consult_no, consult_type, content_status, session_status, start_time, end_time, duration_minutes, cost, expert_id, member_id |
|
||||
| `con_expert` | 专家信息表 | expert_no, expert_type, overall_merit, response_rate, reg_date, off_date, hospital_id, dept_id |
|
||||
| `con_evaluate` | 咨询评价表 | score, consult_id, expert_id |
|
||||
| `con_family_member` | 家庭成员表 | member_name, birthday, height, weight, sex |
|
||||
| `con_medical_record` | 病历档案表 | medical_describe, duration_type |
|
||||
| `con_im_message` | IM 消息记录表 | msg_content, msg_type, consult_id |
|
||||
| `con_expert_schedule_rule` | 专家排班规则表 | week_day, period, start_time, end_time, capacity |
|
||||
| `con_settle_main` | 结算主表 | settle_status, com_fee_per_month |
|
||||
|
||||
#### 检测规则
|
||||
|
||||
**con_consult(咨询会话)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| C01-D01 | 咨询单号缺失 | `consult_no` | `IS NULL OR = ''` |
|
||||
| C01-D03 | 咨询类型枚举越界 | `consult_type` | `NOT IN (1, 2)` |
|
||||
| C01-D03 | 内容状态枚举越界 | `content_status` | `NOT IN (1,2,3,4,5,6,7,8)` |
|
||||
| C01-D03 | 咨询费用为负数 | `cost` | `< 0` |
|
||||
| C01-D04 | 结束时间早于开始时间 | `start_time`, `end_time` | 结束时间早于开始时间 |
|
||||
| C01-D04 | 会话进行中但已有结束时间 | `session_status`, `end_time` | 会话状态为进行中但已有结束时间 |
|
||||
| C01-D04 | 内容已完成但无结束时间 | `content_status`, `end_time` | 内容已完成但无结束时间 |
|
||||
| C01-D04 | 已拒绝但无拒绝原因 | `content_status`, `reject_reason` | 已拒绝但未填写拒绝原因 |
|
||||
| C01-D05 | 关联专家不存在 | `expert_id` | 引用的专家不存在 |
|
||||
| C01-D05 | 关联咨询人不存在 | `member_id` | 引用的家庭成员不存在 |
|
||||
|
||||
**con_expert(专家信息)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| C02-D01 | 专家编号缺失 | `expert_no` | `IS NULL OR = ''` |
|
||||
| C02-D03 | 专家类型枚举越界 | `expert_type` | `NOT IN (1, 2, 3)` |
|
||||
| C02-D03 | 综合评分越界 | `overall_merit` | `< 0 OR > 5` |
|
||||
| C02-D03 | 回复率越界 | `response_rate` | `< 0 OR > 100` |
|
||||
| C02-D04 | 离职日期早于入职日期 | `reg_date`, `off_date` | 离职日期早于入职日期 |
|
||||
| C02-D05 | 关联医院不存在 | `hospital_id` | 引用的医院不存在 |
|
||||
| C02-D05 | 关联科室不存在 | `dept_id` | 引用的科室不存在 |
|
||||
|
||||
**con_evaluate(咨询评价)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| C03-D03 | 评分越界 | `score` | `< 0 OR > 5` |
|
||||
| C03-D05 | 关联咨询不存在 | `consult_id` | 引用的咨询记录不存在 |
|
||||
| C03-D05 | 关联专家不存在 | `expert_id` | 引用的专家不存在 |
|
||||
|
||||
**con_family_member(家庭成员)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| C04-D03 | 身高越界 | `height` | `< 50 OR > 250` |
|
||||
| C04-D03 | 体重越界 | `weight` | `< 20 OR > 300` |
|
||||
| C04-D03 | 性别枚举越界 | `sex` | `NOT IN (1, 2)` |
|
||||
| C04-D03 | 生日为未来日期 | `birthday` | `birthday > CURRENT_DATE` |
|
||||
|
||||
**con_medical_record(病历档案)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| C05-D01 | 病情描述缺失 | `medical_describe` | `IS NULL OR = ''` |
|
||||
| C05-D03 | 持续时间类型枚举越界 | `duration_type` | `NOT IN (1, 2, 3, 4)` |
|
||||
|
||||
**con_im_message(IM 消息)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| C06-D01 | 消息内容缺失(非撤回消息) | `msg_content` | 消息内容为空且消息类型非撤回 |
|
||||
| C06-D03 | 消息类型枚举越界 | `msg_type` | `NOT IN (1, 2, 3, 4)` |
|
||||
| C06-D05 | 关联咨询不存在 | `consult_id` | 引用的咨询记录不存在 |
|
||||
|
||||
**con_expert_schedule_rule(排班规则)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| C07-D03 | 星期枚举越界 | `week_day` | `NOT IN (1,2,3,4,5,6,7)` |
|
||||
| C07-D03 | 时段枚举越界 | `period` | `NOT IN (1, 2, 3, 4)` |
|
||||
| C07-D03 | 接诊人数为负数 | `capacity` | `< 0` |
|
||||
| C07-D04 | 排班结束时间早于开始时间 | `start_time`, `end_time` | `end_time < start_time` |
|
||||
|
||||
---
|
||||
|
||||
### 5.3 应急就医
|
||||
|
||||
#### 涉及数据表
|
||||
|
||||
| 表名 | 说明 | 核心校验字段 |
|
||||
|------|------|-------------|
|
||||
| `em_emergency_order` | 应急工单 | order_no, status, location_lat, location_lng, closed_at |
|
||||
| `em_call_record` | 通话记录 | incoming_phone, match_status, order_id, begin_time, end_time, answer_status, duration |
|
||||
| `em_aed_device` | AED 设备 | battery_level, battery_status |
|
||||
| `em_aed_deploy_point` | AED 部署点位 | longitude, latitude |
|
||||
| `em_aed_latest` | AED 最新状态 | battery_power |
|
||||
| `em_aed_maintenance` | AED 维护记录 | maintenance_date |
|
||||
| `em_user_profile` | 应急人员档案 | cert_number, license_number, station_status |
|
||||
| `em_ambulance` | 救护车 | plate_no, status |
|
||||
| `em_emergency_center` | 应急中心 | longitude, latitude, center_phone, status |
|
||||
| `em_emergency_center_call_record` | 应急中心通话 | call_id, employee_id, begin_time, end_time |
|
||||
| `em_emergency_center_call_join` | 通话参与人 | employee_id, call_record_id |
|
||||
|
||||
#### 检测规则
|
||||
|
||||
**em_call_record(通话记录)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E01-D02 | 来电号码格式不合法 | `incoming_phone` | 不匹配手机号/座机号正则 |
|
||||
| E01-D03 | 通话时长负数 | `duration` | `< 0` |
|
||||
| E01-D04 | 结束时间早于开始时间 | `begin_time`, `end_time` | 结束时间早于开始时间 |
|
||||
| E01-D04 | 已接听但通话时长为 0 | `answer_status`, `duration` | 已接听但通话时长为零 |
|
||||
| E01-D05 | 关联工单不存在 | `order_id` | 引用的应急工单不存在 |
|
||||
|
||||
**em_aed_device / em_aed_latest(AED 设备)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E02-D03 | 电池电量百分比越界 | `battery_level` | 电池电量百分比不在 0~100 范围内 |
|
||||
| E02-D03 | 电池状态枚举越界 | `battery_status` | 不在预定义电池状态枚举集合内 |
|
||||
|
||||
**em_aed_deploy_point(AED 部署点位)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E03-D03 | GPS 坐标超出国内范围 | `longitude`, `latitude` | `longitude < 73 OR longitude > 136 OR latitude < 4 OR latitude > 54` |
|
||||
| E03-D01 | GPS 坐标为 (0, 0) 或 NULL | `longitude`, `latitude` | GPS 坐标为空值或为 (0, 0) |
|
||||
| E03-D05 | 关联应急中心不存在 | 中心关联字段 | 引用的应急中心不存在 |
|
||||
|
||||
**em_aed_maintenance(AED 维护记录)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E04-D03 | 维护日期为未来日期 | `maintenance_date` | `maintenance_date > CURRENT_DATE` |
|
||||
| E04-D01 | 设备缺少维护记录 | — | 设备已部署超过阈值天数且无维护记录 |
|
||||
|
||||
**em_emergency_order(应急工单)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E05-D01 | 工单编号缺失 | `order_no` | `IS NULL OR = ''` |
|
||||
| E05-D03 | GPS 坐标超出国内范围 | `location_lat`, `location_lng` | 同 E03-D03 |
|
||||
| E05-D04 | 已关闭但无关闭时间 | `status`, `closed_at` | 已关闭但无关闭时间 |
|
||||
|
||||
**em_user_profile(应急人员档案)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E06-D01 | 证书编号缺失 | `cert_number` | `IS NULL OR = ''` |
|
||||
| E06-D03 | 驻场状态枚举越界 | `station_status` | 不在预定义状态枚举集合内 |
|
||||
|
||||
**em_ambulance(救护车)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E07-D01 | 车牌号缺失 | `plate_no` | `IS NULL OR = ''` |
|
||||
| E07-D03 | 车辆状态枚举越界 | `status` | 不在预定义状态枚举集合内 |
|
||||
|
||||
**em_emergency_center_call_record(应急中心通话)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| E08-D04 | 结束时间早于开始时间 | `begin_time`, `end_time` | 结束时间早于开始时间 |
|
||||
| E08-D01 | 通话ID缺失 | `call_id` | `IS NULL OR = ''` |
|
||||
|
||||
---
|
||||
|
||||
### 5.4 健康评估
|
||||
|
||||
#### 涉及数据表
|
||||
|
||||
健康评估模块对员工进行多维度健康评估,涵盖风险评估、心理评估、规律起居评估及人群分类。以下按业务子域列出核心校验表。
|
||||
|
||||
| 业务子域 | 核心表 | 关键校验字段 |
|
||||
|----------|--------|-------------|
|
||||
| 风险评估 | `ass_risk_evaluation`, `ass_risk_result`, `ass_risk_questionnaire_info`, `ass_risk_evaluation_statistics`, `ass_risk_diet_sports_prescription` | user_id, eval_source, status, is_exception, bmi, sbp, dbp, fasting_bg, health_age, bmi_mark, smoke_mark, height, weight, waist_line, smoking_status, personal_*, questions_json, statics_num, exception_num, energy_level, f_elements, aerobic_*, strength_*, flexibility_* |
|
||||
| 心理评估 | `ass_psychology`, `ass_psychology_statistics` | type, grade, phy_grade_level, status, is_exception, questions_id |
|
||||
| 规律起居 | `ass_daily_routine_online`, `ass_daily_routine_paper` | sleep_hour, sport_minute, resting_heart_rate, oxygen_saturation, pressure_value, week_start_date, week_end_date, sport_value, sleep_value, time_of_net_value, sport_level, sleep_type, time_of_net_level, is_exception |
|
||||
| 人群分类 | `ass_group_user`, `ass_group_user_detail` | user_group, group_change_type, audit_status, is_archived, archived_time, date_year, latest_assessment_date, detail_type, data_type |
|
||||
| 问卷 | `ass_questions` | questions_json |
|
||||
|
||||
#### 检测规则
|
||||
|
||||
**ass_risk_evaluation(风险评估主表)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A01-D01 | 用户ID缺失 | `user_id` | `IS NULL` |
|
||||
| A01-D03 | 评估来源枚举越界 | `eval_source` | `NOT IN (1, 2)`(1=问卷, 2=自动) |
|
||||
| A01-D04 | 异常标记与评估结果矛盾 | `is_exception` ↔ `ass_risk_result.*_level` | `is_exception = 1` 但关联结果中所有疾病等级均为正常 |
|
||||
|
||||
**ass_risk_result(风险评估结果)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A02-D03 | BMI 值越界 | `bmi` | `< 10 OR > 80` |
|
||||
| A02-D03 | 收缩压越界 | `sbp` | `< 50 OR > 300` (mmHg) |
|
||||
| A02-D03 | 舒张压越界 | `dbp` | `< 30 OR > 200` (mmHg) |
|
||||
| A02-D03 | 空腹血糖越界 | `fasting_bg` | `< 0 OR > 50` (mmol/L) |
|
||||
| A02-D03 | 健康年龄越界 | `health_age`, `potential_health_age` | `< 0 OR > 150` |
|
||||
| A02-D03 | 疾病等级枚举越界 | `diabetes_level`, `hypertension_level` 等 | 不在预定义枚举集合内 |
|
||||
| A02-D02 | 生活习惯标记格式非法 | `bmi_mark`, `smoke_mark`, `sleep_mark` 等 | 不符合 `state,score,fullmark` 格式 |
|
||||
| A02-D04 | 收缩压低于舒张压 | `sbp`, `dbp` | `sbp < dbp` |
|
||||
| A02-D04 | 血脂数据缺失但疾病等级非空 | `cholesterol`, `triglycerin`, `hdlc`, `ldlc` + `*_level` | 血脂四项均为 0 或 NULL 但存在非空疾病等级 |
|
||||
|
||||
**ass_risk_questionnaire_info(问卷信息)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A03-D01 | 基础体测数据完全缺失 | `height`, `weight`, `waist_line` | 三者均为 NULL |
|
||||
| A03-D03 | 身高越界 | `height` | `< 50 OR > 250` (cm) |
|
||||
| A03-D03 | 体重越界 | `weight` | `< 20 OR > 300` (kg) |
|
||||
| A03-D03 | 腰围越界 | `waist_line` | `< 30 OR > 200` (cm) |
|
||||
| A03-D03 | 吸烟状态枚举越界 | `smoking_status` | `NOT IN (1, 2, 3)` |
|
||||
| A03-D03 | 个人病史枚举越界 | `personal_diabetes`, `personal_hypertension` 等 `personal_*` 字段 | `NOT IN (0, 1)` |
|
||||
| A03-D02 | 问卷 JSON 格式非法 | `questions_json`(通过 `ass_questions`) | JSON 解析失败 |
|
||||
|
||||
**ass_risk_evaluation_statistics(评估统计)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A04-D04 | 统计次数与实际评估次数不一致 | `statics_num` ↔ `ass_risk_evaluation` | `statics_num` 不等于该用户实际评估记录数 |
|
||||
| A04-D04 | 疾病异常标记与源数据脱节 | `is_bmi_abnormal`, `is_sbp_abnormal` 等 ↔ `last_evaluation_id` 对应结果 | 统计标记与引用的评估结果数据不一致 |
|
||||
| A04-D04 | 异常计数错误 | `exception_num` | `exception_num` 不等于实际 `is_exception = 1` 的记录数 |
|
||||
|
||||
**ass_psychology / ass_psychology_statistics(心理评估)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A05-D01 | 评估得分或健康等级缺失 | `grade`, `phy_grade_level` | 任一为 NULL |
|
||||
| A05-D03 | 评估类型枚举越界 | `type` | `NOT IN (1, 2, 3, 4)`(1=综合, 2=压力, 3=焦虑, 4=抑郁) |
|
||||
| A05-D03 | 评估得分超出合理范围 | `grade` | 超出该评估量表的标准分值范围 |
|
||||
| A05-D03 | 健康等级编码越界 | `phy_grade_level` | 不在数据字典预定义编码范围内 |
|
||||
| A05-D04 | 异常标记与得分矛盾 | `is_exception`, `grade` | `is_exception = 1` 但 `grade` 在正常范围内 |
|
||||
| A05-D04 | 统计值与实际评估记录数不一致 | `ass_psychology_statistics` 统计字段 | 统计值不等于 `ass_psychology` 中实际记录数 |
|
||||
| A05-D05 | 关联问卷不存在 | `questions_id` | 引用的问卷在 `ass_questions` 中不存在 |
|
||||
|
||||
**ass_daily_routine_online(在线穿戴起居评估)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A06-D01 | 异常标记缺失 | `is_exception` | `IS NULL` |
|
||||
| A06-D03 | 睡眠时长越界 | `sleep_hour` | `< 0 OR > 1440`(分钟) |
|
||||
| A06-D03 | 运动时长越界 | `sport_minute` | `< 0 OR > 1440`(分钟) |
|
||||
| A06-D03 | 静息心率越界 | `resting_heart_rate` | `< 20 OR > 300` (bpm) |
|
||||
| A06-D03 | 血氧饱和度越界 | `oxygen_saturation` | `< 50 OR > 100` (%) |
|
||||
| A06-D03 | 压力值超出量程 | `pressure_value` | 超出传感器标定量程范围 |
|
||||
| A06-D04 | 周结束日期早于开始日期 | `week_start_date`, `week_end_date` | `week_end_date < week_start_date` |
|
||||
| A06-D04 | 睡眠+运动超过 24 小时 | `sleep_hour`, `sport_minute` | `sleep_hour + sport_minute > 1440` |
|
||||
|
||||
**ass_daily_routine_paper(问卷起居评估)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A07-D01 | 异常标记缺失 | `is_exception` | `IS NULL` |
|
||||
| A07-D03 | 运动/睡眠/上网时长为负数 | `sport_value`, `sleep_value`, `time_of_net_value` | 任一 < 0 |
|
||||
| A07-D03 | 上网时长超出合理范围 | `time_of_net_value` | `> 24`(小时) |
|
||||
| A07-D03 | 运动/睡眠/上网等级枚举越界 | `sport_level`, `sleep_type`, `time_of_net_level` | 不在预定义枚举集合内 |
|
||||
| A07-D05 | 关联问卷不存在 | `questions_id` | 引用的问卷在 `ass_questions` 中不存在 |
|
||||
|
||||
**ass_group_user / ass_group_user_detail(人群分类)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A08-D03 | 人群分类枚举越界 | `user_group` | `NOT IN ('A', 'B', 'C', 'D', 'E')` |
|
||||
| A08-D03 | 变更类型枚举越界 | `group_change_type` | `NOT IN (1, 2, 3)` |
|
||||
| A08-D03 | 审核状态枚举越界 | `audit_status` | `NOT IN (0, 1, 2)` |
|
||||
| A08-D03 | 归属年份越界 | `date_year` | 早于 2020 或为未来年份 |
|
||||
| A08-D03 | 最近评估日期为未来日期 | `latest_assessment_date` | `latest_assessment_date > CURRENT_DATE` |
|
||||
| A08-D03 | 明细类型枚举越界 | `detail_type` | `NOT IN ('A', 'B', 'C', 'D')` |
|
||||
| A08-D03 | 数据类型枚举越界 | `data_type` | `NOT IN (1, 2)` |
|
||||
| A08-D04 | 已归档但无归档时间 | `is_archived`, `archived_time` | `is_archived = 1` 且 `archived_time IS NULL` |
|
||||
| A08-D05 | 关联人群分类主表记录不存在 | `group_user_id` | 引用的 `ass_group_user` 记录不存在 |
|
||||
| A08-D06 | 同一用户同一年度多条活跃分类记录 | `user_id`, `date_year` | `COUNT(*) > 1 GROUP BY user_id, date_year`(排除已归档记录) |
|
||||
|
||||
**ass_questions(问卷答案)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A09-D02 | 问卷 JSON 格式非法 | `questions_json` | JSON 解析失败 |
|
||||
|
||||
**ass_risk_diet_sports_prescription(饮食运动处方)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| A10-D01 | 评估正常但运动处方字段全为空 | `aerobic_*`, `strength_*`, `flexibility_*` | 关联评估为正常但所有运动字段均为 NULL |
|
||||
| A10-D02 | 食物元素格式非法 | `f_elements` | 不符合 `meal_foodCode_amount;...` 格式 |
|
||||
| A10-D03 | 能量等级越界 | `energy_level` | `< 0 OR > 10000` (kcal) |
|
||||
|
||||
---
|
||||
|
||||
### 5.5 营养管理
|
||||
|
||||
#### 涉及数据表
|
||||
|
||||
营养管理是系统中表数量最多的模块。以下按业务子域列出核心校验表。
|
||||
|
||||
| 业务子域 | 核心表 | 关键校验字段 |
|
||||
|----------|--------|-------------|
|
||||
| 用户档案 | `nut_user`, `nut_user_profile` | phone, bmi, height, weight, birthday, health_goal, dietary_restrictions |
|
||||
| 食品管理 | `nut_food`, `nut_food_composition`, `nut_food_nutri`, `nut_food_spec` | price, nutrition_info, value_per_100g, weight |
|
||||
| 食谱管理 | `nut_recipe`, `nut_recipe_food` | total_calories, nutrition_summary, food_id, portion |
|
||||
| 订餐管理 | `nut_order`, `nut_order_item` | total_amount, status, order_type, quantity, price, subtotal |
|
||||
| 原材料 | `nut_mater_base`, `nut_raw_inbound` | price, unit, quantity, weight, supplier_id |
|
||||
| 库存 | `nut_sup_inventory`, `nut_sup_inventory_flow` | quantity, qty, current_qty_after |
|
||||
| 生产 | `nut_prod_combo_task`, `nut_prod_qc_sample`, `nut_prod_serve_record` | task_no, plan_date, status, qc_status |
|
||||
| 算法 | `nut_algo_personal`, `nut_algo_mass` | user_id, 营养结果字段 |
|
||||
| 农场 | `nut_farm_plot`, `nut_farm_crop_batch`, `nut_farm_harvest`, `nut_farm_livestock_batch` | area, plant_date, expected_harvest, quantity, count |
|
||||
| 设备 | `nut_meter_reading` | 读数合理性 |
|
||||
|
||||
#### 检测规则
|
||||
|
||||
**nut_user / nut_user_profile(用户档案)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N01-D01 | 手机号为空 | `phone` | `IS NULL OR = ''` |
|
||||
| N01-D03 | BMI 值越界 | `bmi` | `< 10 OR > 80` |
|
||||
| N01-D03 | 身高越界 | `height` | `< 50 OR > 250` |
|
||||
| N01-D03 | 体重越界 | `weight` | `< 20 OR > 300` |
|
||||
| N01-D04 | 计算年龄为负数 | `birthday` | 生日晚于当前日期 |
|
||||
| N01-D06 | 同一 user_id 存在多条档案记录 | `user_id` | 同一用户存在多条档案记录 |
|
||||
|
||||
**nut_food / nut_food_composition / nut_food_nutri / nut_food_spec(食品管理)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N02-D03 | 价格为负数 | `price` | `< 0` |
|
||||
| N02-D03 | 每 100g 营养值为负数 | `value_per_100g` | `< 0` |
|
||||
| N02-D01 | 营养信息JSON为空 | `nutrition_info` | `IS NULL OR = '' OR = '{}'` |
|
||||
| N02-D02 | 营养信息JSON解析失败 | `nutrition_info` | JSON 格式不合法,无法解析 |
|
||||
| N02-D03 | 份量重量为负数或零 | `weight` | `<= 0` |
|
||||
|
||||
**nut_recipe / nut_recipe_food(食谱管理)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N03-D01 | 总热量为空 | `total_calories` | `IS NULL` |
|
||||
| N03-D02 | 营养汇总JSON格式非法 | `nutrition_summary` | JSON 格式不合法,无法解析 |
|
||||
| N03-D05 | 食谱引用的食材ID不存在 | `food_id` | 引用的食品不存在 |
|
||||
|
||||
**nut_order / nut_order_item(订餐管理)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N04-D03 | 订单金额为负数 | `total_amount` | `< 0` |
|
||||
| N04-D03 | 单次订餐份数超出上限 | `quantity` | `quantity > 系统设定的最大份数` |
|
||||
| N04-D03 | 单价为负数 | `price` | `< 0` |
|
||||
| N04-D04 | 订单金额与明细小计之和不一致 | `total_amount` ↔ `SUM(subtotal)` | 订单金额与明细小计之和不相等 |
|
||||
|
||||
**nut_mater_base / nut_raw_inbound / nut_sup_inventory / nut_sup_inventory_flow(原材料与库存)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N05-D03 | 入库数量为负数或零 | `quantity` | `<= 0` |
|
||||
| N05-D03 | 入库重量为负数 | `weight` | `< 0` |
|
||||
| N05-D03 | 库存数量为负数 | `quantity` | `< 0` |
|
||||
| N05-D04 | 库存流水余额与当前库存不一致 | `current_qty_after` ↔ `quantity` | 库存流水余额与当前库存数量不相等 |
|
||||
| N05-D05 | 原材料ID在原料库中不存在 | `mater_id` | 引用的原材料不存在 |
|
||||
| N05-D05 | 供应商ID不存在 | `supplier_id` | 引用的供应商不存在 |
|
||||
|
||||
**nut_prod_combo_task / nut_prod_qc_sample / nut_prod_serve_record(健康生产)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N06-D02 | 生产批次号含非法字符 | `task_no` | 不匹配批次号正则 |
|
||||
| N06-D04 | 生产任务完成但无供餐记录 | `status` ↔ `nut_prod_serve_record` | 生产任务已完成但无关联供餐记录 |
|
||||
| N06-D01 | 质检样品状态为"留样中"超过阈值天数 | `qc_status`, `sample_date` | 留样中状态持续超过阈值天数 |
|
||||
| N06-D03 | 计划日期为未来年份 | `plan_date` | 年份超出合理范围 |
|
||||
|
||||
**nut_algo_personal / nut_algo_mass(算法与AI)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N07-D06 | 同一用户同日存在多条分析记录 | `user_id` + 日期 | 同一用户同日存在多条分析记录 |
|
||||
| N07-D03 | 营养素计算结果超出合理范围 | 营养结果字段 | 各营养素值 < 0 或超过人体每日上限的 N 倍 |
|
||||
|
||||
**nut_farm_plot / nut_farm_crop_batch / nut_farm_harvest / nut_farm_livestock_batch(农场)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| N08-D03 | 地块面积为负数 | `area` | `< 0` |
|
||||
| N08-D04 | 预计采收日期早于种植日期 | `plant_date`, `expected_harvest` | `expected_harvest < plant_date` |
|
||||
| N08-D03 | 采收数量为负数 | `quantity` | `< 0` |
|
||||
| N08-D03 | 牲畜数量为负数 | `count` | `< 0` |
|
||||
|
||||
---
|
||||
|
||||
### 5.6 健康监测
|
||||
|
||||
#### 涉及数据表
|
||||
|
||||
| 表名 | 说明 | 核心校验字段 |
|
||||
|------|------|-------------|
|
||||
| `mon_device` | 可穿戴设备主表 | device_code, bind_status, bind_user_id, online_status, last_data_time |
|
||||
| `mon_device_config` | 设备监测配置 | threshold_min, threshold_max, upload_frequency |
|
||||
| `mon_bind_record` | 设备绑定/解绑记录 | bind_date, unbind_date |
|
||||
| `mon_daily_stats_heart_rate` | 心率日统计 | hr_max, hr_min, hr_avg, silence_hr_avg |
|
||||
| `mon_daily_stats_spo2` | 血氧日统计 | spo2_max, spo2_min, spo2_avg |
|
||||
| `mon_daily_stats_temperature` | 体温日统计 | temp_max, temp_min, temp_avg |
|
||||
| `mon_daily_stats_blood_pressure` | 血压日统计 | systolic_max, systolic_min, diastolic_max, diastolic_min |
|
||||
| `mon_daily_stats_sleep` | 睡眠日统计 | total_min, deep_min, score |
|
||||
| `mon_data_heart_rate` | 心率原始数据 | data_value |
|
||||
| `mon_data_spo2` | 血氧原始数据 | data_value |
|
||||
| `mon_data_temperature` | 体温原始数据 | data_value |
|
||||
| `mon_data_blood_pressure` | 血压原始数据 | systolic_value, diastolic_value |
|
||||
| `mon_data_gps` | GPS 原始数据 | longitude, latitude |
|
||||
| `mon_data_ecg` | 心电原始数据 | avg_heart_rate, data_points_url |
|
||||
| `mon_survey_answer_record` | 问卷回答记录 | user_id, survey_id, answer_time |
|
||||
| 环境监测各表 | 室内/室外/花粉/水质 | 温度、PM2.5、pH、read_time、meter_code |
|
||||
|
||||
#### 检测规则
|
||||
|
||||
**mon_device(设备主表)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M01-D01 | 设备编码缺失 | `device_code` | `IS NULL OR = ''` |
|
||||
| M01-D03 | 绑定状态枚举越界 | `bind_status` | 不在预定义枚举集合内 |
|
||||
| M01-D03 | 在线状态枚举越界 | `online_status` | 不在预定义枚举集合内 |
|
||||
| M01-D04 | 设备在线但最后上报时间为未来 | `online_status`, `last_data_time` | 设备在线但最后数据上报时间为未来时间 |
|
||||
|
||||
**mon_device_config(设备配置)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M02-D03 | 阈值下限大于阈值上限 | `threshold_min`, `threshold_max` | `threshold_min > threshold_max` |
|
||||
| M02-D03 | 上报频率为负数或零 | `upload_frequency` | `<= 0` |
|
||||
|
||||
**mon_bind_record(绑定记录)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M03-D04 | 解绑日期早于绑定日期 | `bind_date`, `unbind_date` | 解绑日期早于绑定日期 |
|
||||
| M03-D02 | 绑定日期格式非法 | `bind_date` | 无法解析为有效日期 |
|
||||
|
||||
**mon_data_heart_rate / mon_data_spo2 / mon_data_temperature / mon_data_blood_pressure / mon_data_ecg(穿戴原始数据)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M04-D03 | 心率值越界 | `data_value` | `< 20 OR > 300` (bpm) |
|
||||
| M04-D03 | 血氧值越界 | `data_value` | `< 50 OR > 100` (%) |
|
||||
| M04-D03 | 体温值越界 | `data_value` | `< 30 OR > 45` (℃) |
|
||||
| M04-D03 | 收缩压越界 | `systolic_value` | `< 50 OR > 300` (mmHg) |
|
||||
| M04-D03 | 舒张压越界 | `diastolic_value` | `< 30 OR > 200` (mmHg) |
|
||||
| M04-D04 | 收缩压低于舒张压 | `systolic_value`, `diastolic_value` | `systolic_value < diastolic_value` |
|
||||
| M04-D06 | 同一设备同一时间点重复数据 | `device_id` + `data_time` | 同一设备同一时间点存在多条数据 |
|
||||
| M04-D02 | 心电数据文件URL不可达 | `data_points_url` | URL格式非法或指向不存在资源 |
|
||||
|
||||
**mon_data_gps(GPS 数据)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M05-D03 | GPS 坐标超出国内范围 | `longitude`, `latitude` | `longitude < 73 OR longitude > 136 OR latitude < 4 OR latitude > 54` |
|
||||
|
||||
**mon_daily_stats_heart_rate / mon_daily_stats_spo2 / mon_daily_stats_temperature / mon_daily_stats_blood_pressure / mon_daily_stats_sleep(日统计聚合数据)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M06-D04 | 最大心率低于最小心率 | `hr_max`, `hr_min` | `hr_max < hr_min` |
|
||||
| M06-D04 | 最大收缩压低于最小收缩压 | `systolic_max`, `systolic_min` | `systolic_max < systolic_min` |
|
||||
| M06-D04 | 最大舒张压低于最小舒张压 | `diastolic_max`, `diastolic_min` | `diastolic_max < diastolic_min` |
|
||||
| M06-D04 | 静息心率高于平均心率 | `silence_hr_avg`, `hr_avg` | `silence_hr_avg > hr_avg` |
|
||||
| M06-D03 | 睡眠总时长越界 | `total_min` | `< 0 OR > 1440` |
|
||||
| M06-D04 | 深睡时长大于总时长 | `deep_min`, `total_min` | `deep_min > total_min` |
|
||||
|
||||
**mon_survey_answer_record / mon_survey_activity(调查问卷)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M07-D04 | 回答时间早于活动发布时间 | `answer_time` ↔ `mon_survey_activity.publish_time` | `answer_time < publish_time` |
|
||||
| M07-D06 | 同一用户同一问卷多条回答 | `user_id`, `survey_id` | 同一用户对同一问卷存在多条回答 |
|
||||
|
||||
**mon_env_data_indoor / mon_env_data_outdoor / mon_env_data_pollen / mon_env_data_water(环境监测)**
|
||||
|
||||
| 编码 | 规则描述 | 涉及字段 | 判定逻辑 |
|
||||
|------|----------|----------|----------|
|
||||
| M08-D03 | PM2.5 值为负数 | PM2.5字段 | `< 0` |
|
||||
| M08-D03 | pH 值越界 | `ph` | `< 0 OR > 14` |
|
||||
| M08-D03 | 环境温度超出设备量程 | 温度字段 | 超出传感器标定量程范围 |
|
||||
| M08-D05 | 环境数据关联仪表不存在 | `meter_code` | 引用的环境监测仪表不存在 |
|
||||
|
||||
---
|
||||
|
||||
## 六、附录
|
||||
|
||||
### 6.1 脏数据类型编码速查
|
||||
|
||||
| 编码 | 名称 | 一句话描述 |
|
||||
|------|------|--------------|
|
||||
| D01 | 数据缺失 | 该有的值没有 |
|
||||
| D02 | 格式违规 | 数据值的形态不对 |
|
||||
| D03 | 取值越界 | 值的量级不对 |
|
||||
| D04 | 逻辑冲突 | 多个值互相矛盾 |
|
||||
| D05 | 关联断裂 | 外键指向了不存在的记录 |
|
||||
| D06 | 数据重复 | 本应唯一的记录出现了多条 |
|
||||
|
||||
### 6.2 规则编号说明
|
||||
|
||||
规则编号格式:`{模块前缀}{序号}-{脏数据类型编码}`
|
||||
|
||||
| 模块 | 前缀 |
|
||||
|------|------|
|
||||
| 健康咨询 | C (Consult) |
|
||||
| 应急就医 | E (Emergency) |
|
||||
| 健康评估 | A (Assessment) |
|
||||
| 营养管理 | N (Nutrition) |
|
||||
| 健康监测 | M (Monitor) |
|
||||
| 系统管理 | S (System) |
|
||||
|
||||
示例:`C01-D03` = 健康咨询模块第 1 组规则,脏数据类型为 D03(取值越界)。
|
||||
|
||||
### 6.3 各模块规则数量统计
|
||||
|
||||
| 模块 | D01 | D02 | D03 | D04 | D05 | D06 | 合计 |
|
||||
|------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
| 系统管理 | 12 | 2 | 14 | 1 | 5 | 7 | **41** |
|
||||
| 健康咨询 | 3 | 0 | 12 | 5 | 8 | 0 | **28** |
|
||||
| 应急就医 | 5 | 2 | 10 | 4 | 3 | 0 | **24** |
|
||||
| 健康评估 | 6 | 4 | 31 | 11 | 3 | 1 | **56** |
|
||||
| 营养管理 | 4 | 3 | 14 | 5 | 4 | 2 | **32** |
|
||||
| 健康监测 | 1 | 2 | 14 | 9 | 1 | 3 | **30** |
|
||||
| **合计** | **31** | **13** | **95** | **35** | **24** | **13** | **211** |
|
||||
@@ -0,0 +1,18 @@
|
||||
# 管理员端 - 变更日志
|
||||
|
||||
## [2026-06-23]
|
||||
|
||||
### 新增
|
||||
- **个人中心模块** — 新建管理员「我的」页面及子页面
|
||||
- `profile/index.html` — 个人中心主页,含蓝色渐变头部(姓名/角色/部门)、菜单卡片(修改个人信息/修改密码/关于我们)、退出登录弹窗、底部Tab导航栏
|
||||
- `profile/edit-profile.html` — 修改个人信息,支持头像更换(拍照/相册ActionSheet)、姓名编辑、保存Toast反馈
|
||||
- `profile/change-pwd.html` — 修改密码,含当前密码/新密码/确认新密码三段式表单,修改成功弹窗
|
||||
- 页面布局与交互参考驻场人员端(`onsite/profile/`),适配管理员身份(王建国/健康管理员/安全环保部·健康管理科)
|
||||
|
||||
### 修改的文件
|
||||
- `profile/index.html` - 个人中心主页
|
||||
- `profile/edit-profile.html` - 修改个人信息
|
||||
- `profile/change-pwd.html` - 修改密码
|
||||
- `index.html`(导航页) - SITE_MAP 中 admin「我的」模块补全 3 个页面条目
|
||||
- `../../ai-assistant/index.html` - roleBaseMap 新增 admin 角色路由
|
||||
- `../../message/index.html` - roleBaseMap 新增 admin 角色路由
|
||||
@@ -0,0 +1,522 @@
|
||||
# 管理员端(健康应急APP)产品需求文档
|
||||
|
||||
## 模块概述
|
||||
|
||||
管理员端为健康应急APP的管理与配置角色提供移动端工作平台,负责监控体重管理活动的执行进度、追踪员工体检完成情况、查看应急工单处理状态以及穿戴设备异常告警记录。管理员通过数据看板掌握各业务线运行状况,并对体重管理目标进行审核把关。
|
||||
|
||||
### 页面导航结构
|
||||
|
||||
```
|
||||
管理员端
|
||||
├── 工作台(底部Tab)
|
||||
│ ├── 体重管理详情 → 每日打卡
|
||||
│ │ ├── 饮食记录详情 → 员工饮食详情
|
||||
│ │ ├── 运动记录详情 → 员工运动详情
|
||||
│ │ └── 体重记录 → 员工体重详情
|
||||
│ ├── 体重管理详情 → 减重排名
|
||||
│ ├── 体重管理详情 → 目标审核
|
||||
│ │ ├── 目标审核(待审核)
|
||||
│ │ ├── 目标审核(已通过)
|
||||
│ │ └── 目标审核(已驳回)
|
||||
│ ├── 体检进度追踪
|
||||
│ ├── 应急求助工单 → 应急工单详情
|
||||
│ └── 穿戴设备预警记录
|
||||
├── 知识库(底部Tab)
|
||||
│ ├── 知识文章(分类Tab)→ 知识详情
|
||||
│ └── 规章制度(PDF列表)
|
||||
├── AI助手(底部Tab)
|
||||
├── 消息(底部Tab)
|
||||
│ ├── 消息列表
|
||||
│ ├── 通讯录
|
||||
│ └── 即时通讯
|
||||
└── 我的(底部Tab)
|
||||
├── 修改个人信息
|
||||
├── 修改密码
|
||||
└── 退出登录(确认弹窗)
|
||||
```
|
||||
|
||||
|
||||
## 页面清单
|
||||
|
||||
| 序号 | 文件名 | 页面名称 | 所属模块 |
|
||||
|------|--------|----------|----------|
|
||||
| 1 | workbench/index.html | 工作台首页 | 工作台 |
|
||||
| 2 | workbench/weight-detail.html | 体重管理详情 | 工作台·体重管理 |
|
||||
| 3 | workbench/eat-detail.html | 饮食记录详情 | 工作台·体重管理 |
|
||||
| 4 | workbench/diet-employee-detail.html | 员工饮食详情 | 工作台·体重管理 |
|
||||
| 5 | workbench/exercise-detail.html | 运动记录详情 | 工作台·体重管理 |
|
||||
| 6 | workbench/exercise-employee-detail.html | 员工运动详情 | 工作台·体重管理 |
|
||||
| 7 | workbench/weight-record.html | 体重记录 | 工作台·体重管理 |
|
||||
| 8 | workbench/weight-employee-detail.html | 员工体重详情 | 工作台·体重管理 |
|
||||
| 9 | workbench/goal-review.html?status=pending | 目标审核(待审核) | 工作台·体重管理 |
|
||||
| 10 | workbench/goal-review.html?status=approved | 目标审核(已通过) | 工作台·体重管理 |
|
||||
| 11 | workbench/goal-review.html?status=rejected | 目标审核(已驳回) | 工作台·体重管理 |
|
||||
| 12 | workbench/checkup-progress.html | 体检进度追踪 | 工作台·体检管理 |
|
||||
| 13 | workbench/emergency-orders.html | 应急求助工单 | 工作台·应急管理 |
|
||||
| 14 | workbench/emergency-order-detail.html | 应急工单详情 | 工作台·应急管理 |
|
||||
| 15 | workbench/wearable-alerts.html | 穿戴设备预警记录 | 工作台·预警管理 |
|
||||
| 16 | knowledge/index.html | 知识库首页 | 知识库 |
|
||||
| 17 | knowledge/detail.html | 知识详情 | 知识库 |
|
||||
| 18 | ../ai-assistant/index.html | AI 助手 | AI 助手 |
|
||||
| 19 | ../message/index.html | 消息列表 | 消息 |
|
||||
| 20 | ../message/contacts.html | 通讯录 | 消息 |
|
||||
| 21 | ../message/im-chat.html | 即时通讯 | 消息 |
|
||||
| 22 | profile/index.html | 个人中心主页 | 我的 |
|
||||
| 23 | profile/edit-profile.html | 修改个人信息 | 我的 |
|
||||
| 24 | profile/change-pwd.html | 修改密码 | 我的 |
|
||||
|
||||
## 逐页说明
|
||||
|
||||
---
|
||||
|
||||
## 一、工作台模块
|
||||
|
||||
### 1. 工作台首页
|
||||
|
||||

|
||||
|
||||
**功能说明:** 管理员工作台入口,顶部展示用户信息(头像、姓名、角色、单位部门),下方以功能卡片形式呈现四大管理入口:体重管理详情、体检进度追踪、应急求助工单、穿戴设备预警记录。
|
||||
|
||||
**业务规则:**
|
||||
- 用户信息区:头像、姓名、角色、所属单位部门
|
||||
- 功能卡片共 4 个:
|
||||
1. 体重管理详情
|
||||
2. 体检进度追踪
|
||||
3. 应急求助工单
|
||||
4. 穿戴设备预警记录
|
||||
- 底部 5 个 Tab:工作台、知识库、AI 助手、消息、我的
|
||||
|
||||
**交互说明:**
|
||||
- 点击"体重管理详情"卡片:跳转到体重管理详情页
|
||||
- 点击"体检进度追踪"卡片:跳转到体检进度追踪页
|
||||
- 点击"应急求助工单"卡片:跳转到应急求助工单页
|
||||
- 点击"穿戴设备预警记录"卡片:跳转到穿戴设备预警记录页
|
||||
- 底部导航栏切换:跳转到对应 Tab 页面
|
||||
|
||||
---
|
||||
|
||||
## 二、体重管理业务线
|
||||
|
||||
### 2. 体重管理详情
|
||||
|
||||

|
||||
|
||||
**功能说明:** 体重管理活动总览页面,支持切换不同活动,展示活动整体统计数据,并通过三个主 Tab(每日打卡、减重排名、目标审核)分类查看管理数据。
|
||||
|
||||
**业务规则:**
|
||||
- 顶部活动选择器:下拉切换不同活动期次(如"2026年Q2体重管理活动"),切换后刷新活动统计数据
|
||||
- 活动统计区:已进行天数 / 总天数、预期参与人数、实际参与人数
|
||||
- 三个主 Tab:
|
||||
- 每日打卡(默认):按日查看饮食、运动、体重的打卡统计数据
|
||||
- 减重排名:4 个子 Tab(减重总量排名 / 减重率排名 / 综合评分排名 / 行动评分排名)
|
||||
- 目标审核:3 个子 Tab(待审核 / 已通过 / 已驳回),显示员工减重目标申请
|
||||
- 每日打卡面板:日期导航(左右箭头 + 日历选择器)、三张统计卡片(饮食已打卡XX人/未打卡XX人、运动已打卡XX人/未打卡XX人、体重已打卡XX人/未打卡XX人)
|
||||
- 减重排名:前三名展示金/银/铜牌勋章 + 员工脱敏姓名 + 部门 + 数值
|
||||
- 目标审核卡片:展示员工姓名、初始/目标数据对比、操作按钮
|
||||
|
||||
**交互说明:**
|
||||
- 点击活动下拉框:展开活动列表,选择后切换数据
|
||||
- 点击每日打卡 Tab:切换到打卡统计面板
|
||||
- 点击减重排名 Tab:切换到排名面板
|
||||
- 点击目标审核 Tab:切换到审核面板
|
||||
- 点击饮食"查看详情":跳转到饮食记录详情页
|
||||
- 点击运动"查看详情":跳转到运动记录详情页
|
||||
- 点击体重"查看详情":跳转到体重记录页
|
||||
- 日期导航左右箭头:切换前一天/后一天
|
||||
- 点击日历图标:弹出日历选择面板
|
||||
- 点击排名子 Tab:切换不同排名维度
|
||||
- 点击审核子 Tab(待审核/已通过/待签收/已驳回):切换审核状态面板
|
||||
- 点击目标审核卡片的"审核"按钮:跳转到目标审核页
|
||||
- 点击返回:返回工作台首页
|
||||
|
||||
---
|
||||
|
||||
### 3. 饮食记录详情
|
||||
|
||||

|
||||
|
||||
**功能说明:** 按日查看员工的饮食打卡情况,分"已记录"和"未记录"两个 Tab 展示员工列表。
|
||||
|
||||
**业务规则:**
|
||||
- 日期导航:支持左右箭头切换日期、日历面板选择日期
|
||||
- 两个 Tab:已记录(角标数字显示已打卡人数)、未记录(角标数字显示未打卡人数)
|
||||
- 员工卡片信息:姓名、性别、年龄、部门、身高
|
||||
- 已记录员工卡片右侧有"记录详情 >"链接
|
||||
|
||||
**交互说明:**
|
||||
- 点击已记录员工的"记录详情":跳转到员工饮食详情页
|
||||
- 点击日历图标:弹出日历选择面板
|
||||
- 日期左右箭头:切换日期
|
||||
- 点击 Tab 切换:已记录 / 未记录列表
|
||||
- 点击返回:返回体重管理详情页
|
||||
|
||||
---
|
||||
|
||||
### 4. 员工饮食详情
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看单个员工的当日饮食详细记录,包括各餐次的摄入热量、食物明细及记录来源。同时展示该员工的初始/目标/当前身体数据对比。
|
||||
|
||||
**业务规则:**
|
||||
- 员工信息:姓名(张晓红)、性别、年龄、部门、身高
|
||||
- 数据对比表(三列):初始数据 / 目标数据 / 当前数据,包含体重、BMI、体脂率、腰围
|
||||
- 变化指标:体重变化、BMI 变化、体脂率变化、腰围变化(绿色数值表示下降)
|
||||
- 当日饮食记录:按餐次展示(早餐、午餐、加餐、晚餐),每条记录显示热量(kcal)、时间、记录来源标签
|
||||
|
||||
**交互说明:**
|
||||
- 点击返回:返回饮食记录详情页
|
||||
|
||||
---
|
||||
|
||||
### 5. 运动记录详情
|
||||
|
||||

|
||||
|
||||
**功能说明:** 按日查看员工的运动打卡情况,分"已记录"和"未记录"两个 Tab 展示员工列表。
|
||||
|
||||
**业务规则:**
|
||||
- 日期导航:支持左右箭头切换日期、日历面板选择日期
|
||||
- 两个 Tab:已记录(角标数字)、未记录(角标数字)
|
||||
- 员工卡片信息:姓名、性别、年龄、单位部门、身高
|
||||
- 已记录员工卡片右侧有"记录详情 >"链接
|
||||
|
||||
**交互说明:**
|
||||
- 点击已记录员工的"记录详情":跳转到员工运动详情页
|
||||
- 点击日历图标:弹出日历选择面板
|
||||
- 日期左右箭头:切换日期
|
||||
- 点击 Tab 切换:已记录 / 未记录列表
|
||||
- 点击返回:返回体重管理详情页
|
||||
|
||||
---
|
||||
|
||||
### 6. 员工运动详情
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看单个员工的当日运动详细记录,包括运动类型、时长、消耗热量及记录来源。同时展示员工的初始/目标/当前身体数据对比。
|
||||
|
||||
**业务规则:**
|
||||
- 员工信息:姓名(陈伟)、性别、年龄
|
||||
- 数据对比表:初始数据 / 目标数据 / 当前数据(体重、BMI、体脂率、腰围)
|
||||
- 变化指标:绿色显示下降数值
|
||||
- 当日运动记录:运动名称、运动时长/步数、消耗千卡、时间、来源(穿戴、器械、微信、自记)
|
||||
|
||||
**交互说明:**
|
||||
- 点击返回:返回运动记录详情页
|
||||
|
||||
---
|
||||
|
||||
### 7. 体重记录
|
||||
|
||||

|
||||
|
||||
**功能说明:** 按日查看员工的体重打卡情况,分"已记录"和"未记录"两个 Tab 展示员工列表。
|
||||
|
||||
**业务规则:**
|
||||
- 日期导航:支持左右箭头切换日期、日历面板选择日期
|
||||
- 两个 Tab:已记录(角标数字)、未记录(角标数字)
|
||||
- 员工卡片信息:姓名、性别、年龄、单位部门、身高
|
||||
- 已记录员工卡片右侧有"记录详情 >"链接
|
||||
|
||||
**交互说明:**
|
||||
- 点击已记录员工的"记录详情":跳转到员工体重详情页
|
||||
- 点击日历图标:弹出日历选择面板
|
||||
- 日期左右箭头:切换日期
|
||||
- 点击 Tab 切换:已记录 / 未记录列表
|
||||
- 点击返回:返回体重管理详情页
|
||||
|
||||
---
|
||||
|
||||
### 8. 员工体重详情
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看单个员工的体重历史记录,每条记录包含体重、体脂率、腰围及记录来源,同时展示该员工的初始/目标/当前身体数据对比。
|
||||
|
||||
**业务规则:**
|
||||
- 员工信息:姓名(张晓红)、性别、年龄、部门
|
||||
- 数据对比表:初始数据 / 目标数据 / 当前数据(体重、BMI、体脂率、腰围)
|
||||
- 变化指标:绿色显示下降数值
|
||||
- 体重记录列表:按时间倒序,每条显示时间、体重、体脂率、腰围、来源标签(体重秤、蓝牙称、体脂仪、自记)
|
||||
|
||||
**交互说明:**
|
||||
- 点击返回:返回体重记录页
|
||||
|
||||
---
|
||||
|
||||
### 9. 目标审核(待审核)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 管理员查看待审核的减重目标申请,可选择通过或驳回。驳回时需填写驳回原因。
|
||||
|
||||
**业务规则:**
|
||||
- 员工信息:姓名、性别、年龄、身高
|
||||
- 申请时间
|
||||
- 数据对比卡片:
|
||||
- 初始数据:体重/ BMI/ 体脂/ 腰围
|
||||
- 目标数据:体重/ BMI/ 体脂/ 腰围
|
||||
- 推荐范围:体重/ BMI/ 体脂/ 腰围
|
||||
- 目标起止日期
|
||||
- 底部操作区:单选按钮(审核通过 / 审核驳回)、驳回原因文本框(默认隐藏)、提交审核按钮
|
||||
|
||||
**交互说明:**
|
||||
- 点击"审核通过"单选按钮:选中通过,隐藏驳回原因文本框
|
||||
- 点击"审核驳回"单选按钮:选中驳回,展开驳回原因文本框
|
||||
- 点击"提交审核":弹出确认弹窗
|
||||
- 通过时:"确认通过该目标申请?",确认按钮为蓝色"确认通过"
|
||||
- 驳回时:"确认驳回该目标申请?驳回原因:…",确认按钮为红色"确认驳回"
|
||||
- 确认弹窗点击取消:关闭弹窗
|
||||
- 确认弹窗点击确认:关闭确认弹窗,弹出成功弹窗("审核完成"),点击"返回列表"跳转回体重管理详情页
|
||||
- 点击返回:返回体重管理详情页
|
||||
|
||||
**审核操作表单:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 校验规则 |
|
||||
|--------|------|------|----------|
|
||||
| 审核结果 | 单选按钮 | 是 | 必选"通过"或"驳回" |
|
||||
| 驳回原因 | 多行文本 | 驳回时必填 | 驳回时不可为空 |
|
||||
|
||||
---
|
||||
|
||||
### 10. 目标审核(已通过)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看已通过审核的减重目标申请详情,纯只读展示,无操作按钮。
|
||||
|
||||
**业务规则:**
|
||||
- 员工信息:姓名、性别、年龄、身高
|
||||
- 申请时间
|
||||
- 数据对比卡片:
|
||||
- 初始数据:体重/ BMI/ 体脂/ 腰围
|
||||
- 目标数据:体重/ BMI/ 体脂/ 腰围
|
||||
- 推荐范围:体重/ BMI/ 体脂/ 腰围
|
||||
- 目标起止日期
|
||||
- 审核结果卡片:审核结果标签、审核人、审核时间
|
||||
|
||||
**交互说明:**
|
||||
- 点击返回:返回体重管理详情页
|
||||
|
||||
---
|
||||
|
||||
### 11. 目标审核(已驳回)
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看已被驳回的减重目标申请详情,包含驳回原因,纯只读展示,无操作按钮。
|
||||
|
||||
**业务规则:**
|
||||
- 员工信息:姓名、性别、年龄、身高
|
||||
- 申请时间
|
||||
- 数据对比卡片:
|
||||
- 初始数据:体重/ BMI/ 体脂/ 腰围
|
||||
- 目标数据:体重/ BMI/ 体脂/ 腰围
|
||||
- 推荐范围:体重/ BMI/ 体脂/ 腰围
|
||||
- 目标起止日期
|
||||
- 审核结果卡片:审核结果标签、审核人、审核时间、驳回原因
|
||||
|
||||
**交互说明:**
|
||||
- 点击返回:返回体重管理详情页
|
||||
|
||||
---
|
||||
|
||||
## 三、体检管理业务线
|
||||
|
||||
### 12. 体检进度追踪
|
||||
|
||||

|
||||
|
||||
**功能说明:** 按体检计划期次查看员工的体检预约和完成进度,通过四个分段 Tab(已预约、已检查、未预约、未检查)统计并展示各状态的员工列表。
|
||||
|
||||
**业务规则:**
|
||||
- 顶部体检计划选择器:下拉切换不同体检计划,切换后刷新各状态统计数据
|
||||
- 统计区:预期人数、已预约人数、已检查人数、未预约人数、未检查人数
|
||||
- 四个分段 Tab:已预约(角标数字)、已检查(角标数字)、未预约(角标数字)、未检查(角标数字)
|
||||
- 员工卡片信息:头像、姓名 + 工号、所属单位 + 部门、联系电话
|
||||
|
||||
**交互说明:**
|
||||
- 点击体检计划下拉框:展开计划列表,选择后切换数据
|
||||
- 点击分段 Tab:切换不同状态的员工列表
|
||||
- 点击返回:返回工作台首页
|
||||
|
||||
---
|
||||
|
||||
## 四、应急管理业务线
|
||||
|
||||
### 13. 应急求助工单
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看所有应急求助工单的处理情况,分"进行中"和"已完成"两个 Tab 展示,支持按月度和求助方式筛选。管理员以只读监控视角查看工单流转。
|
||||
|
||||
**业务规则:**
|
||||
- 两个主 Tab:进行中(角标数字)、已完成(角标数字)
|
||||
- 筛选栏:月份下拉、求助方式下拉(全部 / 应急视频 / 应急电话 / 拨120)
|
||||
- 工单卡片信息:求助方式标签、时间、求助员工信息(头像、姓名、工号、性别、年龄、单位、部门、电话)、工单信息(应急中心、操作人员、专业人员)
|
||||
|
||||
**交互说明:**
|
||||
- 点击进行中/已完成 Tab:切换工单列表
|
||||
- 点击月份下拉:按月份筛选工单
|
||||
- 点击求助方式下拉:按求助方式筛选工单
|
||||
- 点击工单卡片:跳转到应急工单详情页
|
||||
- 点击返回:返回工作台首页
|
||||
|
||||
---
|
||||
|
||||
### 14. 应急工单详情
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看单条应急工单的完整信息,以可折叠卡片形式展示基本信息、救助信息、派单信息、救护车支持信息四个维度的详情。管理员视角为纯只读页面。
|
||||
|
||||
**业务规则:**
|
||||
- 四个可折叠卡片(默认展开):
|
||||
1. 基本信息:工单编号、应急中心、求助员工姓名/工号、单位、部门、求助方式、求助时间、位置信息、操作人员、专业人员、工单状态(已完成绿色标签)、完成时间
|
||||
2. 救助信息:救助意见(文本)、救助结果(文本)、相关照片、补充小结(多条带圆点标记的列表)
|
||||
3. 派单信息:派单时间、驻场人员、派单状态(已完成)、结束时间、派单说明、驻场说明
|
||||
4. 救护车支持:车牌号、机构、驾驶员、驾驶员电话、护送人员、护送人员电话、目的地、备注
|
||||
- 纯只读展示,无编辑或操作按钮
|
||||
|
||||
**交互说明:**
|
||||
- 点击卡片标题行:折叠/展开卡片内容
|
||||
- 点击返回:返回应急求助工单列表页
|
||||
|
||||
---
|
||||
|
||||
## 五、预警管理业务线
|
||||
|
||||
### 15. 穿戴设备预警记录
|
||||
|
||||

|
||||
|
||||
**功能说明:** 查看所有员工穿戴设备的异常告警记录,支持按月份和告警类型筛选。
|
||||
|
||||
**业务规则:**
|
||||
- 筛选栏:月份下拉、告警类型下拉(全部 / 一键告警 / 体温异常 / 心率异常 / 血氧异常 / 压力异常 / 跌倒)
|
||||
- 告警卡片信息:告警类型标签、时间戳、异常数值(含单位)、员工信息(头像、姓名、工号、性别、年龄、单位、部门)、位置信息
|
||||
|
||||
**交互说明:**
|
||||
- 点击月份下拉:按月份筛选告警记录
|
||||
- 点击告警类型下拉:按类型筛选告警记录
|
||||
- 点击返回:返回工作台首页
|
||||
|
||||
---
|
||||
|
||||
## 六、知识库模块
|
||||
|
||||
### 16. 知识库首页
|
||||
|
||||
|
||||
**功能说明:** 等待后续规划,提示“功能开发中,敬请期待~”
|
||||
|
||||
---
|
||||
|
||||
### 17. 知识详情
|
||||
|
||||
|
||||
**功能说明:** 等待后续规划,提示“功能开发中,敬请期待~”
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 七、AI 助手
|
||||
|
||||
### 18. AI 助手
|
||||
|
||||
**功能说明:** 后续规划,暂时显示”开发中,敬请期待~”。
|
||||
|
||||
---
|
||||
|
||||
## 八、消息模块
|
||||
|
||||
### 19. 消息列表
|
||||
|
||||

|
||||
|
||||
**功能说明:** 内部消息模块,与当前系统中”咨询专家”、”咨询小助手”、”操作人员”、”专业人员”、”驻场人员”、”健康管理员”进行对话聊天的消息列表。
|
||||
|
||||
**业务规则:**
|
||||
- 消息列表按最新消息时间倒序排列
|
||||
- 每条消息显示联系人头像、姓名、角色标签、最后一条消息预览、时间
|
||||
|
||||
**交互说明:**
|
||||
- 点击消息项:跳转到 IM 聊天页
|
||||
- 点击右上角通讯录图标:跳转到通讯录页
|
||||
- 底部 Tab 导航切换到其他模块
|
||||
|
||||
---
|
||||
|
||||
### 20. 通讯录
|
||||
|
||||

|
||||
|
||||
**功能说明:** 展示系统内所有可联系的内部人员,支持按角色筛选和姓名搜索。
|
||||
|
||||
**交互说明:**
|
||||
- 点击联系人:跳转到 IM 聊天页
|
||||
- 输入名称进行模糊搜索
|
||||
- 角色分类可收起展开
|
||||
- 返回按钮:跳转到消息列表
|
||||
|
||||
---
|
||||
|
||||
### 21. 即时通讯
|
||||
|
||||

|
||||
|
||||
**功能说明:** 与内部人员的即时通讯聊天界面。
|
||||
|
||||
**交互说明:**
|
||||
- 底部输入框输入文字并发送
|
||||
- 支持语音和输入切换
|
||||
- 支持拍照和发送图库照片
|
||||
- 返回按钮:跳转到消息列表
|
||||
|
||||
---
|
||||
|
||||
## 九、个人中心模块
|
||||
|
||||
### 22. 个人中心主页
|
||||
|
||||

|
||||
|
||||
**功能说明:** 管理员个人信息展示和功能菜单导航。
|
||||
|
||||
**业务规则:**
|
||||
- 用户信息区:头像、姓名、角色、所属单位部门
|
||||
- 菜单项:修改个人信息、修改密码、关于我们
|
||||
- 退出登录按钮
|
||||
|
||||
**交互说明:**
|
||||
- 点击"修改个人信息":跳转到修改个人信息页
|
||||
- 点击"修改密码":跳转到修改密码页
|
||||
- 点击"退出登录":弹出二次确认弹窗("确认退出当前账号?"),取消关闭弹窗,确认退出登录
|
||||
- 底部 Tab 切换:跳转到对应页面
|
||||
|
||||
---
|
||||
|
||||
### 23. 修改个人信息
|
||||
|
||||

|
||||
|
||||
**功能说明:** 编辑管理员头像和姓名。
|
||||
|
||||
**交互说明:**
|
||||
- 点击头像:弹出底部操作面板(拍照 / 从相册选择 / 取消)
|
||||
- 点击"保存":验证姓名非空,显示"保存成功"提示(1.5 秒自动消失)
|
||||
- 点击返回:返回个人中心主页
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 24. 修改密码
|
||||
|
||||

|
||||
|
||||
**功能说明:** 修改管理员登录密码。
|
||||
|
||||
**交互说明:**
|
||||
- 点击"保存修改":弹出修改成功确认弹窗("密码修改成功"),退回到登录页面进行重新登录
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
<!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 { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; }
|
||||
|
||||
/* 顶部导航 */
|
||||
.nav { position: sticky; top: 0; z-index: 100; background: #16213e; border-bottom: 1px solid #0f3460; padding: 14px 24px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
.nav__title { font-size: 16px; font-weight: 600; color: #4fc3f7; margin-right: 20px; white-space: nowrap; }
|
||||
.nav__link { font-size: 13px; color: #aaa; text-decoration: none; padding: 5px 12px; border-radius: 20px; transition: all .2s; white-space: nowrap; border: 1px solid transparent; }
|
||||
.nav__link:hover { background: #0f3460; color: #4fc3f7; border-color: #4fc3f7; }
|
||||
|
||||
/* 业务主线区块 */
|
||||
.section { padding: 24px 24px 8px; }
|
||||
.section__header { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
|
||||
.section__title { font-size: 15px; font-weight: 600; color: #fff; }
|
||||
.section__count { font-size: 12px; color: #666; background: #2a2a4a; padding: 2px 8px; border-radius: 10px; }
|
||||
|
||||
/* 横向滚动容器 */
|
||||
.scroll-row { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 16px; scrollbar-width: thin; scrollbar-color: #333 transparent; }
|
||||
.scroll-row::-webkit-scrollbar { height: 6px; }
|
||||
.scroll-row::-webkit-scrollbar-track { background: transparent; }
|
||||
.scroll-row::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
|
||||
|
||||
/* 卡片 */
|
||||
.card { flex-shrink: 0; width: 260px; cursor: pointer; transition: transform .2s; }
|
||||
.card:hover { transform: scale(1.02); }
|
||||
.card__img { width: 260px; height: 564px; border-radius: 12px; overflow: hidden; border: 2px solid #2a2a4a; background: #111; transition: border-color .2s; }
|
||||
.card:hover .card__img { border-color: #4fc3f7; }
|
||||
.card__img img { width: 100%; height: 100%; object-fit: cover; object-position: top; }
|
||||
.card__label { margin-top: 8px; font-size: 12px; color: #999; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.card__tags { display: flex; justify-content: center; gap: 4px; margin-top: 4px; flex-wrap: wrap; }
|
||||
.tag { font-size: 10px; padding: 1px 5px; border-radius: 3px; }
|
||||
.tag--modal { background: #4a2800; color: #ffa940; }
|
||||
.tag--state { background: #002a4a; color: #4fc3f7; }
|
||||
|
||||
/* 分隔线 */
|
||||
.divider { height: 1px; background: #2a2a4a; margin: 8px 24px; }
|
||||
|
||||
/* 灯箱 */
|
||||
.lightbox { display: none; position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.92); align-items: center; justify-content: center; cursor: zoom-out; }
|
||||
.lightbox.show { display: flex; }
|
||||
.lightbox img { max-width: 90vw; max-height: 92vh; border-radius: 8px; }
|
||||
.lightbox__close { position: absolute; top: 16px; right: 20px; color: #fff; font-size: 24px; cursor: pointer; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: rgba(255,255,255,0.1); }
|
||||
.lightbox__name { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #ccc; font-size: 13px; background: rgba(0,0,0,0.6); padding: 6px 16px; border-radius: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<nav class="nav">
|
||||
<span class="nav__title">管理员端 · 设计总览</span>
|
||||
<a class="nav__link" href="#sec-workbench">工作台首页</a>
|
||||
<a class="nav__link" href="#sec-emergency">应急工单</a>
|
||||
<a class="nav__link" href="#sec-weight">体重管理</a>
|
||||
<a class="nav__link" href="#sec-goal">目标审核</a>
|
||||
<a class="nav__link" href="#sec-checkup">体检与预警</a>
|
||||
<a class="nav__link" href="#sec-knowledge">知识库</a>
|
||||
<a class="nav__link" href="#sec-profile">个人中心</a>
|
||||
</nav>
|
||||
|
||||
<!-- 工作台首页 -->
|
||||
<section class="section" id="sec-workbench">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">工作台首页</h2>
|
||||
<span class="section__count">1 页</span>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="card" onclick="openLightbox('screenshots/index.jpg','工作台首页')">
|
||||
<div class="card__img"><img src="screenshots/index.jpg" alt="工作台首页"></div>
|
||||
<div class="card__label">工作台首页</div>
|
||||
<div class="card__tags"><span class="tag tag--state">功能入口</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 应急工单管理 -->
|
||||
<section class="section" id="sec-emergency">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">应急工单管理</h2>
|
||||
<span class="section__count">2 页</span>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="card" onclick="openLightbox('screenshots/emergency-orders.jpg','应急求助工单')">
|
||||
<div class="card__img"><img src="screenshots/emergency-orders.jpg" alt="应急求助工单"></div>
|
||||
<div class="card__label">应急求助工单</div>
|
||||
<div class="card__tags"><span class="tag tag--state">Tab切换</span></div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/emergency-order-detail.jpg','应急工单详情')">
|
||||
<div class="card__img"><img src="screenshots/emergency-order-detail.jpg" alt="应急工单详情"></div>
|
||||
<div class="card__label">应急工单详情</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 体重管理 -->
|
||||
<section class="section" id="sec-weight">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">体重管理</h2>
|
||||
<span class="section__count">7 页</span>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="card" onclick="openLightbox('screenshots/weight-detail.jpg','体重管理详情')">
|
||||
<div class="card__img"><img src="screenshots/weight-detail.jpg" alt="体重管理详情"></div>
|
||||
<div class="card__label">体重管理详情</div>
|
||||
<div class="card__tags"><span class="tag tag--state">多Tab</span></div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/weight-record.jpg','体重记录')">
|
||||
<div class="card__img"><img src="screenshots/weight-record.jpg" alt="体重记录"></div>
|
||||
<div class="card__label">体重记录</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/weight-employee-detail.jpg','员工体重详情')">
|
||||
<div class="card__img"><img src="screenshots/weight-employee-detail.jpg" alt="员工体重详情"></div>
|
||||
<div class="card__label">员工体重详情</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/eat-detail.jpg','饮食记录详情')">
|
||||
<div class="card__img"><img src="screenshots/eat-detail.jpg" alt="饮食记录详情"></div>
|
||||
<div class="card__label">饮食记录详情</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/diet-employee-detail.jpg','员工饮食详情')">
|
||||
<div class="card__img"><img src="screenshots/diet-employee-detail.jpg" alt="员工饮食详情"></div>
|
||||
<div class="card__label">员工饮食详情</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/exercise-detail.jpg','运动记录详情')">
|
||||
<div class="card__img"><img src="screenshots/exercise-detail.jpg" alt="运动记录详情"></div>
|
||||
<div class="card__label">运动记录详情</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/exercise-employee-detail.jpg','员工运动详情')">
|
||||
<div class="card__img"><img src="screenshots/exercise-employee-detail.jpg" alt="员工运动详情"></div>
|
||||
<div class="card__label">员工运动详情</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 目标审核 -->
|
||||
<section class="section" id="sec-goal">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">目标审核</h2>
|
||||
<span class="section__count">3 页</span>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="card" onclick="openLightbox('screenshots/goal-review-pending.jpg','目标审核(待审核)')">
|
||||
<div class="card__img"><img src="screenshots/goal-review-pending.jpg" alt="目标审核(待审核)"></div>
|
||||
<div class="card__label">目标审核(待审核)</div>
|
||||
<div class="card__tags"><span class="tag tag--state">Tab切换</span></div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/goal-review-approved.jpg','目标审核(已通过)')">
|
||||
<div class="card__img"><img src="screenshots/goal-review-approved.jpg" alt="目标审核(已通过)"></div>
|
||||
<div class="card__label">目标审核(已通过)</div>
|
||||
<div class="card__tags"><span class="tag tag--state">Tab切换</span></div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/goal-review-rejected.jpg','目标审核(已驳回)')">
|
||||
<div class="card__img"><img src="screenshots/goal-review-rejected.jpg" alt="目标审核(已驳回)"></div>
|
||||
<div class="card__label">目标审核(已驳回)</div>
|
||||
<div class="card__tags"><span class="tag tag--state">Tab切换</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 体检与预警 -->
|
||||
<section class="section" id="sec-checkup">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">体检与预警</h2>
|
||||
<span class="section__count">2 页</span>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="card" onclick="openLightbox('screenshots/checkup-progress.jpg','体检进度追踪')">
|
||||
<div class="card__img"><img src="screenshots/checkup-progress.jpg" alt="体检进度追踪"></div>
|
||||
<div class="card__label">体检进度追踪</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/wearable-alerts.jpg','穿戴设备预警记录')">
|
||||
<div class="card__img"><img src="screenshots/wearable-alerts.jpg" alt="穿戴设备预警记录"></div>
|
||||
<div class="card__label">穿戴设备预警记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 知识库 -->
|
||||
<section class="section" id="sec-knowledge">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">知识库</h2>
|
||||
<span class="section__count">2 页</span>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="card" onclick="openLightbox('screenshots/knowledge-index.jpg','知识库首页')">
|
||||
<div class="card__img"><img src="screenshots/knowledge-index.jpg" alt="知识库首页"></div>
|
||||
<div class="card__label">知识库首页</div>
|
||||
<div class="card__tags"><span class="tag tag--state">Tab切换</span></div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/knowledge-detail.jpg','知识详情')">
|
||||
<div class="card__img"><img src="screenshots/knowledge-detail.jpg" alt="知识详情"></div>
|
||||
<div class="card__label">知识详情</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 个人中心 -->
|
||||
<section class="section" id="sec-profile">
|
||||
<div class="section__header">
|
||||
<h2 class="section__title">个人中心</h2>
|
||||
<span class="section__count">3 页</span>
|
||||
</div>
|
||||
<div class="scroll-row">
|
||||
<div class="card" onclick="openLightbox('screenshots/profile-index.jpg','个人中心主页')">
|
||||
<div class="card__img"><img src="screenshots/profile-index.jpg" alt="个人中心主页"></div>
|
||||
<div class="card__label">个人中心主页</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/profile-edit-profile.jpg','修改个人信息')">
|
||||
<div class="card__img"><img src="screenshots/profile-edit-profile.jpg" alt="修改个人信息"></div>
|
||||
<div class="card__label">修改个人信息</div>
|
||||
</div>
|
||||
<div class="card" onclick="openLightbox('screenshots/profile-change-pwd.jpg','修改密码')">
|
||||
<div class="card__img"><img src="screenshots/profile-change-pwd.jpg" alt="修改密码"></div>
|
||||
<div class="card__label">修改密码</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="height:40px;"></div>
|
||||
|
||||
<!-- 灯箱 -->
|
||||
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
|
||||
<span class="lightbox__close">×</span>
|
||||
<img id="lightbox-img" src="" alt="">
|
||||
<div class="lightbox__name" id="lightbox-name"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openLightbox(src, name) {
|
||||
document.getElementById('lightbox-img').src = src;
|
||||
document.getElementById('lightbox-name').textContent = name;
|
||||
document.getElementById('lightbox').classList.add('show');
|
||||
}
|
||||
function closeLightbox() {
|
||||
document.getElementById('lightbox').classList.remove('show');
|
||||
}
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeLightbox();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 管理员端(健康应急APP)页面截图脚本
|
||||
* 使用 Playwright 批量截取 HTML 原型页面,输出 JPEG 到 doc/screenshots/
|
||||
*
|
||||
* 截图约束(参考项目背景.md §7.4):
|
||||
* - 视口宽度 390px,1x 缩放,全页长图
|
||||
* - JPEG 质量:普通页 80%,长图(>1800px)65%
|
||||
* - 目标体积 ≤ 80KB
|
||||
*
|
||||
* 用法: node doc/screenshot.js
|
||||
*/
|
||||
const { chromium } = require('playwright');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const MODULE_DIR = __dirname.replace(/\\doc$/, ''); // admin/
|
||||
const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots');
|
||||
|
||||
// 所有需要截图的页面(相对于模块目录)
|
||||
// 格式:{ file: 'path.html', query: '?key=val', out: 'output-name.jpg' }
|
||||
const PAGES = [
|
||||
// 工作台
|
||||
{ file: 'workbench/index.html' },
|
||||
{ file: 'workbench/weight-detail.html' },
|
||||
{ file: 'workbench/eat-detail.html' },
|
||||
{ file: 'workbench/diet-employee-detail.html' },
|
||||
{ file: 'workbench/exercise-detail.html' },
|
||||
{ file: 'workbench/exercise-employee-detail.html' },
|
||||
{ file: 'workbench/weight-record.html' },
|
||||
{ file: 'workbench/weight-employee-detail.html' },
|
||||
// 目标审核 - 3 种状态
|
||||
{ file: 'workbench/goal-review.html', query: '?status=pending', out: 'goal-review-pending.jpg' },
|
||||
{ file: 'workbench/goal-review.html', query: '?status=approved', out: 'goal-review-approved.jpg' },
|
||||
{ file: 'workbench/goal-review.html', query: '?status=rejected', out: 'goal-review-rejected.jpg' },
|
||||
// 体检管理
|
||||
{ file: 'workbench/checkup-progress.html' },
|
||||
// 应急管理
|
||||
{ file: 'workbench/emergency-orders.html' },
|
||||
{ file: 'workbench/emergency-order-detail.html' },
|
||||
// 预警管理
|
||||
{ file: 'workbench/wearable-alerts.html' },
|
||||
// 知识库
|
||||
{ file: 'knowledge/index.html' },
|
||||
{ file: 'knowledge/detail.html' },
|
||||
// 个人中心
|
||||
{ file: 'profile/index.html' },
|
||||
{ file: 'profile/edit-profile.html' },
|
||||
{ file: 'profile/change-pwd.html' },
|
||||
];
|
||||
|
||||
// 共享页面(ai-assistant、message),截图输出到各自 screenshots 目录
|
||||
const EMERGENCY_DIR = path.resolve(MODULE_DIR, '..'); // emergency-app/
|
||||
const SHARED_PAGES = [
|
||||
{ file: path.join(EMERGENCY_DIR, 'ai-assistant/index.html'), outDir: path.join(EMERGENCY_DIR, 'ai-assistant/screenshots'), out: 'index.jpg' },
|
||||
{ file: path.join(EMERGENCY_DIR, 'message/index.html'), outDir: path.join(EMERGENCY_DIR, 'message/screenshots'), out: 'index.jpg' },
|
||||
{ file: path.join(EMERGENCY_DIR, 'message/contacts.html'), outDir: path.join(EMERGENCY_DIR, 'message/screenshots'), out: 'contacts.jpg' },
|
||||
{ file: path.join(EMERGENCY_DIR, 'message/im-chat.html'), outDir: path.join(EMERGENCY_DIR, 'message/screenshots'), out: 'im-chat.jpg' },
|
||||
];
|
||||
|
||||
function getOutputName(entry) {
|
||||
if (entry.out) return entry.out;
|
||||
let name = entry.file.replace('.html', '.jpg');
|
||||
if (name.startsWith('workbench/')) name = name.replace('workbench/', '');
|
||||
else if (name.startsWith('knowledge/')) name = name.replace('knowledge/', 'knowledge-');
|
||||
else if (name.startsWith('profile/')) name = name.replace('profile/', 'profile-');
|
||||
return name;
|
||||
}
|
||||
|
||||
async function screenshotPage(browser, entry, outDir) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 390, height: 844 },
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const filePath = typeof entry.file === 'string' && path.isAbsolute(entry.file)
|
||||
? entry.file
|
||||
: path.join(MODULE_DIR, entry.file);
|
||||
const query = entry.query || '';
|
||||
const fileUrl = `file:///${filePath.replace(/\\/g, '/')}${query}`;
|
||||
|
||||
try {
|
||||
await page.goto(fileUrl, { waitUntil: 'networkidle', timeout: 15000 });
|
||||
|
||||
// 等待页面渲染完成
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 注入 CSS:移除 phone-shell 限制,展开全部内容
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
.phone-shell {
|
||||
height: auto !important;
|
||||
min-height: 844px !important;
|
||||
max-height: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.scroll-content,
|
||||
[class*="scroll"] {
|
||||
overflow: visible !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
body {
|
||||
background: #fff !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.dropdown-menu, .dropdown-mask, .calendar-mask,
|
||||
.plan-dropdown, .plan-mask {
|
||||
display: none !important;
|
||||
}
|
||||
.bottom-safe {
|
||||
height: 8px !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// 获取实际内容高度
|
||||
const contentHeight = await page.evaluate(() => {
|
||||
const shell = document.querySelector('.phone-shell');
|
||||
if (shell) return shell.scrollHeight;
|
||||
return document.body.scrollHeight;
|
||||
});
|
||||
|
||||
const finalHeight = Math.max(contentHeight || 844, 400);
|
||||
const clipHeight = Math.min(finalHeight + 40, 12000);
|
||||
|
||||
const outName = entry.out || getOutputName(entry);
|
||||
const outPath = path.join(outDir, outName);
|
||||
|
||||
// 长图适当压缩:>1800px 用 65% 质量,普通用 80%
|
||||
const jpegQuality = finalHeight > 1800 ? 65 : 80;
|
||||
|
||||
await page.screenshot({
|
||||
path: outPath,
|
||||
type: 'jpeg',
|
||||
quality: jpegQuality,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
const label = query ? `${entry.file}${query}` : entry.file;
|
||||
console.log(` ✓ ${label} → ${outName} (390×${finalHeight}, q${jpegQuality})`);
|
||||
} catch (err) {
|
||||
console.error(` ✗ ${entry.file}: ${err.message}`);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const allPages = [
|
||||
...PAGES.map(e => ({ ...e, _outDir: SCREENSHOTS_DIR })),
|
||||
...SHARED_PAGES.map(e => ({ ...e, _outDir: e.outDir })),
|
||||
];
|
||||
|
||||
console.log('管理员端(健康应急APP)- 页面截图');
|
||||
console.log(`模块目录: ${MODULE_DIR}`);
|
||||
console.log(`输出目录: ${SCREENSHOTS_DIR}`);
|
||||
console.log(`共 ${allPages.length} 个页面(含共享页面)\n`);
|
||||
|
||||
// 确保所有输出目录存在
|
||||
const dirs = new Set(allPages.map(e => e._outDir));
|
||||
for (const d of dirs) {
|
||||
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
});
|
||||
|
||||
try {
|
||||
for (let i = 0; i < allPages.length; i++) {
|
||||
const entry = allPages[i];
|
||||
const label = entry.query ? `${entry.file}${entry.query}` : entry.file;
|
||||
console.log(`[${i + 1}/${allPages.length}] ${label}`);
|
||||
await screenshotPage(browser, entry, entry._outDir);
|
||||
}
|
||||
console.log(`\n✅ 全部完成!共 ${allPages.length} 张截图`);
|
||||
console.log(` 管理员截图: ${SCREENSHOTS_DIR}`);
|
||||
for (const sp of SHARED_PAGES) {
|
||||
console.log(` 共享截图: ${sp.outDir}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('执行出错:', err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})();
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,187 @@
|
||||
<!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; 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);">
|
||||
<!-- 由JS动态填充内容 -->
|
||||
</div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 知识库数据集合
|
||||
* 每个key对应一个知识条目的完整内容
|
||||
*/
|
||||
var KB_DATA = {
|
||||
'blood-routine': {
|
||||
icon: '🩺', bg: '#e6f4ff', title: '血常规检查解读指南', cat: '体检知识', date: '2026-03-15',
|
||||
summary: '血常规是最基本的血液检验,通过检测血液中各种细胞成分的数量和形态,帮助医生了解患者的基本健康状况,是临床诊断的重要依据之一。',
|
||||
items: [
|
||||
{ color: '#1677ff', name: '白细胞计数(WBC)', desc: '正常范围:4.0–10.0 × 10⁹/L。升高提示感染、炎症或白血病;降低见于病毒感染、药物影响或免疫抑制。' },
|
||||
{ color: '#52c41a', name: '红细胞计数(RBC)', desc: '男性正常范围:4.5–5.5 × 10¹²/L;女性:3.8–5.1 × 10¹²/L。降低提示贫血,升高见于脱水或红细胞增多症。' },
|
||||
{ color: '#fa8c16', name: '血小板计数(PLT)', desc: '正常范围:100–300 × 10⁹/L。降低有出血风险,升高见于炎症或血小板增多症。' }
|
||||
]
|
||||
},
|
||||
'ecg': {
|
||||
icon: '❤️', bg: '#f6ffed', title: '心电图异常结果说明', cat: '体检知识', date: '2026-03-10',
|
||||
summary: '心电图(ECG)是记录心脏电活动的无创检查,常见异常结果需结合临床症状综合判断,不可单凭报告结论下诊断。',
|
||||
items: [
|
||||
{ color: '#ff4d4f', name: 'ST段改变', desc: 'ST段压低≥0.1mV提示心肌缺血;ST段抬高需警惕急性心肌梗死,需立即就医。' },
|
||||
{ color: '#fa8c16', name: '心律失常', desc: '包括窦性心动过速(>100次/分)、窦性心动过缓(<60次/分)、房颤等,需结合症状评估。' },
|
||||
{ color: '#1677ff', name: '束支传导阻滞', desc: '完全性左束支传导阻滞常提示器质性心脏病;右束支传导阻滞可见于正常人,需随访观察。' }
|
||||
]
|
||||
},
|
||||
'liver': {
|
||||
icon: '🔬', bg: '#fff7e6', title: '肝功能指标详解', cat: '体检知识', date: '2026-03-12',
|
||||
summary: '肝功能检查通过检测血液中多种酶和蛋白质水平,评估肝脏的合成、代谢和排泄功能,是发现肝脏疾病的重要手段。',
|
||||
items: [
|
||||
{ color: '#fa8c16', name: '谷丙转氨酶(ALT)', desc: '正常值:男≤50 U/L,女≤40 U/L。升高提示肝细胞损伤,见于病毒性肝炎、脂肪肝、药物性肝损伤等。' },
|
||||
{ color: '#52c41a', name: '谷草转氨酶(AST)', desc: '正常值:≤40 U/L。AST/ALT比值>2提示酒精性肝病;单纯升高也见于心肌损伤。' },
|
||||
{ color: '#1677ff', name: '总胆红素(TBIL)', desc: '正常值:3.4–17.1 μmol/L。升高引起黄疸,需区分溶血性、肝细胞性和梗阻性黄疸。' }
|
||||
]
|
||||
},
|
||||
'glucose': {
|
||||
icon: '💉', bg: '#fff1f0', title: '血糖检测与糖尿病筛查', cat: '体检知识', date: '2026-03-08',
|
||||
summary: '血糖检测是糖尿病筛查和管理的核心手段,包括空腹血糖、餐后2小时血糖和糖化血红蛋白三项主要指标。',
|
||||
items: [
|
||||
{ color: '#ff4d4f', name: '空腹血糖(FPG)', desc: '正常:<6.1 mmol/L;糖尿病前期:6.1–7.0 mmol/L;糖尿病诊断标准:≥7.0 mmol/L(需两次确认)。' },
|
||||
{ color: '#fa8c16', name: '餐后2小时血糖', desc: '正常:<7.8 mmol/L;糖耐量受损:7.8–11.1 mmol/L;≥11.1 mmol/L结合症状可诊断糖尿病。' },
|
||||
{ color: '#722ed1', name: '糖化血红蛋白(HbA1c)', desc: '反映近2–3个月平均血糖水平。正常<5.7%;5.7–6.4%为糖尿病前期;≥6.5%可诊断糖尿病。' }
|
||||
]
|
||||
},
|
||||
'antihypertensive': {
|
||||
icon: '💊', bg: '#e6f4ff', title: '常用降压药物对比', cat: '药品知识', date: '2026-02-20',
|
||||
summary: '降压药物分为五大类,各有适应症和禁忌症,临床选药需结合患者合并症、年龄、肾功能等综合考量。',
|
||||
items: [
|
||||
{ color: '#1677ff', name: 'ACEI/ARB类', desc: '代表药:依那普利、缬沙坦。适用于合并糖尿病、蛋白尿、心衰患者。禁用于妊娠、双侧肾动脉狭窄。' },
|
||||
{ color: '#52c41a', name: '钙通道阻滞剂(CCB)', desc: '代表药:氨氯地平、硝苯地平。适用于老年、单纯收缩期高血压。不良反应:踝部水肿、头痛。' },
|
||||
{ color: '#fa8c16', name: '利尿剂', desc: '代表药:氢氯噻嗪、螺内酯。适用于容量负荷过重、心衰患者。注意监测血钾、血糖、血尿酸。' }
|
||||
]
|
||||
},
|
||||
'statin': {
|
||||
icon: '🧪', bg: '#f6ffed', title: '他汀类药物使用指南', cat: '药品知识', date: '2026-02-15',
|
||||
summary: '他汀类药物是目前最有效的调脂药物,通过抑制HMG-CoA还原酶降低LDL-C,同时具有抗炎、稳定斑块等多效性作用。',
|
||||
items: [
|
||||
{ color: '#52c41a', name: '适应症与目标值', desc: '适用于LDL-C升高、动脉粥样硬化性心血管病(ASCVD)患者。高危患者LDL-C目标<1.8 mmol/L。' },
|
||||
{ color: '#fa8c16', name: '常用药物与剂量', desc: '阿托伐他汀10–80mg/日;瑞舒伐他汀5–40mg/日;辛伐他汀20–40mg/日(晚间服用效果更佳)。' },
|
||||
{ color: '#ff4d4f', name: '副作用监测', desc: '肌肉疼痛(肌病):监测CK;肝功能损害:用药前及用药后4–8周复查ALT/AST;血糖轻度升高。' }
|
||||
]
|
||||
},
|
||||
'hypertension': {
|
||||
icon: '🫀', bg: '#fff7e6', title: '高血压诊断与管理', cat: '疾病知识', date: '2026-03-01',
|
||||
summary: '高血压是最常见的慢性病之一,长期控制不佳可导致心、脑、肾等靶器官损害。规范管理可显著降低心脑血管事件风险。',
|
||||
items: [
|
||||
{ color: '#ff4d4f', name: '诊断标准与分级', desc: '1级:140–159/90–99 mmHg;2级:160–179/100–109 mmHg;3级:≥180/110 mmHg。非同日3次测量均达标方可诊断。' },
|
||||
{ color: '#fa8c16', name: '生活方式干预', desc: '减重(BMI<24)、低盐饮食(<6g/日)、戒烟限酒、规律有氧运动(每周≥150分钟)、减少精神压力。' },
|
||||
{ color: '#1677ff', name: '药物治疗原则', desc: '小剂量起始,优先选用长效制剂,联合用药,个体化治疗。血压目标:一般患者<140/90 mmHg,高危患者<130/80 mmHg。' }
|
||||
]
|
||||
},
|
||||
'migraine': {
|
||||
icon: '🧠', bg: '#fff1f0', title: '偏头痛的诊断与治疗', cat: '疾病知识', date: '2026-03-05',
|
||||
summary: '偏头痛是一种反复发作的神经血管性头痛,以单侧搏动性头痛为主要特征,常伴恶心、畏光、畏声,严重影响生活质量。',
|
||||
items: [
|
||||
{ color: '#ff4d4f', name: '诊断要点', desc: '发作5次以上;持续4–72小时;单侧、搏动性、中重度;活动加重;伴恶心/呕吐或畏光/畏声。需排除继发性头痛。' },
|
||||
{ color: '#fa8c16', name: '急性期治疗', desc: '轻中度:NSAIDs(布洛芬600mg)或对乙酰氨基酚。中重度:曲普坦类(舒马曲坦50–100mg)为首选,发作早期使用效果最佳。' },
|
||||
{ color: '#722ed1', name: '预防性治疗', desc: '每月发作≥4次或严重影响生活时考虑预防用药。常用药物:普萘洛尔、托吡酯、阿米替林、丙戊酸钠。' }
|
||||
]
|
||||
},
|
||||
'nutrition': {
|
||||
icon: '🥗', bg: '#f6ffed', title: '职场人群营养膳食指南', cat: '保健知识', date: '2026-02-28',
|
||||
summary: '久坐办公人群因活动量少、饮食不规律,易出现超重、血脂异常、颈椎病等问题。科学膳食是维护健康的基础。',
|
||||
items: [
|
||||
{ color: '#52c41a', name: '能量与三大营养素', desc: '每日能量摄入:轻体力劳动男性2250kcal,女性1800kcal。碳水化合物50–65%,蛋白质15–20%,脂肪20–30%。' },
|
||||
{ color: '#1677ff', name: '重点营养建议', desc: '增加蔬菜水果(每日500g以上);减少精制碳水和加工食品;优选全谷物;每周鱼类2次以上;控制红肉摄入。' },
|
||||
{ color: '#fa8c16', name: '饮食行为干预', desc: '规律三餐,避免不吃早餐;控制外卖频率;减少含糖饮料;每日饮水1500–1700ml;避免久坐后立即进食。' }
|
||||
]
|
||||
},
|
||||
'exercise': {
|
||||
icon: '🏃', bg: '#e6f4ff', title: '运动与心血管健康', cat: '保健知识', date: '2026-02-25',
|
||||
summary: '规律有氧运动是预防和改善心血管疾病的重要非药物手段,可降低血压、改善血脂、控制体重、减少心血管事件风险。',
|
||||
items: [
|
||||
{ color: '#1677ff', name: '运动推荐量', desc: '每周≥150分钟中等强度有氧运动(如快走、游泳、骑车),或≥75分钟高强度运动。每次至少10分钟,分散进行亦有效。' },
|
||||
{ color: '#52c41a', name: '运动强度判断', desc: '中等强度:运动时心率达最大心率(220-年龄)的50–70%,可说话但不能唱歌。高强度:心率达70–85%,说话困难。' },
|
||||
{ color: '#ff4d4f', name: '特殊人群注意事项', desc: '高血压患者避免憋气用力动作;冠心病患者运动前需评估;运动前热身5–10分钟,运动后拉伸放松,避免突然停止。' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 页面加载时根据URL参数渲染知识详情
|
||||
* 从URL的key参数获取知识条目标识,查找数据并填充页面
|
||||
*/
|
||||
function renderDetail() {
|
||||
// 解析URL参数获取知识条目key
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var key = params.get('key') || 'blood-routine'; // 默认显示血常规
|
||||
var d = KB_DATA[key];
|
||||
if (!d) {
|
||||
// 未找到对应数据时使用默认值
|
||||
d = KB_DATA['blood-routine'];
|
||||
}
|
||||
|
||||
// 设置导航栏标题
|
||||
document.getElementById('kd-nav-title').textContent = d.title;
|
||||
|
||||
// 生成主要内容条目的HTML
|
||||
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,298 @@
|
||||
<!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; 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样式 ========== */
|
||||
.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; }
|
||||
|
||||
/* ========== 底部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; 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; }
|
||||
/* ========== 子分类Tab ========== */
|
||||
.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;
|
||||
}
|
||||
.sub-tab.active { background: #e6f4ff; color: #1677ff; font-weight: 600; }
|
||||
|
||||
/* ========== PDF文档条目 ========== */
|
||||
.pdf-item {
|
||||
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;
|
||||
}
|
||||
.pdf-item__icon {
|
||||
width: 44px; height: 44px; border-radius: 10px; background: #fff1f0;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.pdf-item__info { flex: 1; }
|
||||
.pdf-item__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.pdf-item__meta { font-size: 12px; color: #999; margin-top: 3px; display: flex; gap: 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 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="seg-tabs">
|
||||
<div class="seg-tab active" onclick="switchTopTab(this,'panel-article')">知识文章</div>
|
||||
<div class="seg-tab" onclick="switchTopTab(this,'panel-rule')">规章制度</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识文章面板 -->
|
||||
<div id="panel-article" style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
<!-- 子分类Tab -->
|
||||
<div class="sub-tabs">
|
||||
<div class="sub-tab active" onclick="switchKbTab(this,'kb-exam')">体检知识</div>
|
||||
<div class="sub-tab" onclick="switchKbTab(this,'kb-drug')">药品知识</div>
|
||||
<div class="sub-tab" onclick="switchKbTab(this,'kb-disease')">疾病知识</div>
|
||||
<div class="sub-tab" onclick="switchKbTab(this,'kb-health')">保健知识</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识条目列表 -->
|
||||
<div class="scroll-content">
|
||||
<!-- 体检知识 -->
|
||||
<div id="kb-exam">
|
||||
<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('blood-routine')">
|
||||
<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('ecg')">
|
||||
<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('liver')">
|
||||
<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;">ALT、AST等肝功能指标的临床意义</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('glucose')">
|
||||
<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-drug" 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('antihypertensive')">
|
||||
<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('statin')">
|
||||
<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>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 疾病知识 -->
|
||||
<div id="kb-disease" 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('hypertension')">
|
||||
<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('migraine')">
|
||||
<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-health" 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('nutrition')">
|
||||
<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('exercise')">
|
||||
<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>
|
||||
</div><!-- /panel-article -->
|
||||
|
||||
<!-- 规章制度面板 -->
|
||||
<div id="panel-rule" style="display:none;flex:1;overflow-y:auto;background:#f7f8fa;">
|
||||
<div style="padding:12px 16px 0;">
|
||||
<div class="pdf-item">
|
||||
<div class="pdf-item__icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 2h9l5 5v15H6a2 2 0 01-2-2V4a2 2 0 012-2z" fill="#ff4d4f" opacity="0.15" stroke="#ff4d4f" stroke-width="1.5"/><path d="M15 2v5h5" stroke="#ff4d4f" stroke-width="1.5"/><text x="8" y="17" font-size="7" font-weight="700" fill="#ff4d4f">PDF</text></svg></div>
|
||||
<div class="pdf-item__info">
|
||||
<div class="pdf-item__name">员工健康管理办法(2026版)</div>
|
||||
<div class="pdf-item__meta"><span>PDF · 2.3MB</span><span>2026-01-15</span></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 class="pdf-item">
|
||||
<div class="pdf-item__icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 2h9l5 5v15H6a2 2 0 01-2-2V4a2 2 0 012-2z" fill="#ff4d4f" opacity="0.15" stroke="#ff4d4f" stroke-width="1.5"/><path d="M15 2v5h5" stroke="#ff4d4f" stroke-width="1.5"/><text x="8" y="17" font-size="7" font-weight="700" fill="#ff4d4f">PDF</text></svg></div>
|
||||
<div class="pdf-item__info">
|
||||
<div class="pdf-item__name">职业病防治管理规定</div>
|
||||
<div class="pdf-item__meta"><span>PDF · 1.8MB</span><span>2025-12-20</span></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 class="pdf-item">
|
||||
<div class="pdf-item__icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 2h9l5 5v15H6a2 2 0 01-2-2V4a2 2 0 012-2z" fill="#ff4d4f" opacity="0.15" stroke="#ff4d4f" stroke-width="1.5"/><path d="M15 2v5h5" stroke="#ff4d4f" stroke-width="1.5"/><text x="8" y="17" font-size="7" font-weight="700" fill="#ff4d4f">PDF</text></svg></div>
|
||||
<div class="pdf-item__info">
|
||||
<div class="pdf-item__name">体检异常结果处置流程</div>
|
||||
<div class="pdf-item__meta"><span>PDF · 0.9MB</span><span>2025-11-08</span></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 class="pdf-item">
|
||||
<div class="pdf-item__icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 2h9l5 5v15H6a2 2 0 01-2-2V4a2 2 0 012-2z" fill="#ff4d4f" opacity="0.15" stroke="#ff4d4f" stroke-width="1.5"/><path d="M15 2v5h5" stroke="#ff4d4f" stroke-width="1.5"/><text x="8" y="17" font-size="7" font-weight="700" fill="#ff4d4f">PDF</text></svg></div>
|
||||
<div class="pdf-item__info">
|
||||
<div class="pdf-item__name">健康咨询服务管理细则</div>
|
||||
<div class="pdf-item__meta"><span>PDF · 1.5MB</span><span>2025-10-22</span></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 class="pdf-item">
|
||||
<div class="pdf-item__icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 2h9l5 5v15H6a2 2 0 01-2-2V4a2 2 0 012-2z" fill="#ff4d4f" opacity="0.15" stroke="#ff4d4f" stroke-width="1.5"/><path d="M15 2v5h5" stroke="#ff4d4f" stroke-width="1.5"/><text x="8" y="17" font-size="7" font-weight="700" fill="#ff4d4f">PDF</text></svg></div>
|
||||
<div class="pdf-item__info">
|
||||
<div class="pdf-item__name">应急救护操作规范</div>
|
||||
<div class="pdf-item__meta"><span>PDF · 3.1MB</span><span>2025-09-15</span></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>
|
||||
|
||||
<!-- 底部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">3</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', 'admin');
|
||||
|
||||
/**
|
||||
* 切换一级Tab(知识文章 / 规章制度)
|
||||
*/
|
||||
function switchTopTab(el, panelId) {
|
||||
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) {
|
||||
t.classList.remove('active');
|
||||
});
|
||||
el.classList.add('active');
|
||||
var displayMap = { 'panel-article': 'flex', 'panel-rule': 'block' };
|
||||
['panel-article', 'panel-rule'].forEach(function(id) {
|
||||
var p = document.getElementById(id);
|
||||
if (p) p.style.display = id === panelId ? displayMap[id] : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换知识库子分类Tab
|
||||
* @param {HTMLElement} el - 被点击的Tab元素
|
||||
* @param {string} panelId - 对应面板的ID
|
||||
*/
|
||||
function switchKbTab(el, panelId) {
|
||||
el.closest('.sub-tabs').querySelectorAll('.sub-tab').forEach(function(t) {
|
||||
t.classList.remove('active');
|
||||
});
|
||||
el.classList.add('active');
|
||||
['kb-exam', 'kb-drug', 'kb-disease', 'kb-health'].forEach(function(id) {
|
||||
var p = document.getElementById(id);
|
||||
if (p) p.style.display = id === panelId ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到知识详情页
|
||||
* @param {string} key - 知识条目的标识key
|
||||
*/
|
||||
function goDetail(key) {
|
||||
window.location.href = './detail.html?key=' + key;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>管理员 - 修改密码 | 健康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; }
|
||||
</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 style="padding:20px 20px 8px;font-size:17px;font-weight:700;text-align:center;color:#1a1a1a;">修改成功</div>
|
||||
<div style="padding:8px 20px 20px;font-size:14px;color:#666;text-align:center;line-height:1.6;">密码已更新,下次登录请使用新密码。</div>
|
||||
<div style="display:flex;border-top:1px solid #f0f0f0;">
|
||||
<button onclick="closeModal('modal-pwd-saved');location.href='./index.html'" style="flex:1;padding:14px;font-size:16px;cursor:pointer;background:none;border:none;color:#1677ff;font-weight:600;">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
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'); }
|
||||
sessionStorage.setItem('appRole', 'admin');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,111 @@
|
||||
<!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; text-decoration: none; }
|
||||
.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; }
|
||||
.avatar-section { display: flex; flex-direction: column; align-items: center; padding: 32px 0 24px; background: #fff; }
|
||||
.avatar-wrap { position: relative; cursor: pointer; }
|
||||
.avatar-img { width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, #e6f4ff, #91caff); display: flex; align-items: center; justify-content: center; font-size: 40px; border: 3px solid #f0f0f0; }
|
||||
.avatar-badge { position: absolute; bottom: 0; right: 0; width: 26px; height: 26px; background: #1677ff; border-radius: 50%; border: 2px solid #fff; display: flex; align-items: center; justify-content: center; }
|
||||
.avatar-hint { font-size: 13px; color: #999; margin-top: 10px; }
|
||||
.form-group { background: #fff; margin: 12px 16px; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
|
||||
.form-row { display: flex; align-items: center; padding: 16px; border-bottom: 1px solid #f5f5f5; }
|
||||
.form-row:last-child { border-bottom: none; }
|
||||
.form-label { font-size: 15px; color: #1a1a1a; width: 72px; flex-shrink: 0; }
|
||||
.form-input { flex: 1; font-size: 15px; color: #1a1a1a; border: none; outline: none; background: transparent; text-align: right; }
|
||||
.form-input::placeholder { color: #ccc; }
|
||||
.bottom-action { padding: 16px; flex-shrink: 0; background: #f7f8fa; }
|
||||
.btn-save { width: 100%; padding: 14px; background: linear-gradient(135deg, #1677ff, #0958d9); color: #fff; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; }
|
||||
.action-sheet-mask { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; }
|
||||
.action-sheet-mask.active { display: block; }
|
||||
.action-sheet { position: absolute; bottom: 0; left: 0; right: 0; background: #fff; border-radius: 16px 16px 0 0; padding-bottom: 20px; z-index: 201; transform: translateY(100%); transition: transform 0.3s ease; }
|
||||
.action-sheet-mask.active .action-sheet { transform: translateY(0); }
|
||||
.action-sheet-item { padding: 16px; text-align: center; font-size: 16px; color: #1a1a1a; cursor: pointer; border-bottom: 1px solid #f5f5f5; }
|
||||
.action-sheet-item:active { background: #f5f5f5; }
|
||||
.action-sheet-cancel { padding: 16px; text-align: center; font-size: 16px; color: #999; cursor: pointer; margin-top: 8px; background: #f7f8fa; }
|
||||
.toast { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(0,0,0,0.75); color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px; z-index: 300; }
|
||||
.toast.active { display: block; }
|
||||
</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">
|
||||
<a href="./index.html" class="back-btn">
|
||||
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 1L2 8l6 7"/></svg>返回
|
||||
</a>
|
||||
<span class="nav-title">修改个人信息</span>
|
||||
</div>
|
||||
<div class="scroll-content">
|
||||
<div class="avatar-section">
|
||||
<div class="avatar-wrap" onclick="openSheet()">
|
||||
<div class="avatar-img" id="avatarDisplay">👤</div>
|
||||
<div class="avatar-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="#fff" stroke-width="1.5"><path d="M7 3v8M3 7h8"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="avatar-hint">点击更换头像</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-row">
|
||||
<span class="form-label">姓名</span>
|
||||
<input class="form-input" id="inputName" type="text" value="石维国" placeholder="请输入姓名" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-action">
|
||||
<button class="btn-save" onclick="handleSave()">保存</button>
|
||||
</div>
|
||||
<div id="avatarSheet" class="action-sheet-mask" onclick="closeSheet(event)">
|
||||
<div class="action-sheet">
|
||||
<div class="action-sheet-item" onclick="pickAvatar('camera')">拍照</div>
|
||||
<div class="action-sheet-item" onclick="pickAvatar('album')">从相册选择</div>
|
||||
<div class="action-sheet-cancel" onclick="closeSheet()">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast" class="toast">保存成功</div>
|
||||
</div>
|
||||
<script>
|
||||
function openSheet() {
|
||||
document.getElementById('avatarSheet').classList.add('active');
|
||||
}
|
||||
function closeSheet(e) {
|
||||
if (!e || e.target.id === 'avatarSheet' || e.target.closest('.action-sheet-cancel')) {
|
||||
document.getElementById('avatarSheet').classList.remove('active');
|
||||
}
|
||||
}
|
||||
function pickAvatar(source) {
|
||||
document.getElementById('avatarSheet').classList.remove('active');
|
||||
showToast('已选择:' + (source === 'camera' ? '拍照' : '从相册选择'));
|
||||
}
|
||||
function handleSave() {
|
||||
var name = document.getElementById('inputName').value.trim();
|
||||
if (!name) { showToast('请输入姓名'); return; }
|
||||
showToast('保存成功');
|
||||
}
|
||||
function showToast(msg) {
|
||||
var t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.add('active');
|
||||
setTimeout(function() { t.classList.remove('active'); }, 1500);
|
||||
}
|
||||
sessionStorage.setItem('appRole', 'admin');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!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: transparent; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; position: absolute; top: 0; left: 0; right: 0; z-index: 10; }
|
||||
.status-bar .time { font-size: 15px; font-weight: 700; color: #fff; }
|
||||
.status-bar .icons { display: flex; gap: 6px; align-items: center; font-size: 12px; color: #fff; }
|
||||
.scroll-content { flex: 1; overflow-y: auto; background: #f7f8fa; }
|
||||
.scroll-content::-webkit-scrollbar { display: none; }
|
||||
.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; }
|
||||
.menu-card { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
|
||||
.menu-item { display: flex; align-items: center; padding: 16px; cursor: pointer; border-bottom: 1px solid #f5f5f5; }
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.logout-btn { width: 100%; height: 48px; border: none; border-radius: 12px; background: #fff1f0; color: #ff4d4f; font-size: 15px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
|
||||
.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; }
|
||||
</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:60px 20px 24px;flex-shrink:0;">
|
||||
<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: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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-content" style="padding:12px 16px;">
|
||||
<div class="menu-card">
|
||||
<div class="menu-item" onclick="location.href='./edit-profile.html'"><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 class="menu-item" onclick="location.href='./change-pwd.html'"><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;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="margin-top:16px;">
|
||||
<button class="logout-btn" onclick="openModal('modal-logout')">退出登录</button>
|
||||
</div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
<div id="modal-logout" class="modal-center">
|
||||
<div class="modal-box">
|
||||
<div style="padding:20px 20px 8px;font-size:17px;font-weight:700;text-align:center;color:#1a1a1a;">退出登录</div>
|
||||
<div style="padding:8px 20px 20px;font-size:14px;color:#666;text-align:center;line-height:1.6;">确认退出当前账号?</div>
|
||||
<div style="display:flex;border-top:1px solid #f0f0f0;">
|
||||
<button onclick="closeModal('modal-logout')" style="flex:1;padding:14px;font-size:16px;cursor:pointer;background:none;border:none;color:#666;border-right:1px solid #f0f0f0;">取消</button>
|
||||
<button onclick="closeModal('modal-logout')" style="flex:1;padding:14px;font-size:16px;cursor:pointer;background:none;border:none;color:#ff4d4f;font-weight:600;">确认退出</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">3</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', 'admin');
|
||||
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,289 @@
|
||||
<!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; 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-info { background: #fff; padding: 12px 16px; margin: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.plan-title-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.plan-title-row__title { font-size: 14px; font-weight: 700; color: #1a1a1a; }
|
||||
.plan-title-row__date { font-size: 12px; color: #666; }
|
||||
.plan-select { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f7f8fa; border-radius: 8px; cursor: pointer; position: relative; border: 1px solid #e8e8e8; }
|
||||
.plan-select__text { flex: 1; font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.plan-select__arrow { font-size: 12px; color: #bbb; transition: transform 0.2s; }
|
||||
.plan-select__arrow.open { transform: rotate(180deg); }
|
||||
.plan-meta { display: flex; gap: 0; margin-top: 10px; }
|
||||
.plan-meta__item { flex: 1; text-align: center; padding: 0 4px; }
|
||||
.plan-meta__item + .plan-meta__item { border-left: 1px solid #f0f0f0; }
|
||||
.plan-meta__val { font-size: 15px; font-weight: 700; color: #1a1a1a; }
|
||||
.plan-meta__label { font-size: 10px; color: #999; margin-top: 2px; }
|
||||
|
||||
/* 计划下拉面板 */
|
||||
.plan-dropdown { display: none; position: absolute; top: 100%; left: 0; right: 0; background: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); z-index: 50; margin-top: 4px; max-height: 200px; overflow-y: auto; }
|
||||
.plan-dropdown.active { display: block; }
|
||||
.plan-dropdown__item { padding: 10px 12px; font-size: 13px; color: #333; cursor: pointer; border-bottom: 1px solid #f5f5f5; }
|
||||
.plan-dropdown__item:hover { background: #f7f8fa; }
|
||||
.plan-dropdown__item.selected { background: #e6f4ff; color: #1677ff; font-weight: 600; }
|
||||
|
||||
/* 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: 13px; 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; }
|
||||
|
||||
/* 员工卡片 */
|
||||
.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; }
|
||||
.employee-card__avatar { width: 44px; height: 44px; border-radius: 50%; background: linear-gradient(135deg, #e6f4ff, #b7d8ff); display: flex; align-items: center; justify-content: center; font-size: 20px; color: #1677ff; flex-shrink: 0; font-weight: 700; }
|
||||
.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__meta span { white-space: nowrap; }
|
||||
.employee-card__phone { font-size: 12px; color: #1677ff; flex-shrink: 0; white-space: nowrap; }
|
||||
</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="plan-info">
|
||||
<!-- 标题行:当前计划 + 起止日期 -->
|
||||
<div class="plan-title-row">
|
||||
<span class="plan-title-row__title">体检计划</span>
|
||||
<span class="plan-title-row__date" id="planDateRange">2026-04-01 至 2026-06-30</span>
|
||||
</div>
|
||||
<!-- [交互] 点击展开计划选择下拉 -->
|
||||
<div class="plan-select" onclick="togglePlanDropdown()" style="position:relative;">
|
||||
<span class="plan-select__text" id="planSelectText">2026年度员工健康体检</span>
|
||||
<span class="plan-select__arrow" id="planSelectArrow">▾</span>
|
||||
<div class="plan-dropdown" id="planDropdown">
|
||||
<div class="plan-dropdown__item selected" onclick="selectPlan(event, '2026年度员工健康体检', '2026-04-01', '2026-06-30', '91', '120', '86', '78', '34', '42')">2026年度员工健康体检</div>
|
||||
<div class="plan-dropdown__item" onclick="selectPlan(event, '2025年度员工健康体检', '2025-04-01', '2025-06-30', '91', '115', '115', '110', '0', '5')">2025年度员工健康体检</div>
|
||||
<div class="plan-dropdown__item" onclick="selectPlan(event, '2025年职业病专项体检', '2025-09-01', '2025-10-31', '61', '45', '45', '45', '0', '0')">2025年职业病专项体检</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 统计指标:第一行 -->
|
||||
<div class="plan-meta">
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planDays">91</div>
|
||||
<div class="plan-meta__label">进行天数</div>
|
||||
</div>
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planExpected">120</div>
|
||||
<div class="plan-meta__label">应参检人数</div>
|
||||
</div>
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planBooked" style="color:#1677ff;">86</div>
|
||||
<div class="plan-meta__label">已预约人数</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 统计指标:第二行 -->
|
||||
<div class="plan-meta" style="margin-top:6px;">
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planChecked" style="color:#52c41a;">78</div>
|
||||
<div class="plan-meta__label">已参检人数</div>
|
||||
</div>
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planUnbooked" style="color:#fa8c16;">34</div>
|
||||
<div class="plan-meta__label">未预约人数</div>
|
||||
</div>
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planUnchecked" style="color:#ff4d4f;">42</div>
|
||||
<div class="plan-meta__label">未参检人数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab:已预约 / 已参检 / 未预约 / 未参检 -->
|
||||
<div class="seg-tabs">
|
||||
<div class="seg-tab active" onclick="switchTab(this,'panel-booked')">已预约<span class="badge" id="badgeBooked">86</span></div>
|
||||
<div class="seg-tab" onclick="switchTab(this,'panel-checked')">已参检<span class="badge" id="badgeChecked">78</span></div>
|
||||
<div class="seg-tab" onclick="switchTab(this,'panel-unbooked')">未预约<span class="badge" id="badgeUnbooked">34</span></div>
|
||||
<div class="seg-tab" onclick="switchTab(this,'panel-unchecked')">未参检<span class="badge" id="badgeUnchecked">42</span></div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
|
||||
<!-- 已预约列表 -->
|
||||
<div id="panel-booked">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<!-- 已参检列表 -->
|
||||
<div id="panel-checked" style="display:none;">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<!-- 未预约列表 -->
|
||||
<div id="panel-unbooked" style="display:none;">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<!-- 未参检列表 -->
|
||||
<div id="panel-unchecked" style="display:none;">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== Mock数据 ===== */
|
||||
var bookedList = [
|
||||
{ name:'张小红', empNo:'CQ001256', unit:'公司机关', dept:'办公室', phone:'13812346789' },
|
||||
{ name:'李建国', empNo:'CQ000832', unit:'采油一厂', dept:'技术部', phone:'13923458901' },
|
||||
{ name:'王芳', empNo:'CQ001103', unit:'信息技术中心', dept:'数据部', phone:'13634564567' },
|
||||
{ name:'陈伟', empNo:'CQ000567', unit:'安全环保部', dept:'安环部', phone:'13545677890' },
|
||||
{ name:'赵志强', empNo:'CQ000089', unit:'公司机关', dept:'后勤部', phone:'13756782345' },
|
||||
{ name:'孙丽', empNo:'CQ001340', unit:'采油一厂', dept:'作业区', phone:'15867890123' },
|
||||
{ name:'周明', empNo:'CQ000921', unit:'信息技术中心', dept:'技术部', phone:'18678903456' },
|
||||
{ name:'刘海波', empNo:'CQ000234', unit:'公司机关', dept:'行政部', phone:'13389015678' }
|
||||
];
|
||||
|
||||
var checkedList = [
|
||||
{ name:'孙丽', empNo:'CQ001340', unit:'采油一厂', dept:'作业区', phone:'15867890123' },
|
||||
{ name:'周明', empNo:'CQ000921', unit:'信息技术中心', dept:'技术部', phone:'18678903456' },
|
||||
{ name:'吴静', empNo:'CQ001412', unit:'公司机关', dept:'人力部', phone:'15289016789' },
|
||||
{ name:'黄晓明', empNo:'CQ000789', unit:'采油一厂', dept:'工程部', phone:'18990120123' },
|
||||
{ name:'杨丽华', empNo:'CQ001098', unit:'安全环保部', dept:'安环部', phone:'13101234567' },
|
||||
{ name:'郑国强', empNo:'CQ000456', unit:'公司机关', dept:'财务部', phone:'17712347890' },
|
||||
{ name:'马燕', empNo:'CQ001521', unit:'信息技术中心', dept:'运维部', phone:'15023452345' },
|
||||
{ name:'林伟东', empNo:'CQ000678', unit:'采油一厂', dept:'设备部', phone:'19934563456' }
|
||||
];
|
||||
|
||||
var unbookedList = [
|
||||
{ name:'何秀兰', empNo:'CQ001623', unit:'公司机关', dept:'后勤部', phone:'15545676789' },
|
||||
{ name:'许建军', empNo:'CQ000345', unit:'安全环保部', dept:'监察部', phone:'13256788901' },
|
||||
{ name:'蒋文博', empNo:'CQ001734', unit:'采油一厂', dept:'作业区', phone:'18867894567' },
|
||||
{ name:'沈晓燕', empNo:'CQ000890', unit:'信息技术中心', dept:'开发部', phone:'13678905678' },
|
||||
{ name:'韩磊', empNo:'CQ001845', unit:'公司机关', dept:'财务部', phone:'15789012345' },
|
||||
{ name:'曹敏', empNo:'CQ000456', unit:'采油一厂', dept:'设备部', phone:'18590125678' },
|
||||
{ name:'邓国栋', empNo:'CQ001956', unit:'安全环保部', dept:'安环部', phone:'17301233456' },
|
||||
{ name:'彭丽娟', empNo:'CQ000567', unit:'公司机关', dept:'办公室', phone:'15912345678' }
|
||||
];
|
||||
|
||||
var uncheckedList = [
|
||||
{ name:'张小红', empNo:'CQ001256', unit:'公司机关', dept:'办公室', phone:'13812346789' },
|
||||
{ name:'何秀兰', empNo:'CQ001623', unit:'公司机关', dept:'后勤部', phone:'15545676789' },
|
||||
{ name:'许建军', empNo:'CQ000345', unit:'安全环保部', dept:'监察部', phone:'13256788901' },
|
||||
{ name:'蒋文博', empNo:'CQ001734', unit:'采油一厂', dept:'作业区', phone:'18867894567' },
|
||||
{ name:'曹敏', empNo:'CQ000456', unit:'采油一厂', dept:'设备部', phone:'18590125678' },
|
||||
{ name:'邓国栋', empNo:'CQ001956', unit:'安全环保部', dept:'安环部', phone:'17301233456' },
|
||||
{ name:'彭丽娟', empNo:'CQ000567', unit:'公司机关', dept:'办公室', phone:'15912345678' },
|
||||
{ name:'冯志远', empNo:'CQ002067', unit:'信息技术中心', dept:'运维部', phone:'13423459012' }
|
||||
];
|
||||
|
||||
/* ===== 头像取姓氏 ===== */
|
||||
function getAvatarChar(name) {
|
||||
return name.charAt(0);
|
||||
}
|
||||
|
||||
/* ===== 渲染员工卡片 ===== */
|
||||
function renderEmployeeCard(item) {
|
||||
var html = '<div class="employee-card">';
|
||||
html += ' <div class="employee-card__avatar">' + getAvatarChar(item.name) + '</div>';
|
||||
html += ' <div class="employee-card__info">';
|
||||
html += ' <div class="employee-card__name">' + item.name + ' <span style="font-weight:400;font-size:12px;color:#999;">' + item.empNo + '</span></div>';
|
||||
html += ' <div class="employee-card__meta"><span>' + item.unit + '</span><span>' + item.dept + '</span></div>';
|
||||
html += ' </div>';
|
||||
html += ' <span class="employee-card__phone">' + item.phone + '</span>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList(listId, data) {
|
||||
var html = '';
|
||||
data.forEach(function(item) {
|
||||
html += renderEmployeeCard(item);
|
||||
});
|
||||
document.getElementById(listId).innerHTML = html;
|
||||
}
|
||||
|
||||
/* ===== Tab切换 ===== */
|
||||
function switchTab(el, panelId) {
|
||||
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
['panel-booked','panel-checked','panel-unbooked','panel-unchecked'].forEach(function(id) {
|
||||
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== 计划选择 ===== */
|
||||
function togglePlanDropdown() {
|
||||
var dd = document.getElementById('planDropdown');
|
||||
var arrow = document.getElementById('planSelectArrow');
|
||||
var isOpen = dd.classList.contains('active');
|
||||
dd.classList.toggle('active');
|
||||
if (isOpen) { arrow.classList.remove('open'); } else { arrow.classList.add('open'); }
|
||||
}
|
||||
|
||||
function selectPlan(e, name, startDate, endDate, days, expected, booked, checked, unbooked, unchecked) {
|
||||
e.stopPropagation();
|
||||
document.getElementById('planSelectText').textContent = name;
|
||||
document.getElementById('planDateRange').textContent = startDate + ' 至 ' + endDate;
|
||||
document.getElementById('planDays').textContent = days;
|
||||
document.getElementById('planExpected').textContent = expected;
|
||||
document.getElementById('planBooked').textContent = booked;
|
||||
document.getElementById('planChecked').textContent = checked;
|
||||
document.getElementById('planUnbooked').textContent = unbooked;
|
||||
document.getElementById('planUnchecked').textContent = unchecked;
|
||||
// 更新Tab角标
|
||||
document.getElementById('badgeBooked').textContent = booked;
|
||||
document.getElementById('badgeChecked').textContent = checked;
|
||||
document.getElementById('badgeUnbooked').textContent = unbooked;
|
||||
document.getElementById('badgeUnchecked').textContent = unchecked;
|
||||
// 更新下拉选中态
|
||||
document.querySelectorAll('.plan-dropdown__item').forEach(function(item) {
|
||||
item.classList.toggle('selected', item.textContent === name);
|
||||
});
|
||||
document.getElementById('planDropdown').classList.remove('active');
|
||||
document.getElementById('planSelectArrow').classList.remove('open');
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.plan-select')) {
|
||||
document.getElementById('planDropdown').classList.remove('active');
|
||||
document.getElementById('planSelectArrow').classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== 初始化 ===== */
|
||||
renderList('panel-booked', bookedList);
|
||||
renderList('panel-checked', checkedList);
|
||||
renderList('panel-unbooked', unbookedList);
|
||||
renderList('panel-unchecked', uncheckedList);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,198 @@
|
||||
<!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: #fff; padding: 14px 16px; margin: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.emp-info__name { font-size: 17px; font-weight: 700; color: #1a1a1a; }
|
||||
.emp-info__meta { font-size: 12px; color: #999; margin-top: 4px; display: flex; gap: 8px; }
|
||||
|
||||
/* 数据对比卡片 */
|
||||
.data-compare { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.data-compare__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; }
|
||||
.data-compare__table { display: flex; gap: 0; }
|
||||
.data-compare__col { flex: 1; text-align: center; }
|
||||
.data-compare__col-label { font-size: 11px; color: #999; margin-bottom: 6px; font-weight: 600; }
|
||||
.data-compare__row { font-size: 12px; color: #666; padding: 4px 0; border-bottom: 1px solid #f8f8f8; }
|
||||
.data-compare__row:last-child { border-bottom: none; }
|
||||
.data-compare__row .val { font-weight: 600; }
|
||||
.data-compare__col--initial .val { color: #666; }
|
||||
.data-compare__col--target .val { color: #52c41a; }
|
||||
.data-compare__col--current .val { color: #1677ff; }
|
||||
|
||||
/* 变化指标 */
|
||||
.change-row { display: flex; gap: 0; background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 10px 0; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.change-item { flex: 1; text-align: center; }
|
||||
.change-item + .change-item { border-left: 1px solid #f0f0f0; }
|
||||
.change-item__val { font-size: 16px; font-weight: 700; }
|
||||
.change-item__val--down { color: #52c41a; }
|
||||
.change-item__val--up { color: #ff4d4f; }
|
||||
.change-item__label { font-size: 10px; color: #999; margin-top: 2px; }
|
||||
|
||||
/* 饮食明细标题 */
|
||||
.section-title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin: 14px 12px 6px; }
|
||||
|
||||
/* 饮食明细卡片 */
|
||||
.diet-card { background: #fff; border-radius: 12px; margin: 8px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.diet-card__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.diet-card__meal { font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.diet-card__time { font-size: 12px; color: #999; }
|
||||
.diet-card__row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.diet-card__kcal { font-size: 16px; font-weight: 700; color: #fa8c16; }
|
||||
.diet-card__kcal span { font-size: 12px; font-weight: 400; color: #999; }
|
||||
.diet-card__source { font-size: 11px; }
|
||||
.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; }
|
||||
</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='./eat-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 style="padding:10px 16px;background:#fff;border-bottom:1px solid #f0f0f0;font-size:14px;color:#333;font-weight:600;">2026-06-22</div>
|
||||
|
||||
<!-- 员工基本信息 -->
|
||||
<div class="emp-info">
|
||||
<div class="emp-info__name">张小红</div>
|
||||
<div class="emp-info__meta">
|
||||
<span>女</span><span>32岁</span><span>公司机关 · 办公室</span><span>身高 162cm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 初始 / 目标 / 当前 -->
|
||||
<div class="data-compare">
|
||||
<div class="data-compare__title">数据对比</div>
|
||||
<div class="data-compare__table">
|
||||
<div class="data-compare__col data-compare__col--initial">
|
||||
<div class="data-compare__col-label">初始</div>
|
||||
<div class="data-compare__row">体重 <span class="val">72.5kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">27.6</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">32.1%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">86cm</span></div>
|
||||
</div>
|
||||
<div class="data-compare__col data-compare__col--target">
|
||||
<div class="data-compare__col-label">目标</div>
|
||||
<div class="data-compare__row">体重 <span class="val">62kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">23.6</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">26%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">76cm</span></div>
|
||||
</div>
|
||||
<div class="data-compare__col data-compare__col--current">
|
||||
<div class="data-compare__col-label">当前</div>
|
||||
<div class="data-compare__row">体重 <span class="val">67.2kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">25.6</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">29.5%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">81cm</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 变化指标 -->
|
||||
<div class="change-row">
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-5.3kg</div>
|
||||
<div class="change-item__label">体重变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-2.0</div>
|
||||
<div class="change-item__label">BMI变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-2.6%</div>
|
||||
<div class="change-item__label">体脂变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-5cm</div>
|
||||
<div class="change-item__label">腰围变化</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当日饮食明细 -->
|
||||
<div class="section-title">当日饮食明细</div>
|
||||
|
||||
<!-- 早餐 -->
|
||||
<div class="diet-card">
|
||||
<div class="diet-card__head">
|
||||
<span class="diet-card__meal">🥣 早餐</span>
|
||||
<span class="diet-card__time">摄入时间:2026-06-22 07:30:00</span>
|
||||
</div>
|
||||
<div class="diet-card__row">
|
||||
<div class="diet-card__kcal">486 <span>kcal</span></div>
|
||||
<span class="tag tag-blue">智记</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 午餐 -->
|
||||
<div class="diet-card">
|
||||
<div class="diet-card__head">
|
||||
<span class="diet-card__meal">🍱 午餐</span>
|
||||
<span class="diet-card__time">摄入时间:2026-06-22 12:15:00</span>
|
||||
</div>
|
||||
<div class="diet-card__row">
|
||||
<div class="diet-card__kcal">720 <span>kcal</span></div>
|
||||
<span class="tag tag-blue">智记</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下午加餐 -->
|
||||
<div class="diet-card">
|
||||
<div class="diet-card__head">
|
||||
<span class="diet-card__meal">🍎 加餐</span>
|
||||
<span class="diet-card__time">摄入时间:2026-06-22 15:40:00</span>
|
||||
</div>
|
||||
<div class="diet-card__row">
|
||||
<div class="diet-card__kcal">185 <span>kcal</span></div>
|
||||
<span class="tag tag-purple">自记</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 晚餐 -->
|
||||
<div class="diet-card">
|
||||
<div class="diet-card__head">
|
||||
<span class="diet-card__meal">🍽️ 晚餐</span>
|
||||
<span class="diet-card__time">摄入时间:2026-06-22 18:30:00</span>
|
||||
</div>
|
||||
<div class="diet-card__row">
|
||||
<div class="diet-card__kcal">560 <span>kcal</span></div>
|
||||
<span class="tag tag-blue">智记</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:20px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,253 @@
|
||||
<!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; 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; }
|
||||
|
||||
/* 日期导航 */
|
||||
.date-nav { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; background: #fff; border-bottom: 1px solid #f0f0f0; position: relative; }
|
||||
.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; }
|
||||
.date-nav__cal:active { background: #e6f4ff; }
|
||||
|
||||
/* 日历面板 */
|
||||
.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__nav:active { background: #e6f4ff; }
|
||||
.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; }
|
||||
.cal-panel__day--selected:hover { background: #1677ff; }
|
||||
|
||||
/* 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; }
|
||||
|
||||
/* 员工卡片 */
|
||||
.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__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__arrow { font-size: 12px; color: #1677ff; flex-shrink: 0; white-space: nowrap; }
|
||||
</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='./weight-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="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>
|
||||
|
||||
<!-- Tab:已记录 / 未记录 -->
|
||||
<div class="seg-tabs">
|
||||
<div class="seg-tab active" onclick="switchTab(this,'panel-recorded')">已记录<span class="badge" id="badgeRecorded">78</span></div>
|
||||
<div class="seg-tab" onclick="switchTab(this,'panel-unrecorded')">未记录<span class="badge" id="badgeUnrecorded">8</span></div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
|
||||
<!-- 已记录列表 -->
|
||||
<div id="panel-recorded">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<!-- 未记录列表 -->
|
||||
<div id="panel-unrecorded" style="display:none;">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== Mock数据 ===== */
|
||||
var recordedList = [
|
||||
{ name:'张小红', gender:'女', age:32, dept:'公司机关 · 办公室', height:162 },
|
||||
{ name:'李建国', gender:'男', age:45, dept:'采油一厂 · 技术部', height:175 },
|
||||
{ name:'王芳', gender:'女', age:28, dept:'信息技术中心 · 数据部', height:160 },
|
||||
{ name:'陈伟', gender:'男', age:36, dept:'安全环保部 · 安环部', height:178 },
|
||||
{ name:'赵志强', gender:'男', age:61, dept:'公司机关 · 后勤部', height:170 },
|
||||
{ name:'孙丽', gender:'女', age:41, dept:'采油一厂 · 作业区', height:158 },
|
||||
{ name:'周明', gender:'男', age:33, dept:'信息技术中心 · 技术部', height:182 },
|
||||
{ name:'刘海波', gender:'男', age:55, dept:'公司机关 · 行政部', height:168 }
|
||||
];
|
||||
|
||||
var unrecordedList = [
|
||||
{ name:'吴静', gender:'女', age:26, dept:'公司机关 · 人力部', height:165 },
|
||||
{ name:'黄晓明', gender:'男', age:42, dept:'采油一厂 · 工程部', height:176 },
|
||||
{ name:'杨丽华', gender:'女', age:39, dept:'安全环保部 · 安环部', height:163 },
|
||||
{ name:'郑国强', gender:'男', age:50, dept:'公司机关 · 财务部', height:172 },
|
||||
{ name:'马燕', gender:'女', age:35, dept:'信息技术中心 · 运维部', height:159 },
|
||||
{ name:'林伟东', gender:'男', age:44, dept:'采油一厂 · 设备部', height:180 },
|
||||
{ name:'何秀兰', gender:'女', age:47, dept:'公司机关 · 后勤部', height:161 },
|
||||
{ name:'许建军', gender:'男', age:31, dept:'安全环保部 · 监察部', height:174 }
|
||||
];
|
||||
|
||||
/* ===== Tab切换 ===== */
|
||||
function switchTab(el, panelId) {
|
||||
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
['panel-recorded','panel-unrecorded'].forEach(function(id) {
|
||||
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== 渲染列表 ===== */
|
||||
function renderRecordedList() {
|
||||
var html = '';
|
||||
recordedList.forEach(function(item) {
|
||||
html += '<div class="employee-card" onclick="location.href=\'./diet-employee-detail.html\'">';
|
||||
html += ' <div class="employee-card__info">';
|
||||
html += ' <div class="employee-card__name">' + item.name + '</div>';
|
||||
html += ' <div class="employee-card__meta"><span>' + item.gender + '</span><span>' + item.age + '岁</span><span>' + item.dept + '</span><span>身高 ' + item.height + 'cm</span></div>';
|
||||
html += ' </div>';
|
||||
html += ' <span class="employee-card__arrow">记录明细 ›</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
document.getElementById('panel-recorded').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderUnrecordedList() {
|
||||
var html = '';
|
||||
unrecordedList.forEach(function(item) {
|
||||
html += '<div class="employee-card">';
|
||||
html += ' <div class="employee-card__info">';
|
||||
html += ' <div class="employee-card__name">' + item.name + '</div>';
|
||||
html += ' <div class="employee-card__meta"><span>' + item.gender + '</span><span>' + item.age + '岁</span><span>' + item.dept + '</span><span>身高 ' + item.height + 'cm</span></div>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
});
|
||||
document.getElementById('panel-unrecorded').innerHTML = html;
|
||||
}
|
||||
|
||||
/* ===== 日期导航 ===== */
|
||||
var currentDate = new Date(2026, 5, 22);
|
||||
var calViewYear = currentDate.getFullYear();
|
||||
var calViewMonth = currentDate.getMonth();
|
||||
|
||||
function changeDate(delta) {
|
||||
currentDate.setDate(currentDate.getDate() + delta);
|
||||
calViewYear = currentDate.getFullYear();
|
||||
calViewMonth = currentDate.getMonth();
|
||||
renderDate();
|
||||
}
|
||||
|
||||
function renderDate() {
|
||||
var y = currentDate.getFullYear();
|
||||
var m = String(currentDate.getMonth() + 1).padStart(2, '0');
|
||||
var d = String(currentDate.getDate()).padStart(2, '0');
|
||||
document.getElementById('dateText').textContent = y + '-' + m + '-' + d;
|
||||
}
|
||||
|
||||
/* ===== 日历面板 ===== */
|
||||
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();
|
||||
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();
|
||||
renderDate();
|
||||
renderCal();
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
/* 初始化 */
|
||||
renderRecordedList();
|
||||
renderUnrecordedList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,263 @@
|
||||
<!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; }
|
||||
.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; }
|
||||
.card { background: #fff; border-radius: 12px; margin: 12px 16px 0; box-shadow: 0 1px 4px rgba(0,0,0,0.06); overflow: hidden; }
|
||||
.card__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px; border-bottom: 1px solid #f0f0f0; cursor: pointer; }
|
||||
.card__title { font-size: 15px; font-weight: 700; color: #1a1a1a; }
|
||||
.card__toggle { display: inline-flex; align-items: center; gap: 4px; color: #1677ff; font-size: 12px; font-weight: 600; }
|
||||
.card__arrow { width: 14px; height: 14px; transition: transform .2s ease; }
|
||||
.card__body { padding: 0 16px 16px; }
|
||||
.card--collapsed .card__body { display: none; }
|
||||
.card--collapsed .card__arrow { transform: rotate(-90deg); }
|
||||
.card--collapsed .card__header { border-bottom: none; }
|
||||
.card__tag { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; color: #1677ff; background: #e6f4ff; }
|
||||
.info-item { display: flex; font-size: 13px; line-height: 1.6; padding: 5px 0; }
|
||||
.info-item__label { color: #999; min-width: 72px; flex-shrink: 0; }
|
||||
.info-item__value { color: #333; flex: 1; word-break: break-all; }
|
||||
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
||||
.tag-green { background: #f6ffed; color: #52c41a; }
|
||||
.location-link { color: #1677ff; display: inline-flex; align-items: center; gap: 3px; }
|
||||
.desc-block { padding: 6px 0; }
|
||||
.desc-block__label { font-size: 13px; color: #999; margin-bottom: 6px; }
|
||||
.desc-block__text { font-size: 13px; color: #333; line-height: 1.8; }
|
||||
.form-textarea, .form-input { width: 100%; border: 1px solid #d9d9d9; border-radius: 10px; padding: 10px 12px; font-size: 13px; color: #333; background: #fff; outline: none; }
|
||||
.form-textarea:focus, .form-input:focus { border-color: #1677ff; box-shadow: 0 0 0 2px rgba(22,119,255,0.12); }
|
||||
.form-textarea { min-height: 82px; resize: none; line-height: 1.7; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.photo-list { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
|
||||
.photo-item { width: 90px; height: 68px; border-radius: 8px; background: linear-gradient(135deg, #e6f7ff, #bae7ff); border: 1px solid #d9d9d9; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #1677ff; }
|
||||
.photo-item__mask { position: absolute; inset: auto 0 0 0; background: rgba(0,0,0,0.42); color: #fff; font-size: 10px; text-align: center; padding: 3px 0; }
|
||||
.photo-item__delete { position: absolute; top: -1px; right: -1px; width: 18px; height: 18px; border-radius: 50%; background: #ff4d4f; color: #fff; font-size: 12px; line-height: 18px; text-align: center; cursor: pointer; z-index: 2; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
|
||||
.photo-add { width: 90px; height: 68px; border-radius: 8px; border: 1px dashed #91caff; background: #f7fbff; color: #1677ff; font-size: 12px; cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; }
|
||||
.photo-add span:first-child { font-size: 18px; line-height: 1; }
|
||||
.result-toolbar, .summary-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.result-action-btn { height: 30px; padding: 0 12px; border: none; border-radius: 8px; background: #e6f4ff; color: #1677ff; font-size: 12px; font-weight: 600; cursor: pointer; }
|
||||
.result-empty { border: 1px dashed #d9d9d9; border-radius: 10px; background: #fafafa; color: #999; font-size: 13px; line-height: 1.7; padding: 12px; }
|
||||
.result-filled { border: 1px solid #e8e8e8; border-radius: 10px; background: #fafafa; color: #333; font-size: 13px; line-height: 1.7; padding: 12px; white-space: pre-line; }
|
||||
.summary-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.summary-add-btn, .summary-edit-btn { border: none; border-radius: 8px; font-size: 12px; cursor: pointer; }
|
||||
.summary-add-btn { height: 30px; padding: 0 12px; background: #e6f4ff; color: #1677ff; font-weight: 600; }
|
||||
.summary-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.summary-item { border: 1px solid #e8e8e8; border-radius: 10px; background: #fafafa; padding: 10px 12px; }
|
||||
.summary-item__top { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.summary-item__idx { font-size: 12px; font-weight: 600; color: #1677ff; }
|
||||
.summary-edit-btn { height: 26px; padding: 0 10px; background: #fff; color: #666; border: 1px solid #d9d9d9; }
|
||||
.summary-delete-btn { height: 26px; padding: 0 10px; background: #fff; color: #ff4d4f; border: 1px solid #ffa39e; border-radius: 8px; font-size: 12px; cursor: pointer; }
|
||||
.summary-item__text { font-size: 13px; color: #333; line-height: 1.7; white-space: pre-line; }
|
||||
.action-row { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
.save-btn { height: 36px; border: none; border-radius: 8px; background: #1677ff; color: #fff; font-size: 13px; font-weight: 600; padding: 0 18px; cursor: pointer; }
|
||||
.tip-text { margin-top: 8px; font-size: 12px; color: #999; line-height: 1.6; }
|
||||
.dialog-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.45); display: none; align-items: center; justify-content: center; padding: 20px; z-index: 30; }
|
||||
.dialog-mask.show { display: flex; }
|
||||
.dialog { width: 100%; background: #fff; border-radius: 16px; overflow: hidden; box-shadow: 0 16px 40px rgba(0,0,0,0.18); }
|
||||
.dialog__header { padding: 16px 18px 10px; font-size: 16px; font-weight: 700; color: #1a1a1a; }
|
||||
.dialog__body { padding: 0 18px 18px; }
|
||||
.dialog__hint { font-size: 12px; color: #999; line-height: 1.6; margin-bottom: 10px; }
|
||||
.dialog__footer { display: flex; border-top: 1px solid #f0f0f0; }
|
||||
.dialog__btn { flex: 1; height: 46px; border: none; background: #fff; font-size: 14px; cursor: pointer; }
|
||||
.dialog__btn + .dialog__btn { border-left: 1px solid #f0f0f0; }
|
||||
.dialog__btn--primary { color: #1677ff; font-weight: 600; }
|
||||
.ambulance-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.ambulance-action-btn { height: 30px; padding: 0 12px; border: none; border-radius: 8px; background: #e6f4ff; color: #1677ff; font-size: 12px; font-weight: 600; cursor: pointer; }
|
||||
.ambulance-display { border: 1px solid #e8e8e8; border-radius: 10px; background: #fafafa; padding: 12px; }
|
||||
.ambulance-display__row { display: flex; gap: 8px; margin-bottom: 6px; font-size: 13px; line-height: 1.7; }
|
||||
.ambulance-display__row:last-child { margin-bottom: 0; }
|
||||
.ambulance-display__label { width: 72px; flex-shrink: 0; color: #999; }
|
||||
.ambulance-display__value { flex: 1; color: #333; }
|
||||
.option-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
||||
.option-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid #e8e8e8; border-radius: 10px; background: #fff; cursor: pointer; }
|
||||
.option-item.selected { border-color: #1677ff; background: #f0f7ff; }
|
||||
.option-item.is-hidden { display: none; }
|
||||
.option-item__check { width: 18px; height: 18px; border-radius: 50%; border: 2px solid #d9d9d9; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.option-item.selected .option-item__check { border-color: #1677ff; background: #1677ff; }
|
||||
.option-item.selected .option-item__check::after { content: '✓'; color: #fff; font-size: 11px; font-weight: 700; }
|
||||
.option-item__main { flex: 1; min-width: 0; }
|
||||
.option-item__title { font-size: 13px; font-weight: 600; color: #1a1a1a; }
|
||||
.option-item__meta { margin-top: 2px; font-size: 12px; color: #999; }
|
||||
.option-item__dist { font-size: 12px; color: #1677ff; font-weight: 600; flex-shrink: 0; }
|
||||
.search-box { margin-bottom: 10px; }
|
||||
.search-input { width: 100%; height: 36px; border: 1px solid #d9d9d9; border-radius: 8px; padding: 0 12px 0 36px; font-size: 13px; color: #333; background: #fff url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2214%22 height=%2214%22 fill=%22none%22%3E%3Ccircle cx=%226%22 cy=%226%22 r=%224%22 stroke=%22%23999%22 stroke-width=%221.4%22/%3E%3Cpath d=%22M10 10l2.5 2.5%22 stroke=%22%23999%22 stroke-width=%221.4%22 stroke-linecap=%22round%22/%3E%3C/svg%3E') no-repeat 12px center; outline: none; }
|
||||
.search-input:focus { border-color: #1677ff; }
|
||||
.dialog-field { margin-bottom: 12px; }
|
||||
.dialog-field:last-child { margin-bottom: 0; }
|
||||
.dialog-field__label { font-size: 12px; color: #666; margin-bottom: 4px; }
|
||||
.dialog-field__input { width: 100%; height: 36px; border: 1px solid #d9d9d9; border-radius: 8px; padding: 0 12px; font-size: 13px; color: #333; outline: none; }
|
||||
.dialog-field__input:focus { border-color: #1677ff; }
|
||||
.dialog-field__textarea { width: 100%; min-height: 60px; border: 1px solid #d9d9d9; border-radius: 8px; padding: 8px 12px; font-size: 13px; color: #333; outline: none; resize: none; font-family: inherit; line-height: 1.6; }
|
||||
.dialog-field__textarea:focus { border-color: #1677ff; }
|
||||
</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='./emergency-orders.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="card" id="basic-info-card">
|
||||
<div class="card__header" onclick="toggleCard('basic-info-card', this)">
|
||||
<div class="card__title">基本信息</div>
|
||||
<div class="card__toggle"><span>收起</span><svg class="card__arrow" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6l5 5 5-5"/></svg></div>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<div class="info-item"><span class="info-item__label">工单编号</span><span class="info-item__value">YJ5633256</span></div>
|
||||
<div class="info-item"><span class="info-item__label">应急中心</span><span class="info-item__value">西安应急中心</span></div>
|
||||
<div class="info-item"><span class="info-item__label">求助员工</span><span class="info-item__value">王旭(EMP-56985)</span></div>
|
||||
<div class="info-item"><span class="info-item__label">所属单位</span><span class="info-item__value">公司机关</span></div>
|
||||
<div class="info-item"><span class="info-item__label">所属部门</span><span class="info-item__value">健康科</span></div>
|
||||
<div class="info-item"><span class="info-item__label">求助方式</span><span class="info-item__value">应急视频</span></div>
|
||||
<div class="info-item"><span class="info-item__label">求助时间</span><span class="info-item__value">2026-03-21 15:12:21</span></div>
|
||||
<div class="info-item"><span class="info-item__label">位置信息</span><span class="info-item__value"><span class="location-link"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 7 5 7s5-3.25 5-7c0-2.76-2.24-5-5-5z" stroke="currentColor" stroke-width="1.4"/><circle cx="8" cy="6" r="1.5" stroke="currentColor" stroke-width="1.2"/></svg>陕西省西安市雁塔区电子一路56号</span></span></div>
|
||||
<div class="info-item"><span class="info-item__label">操作人员</span><span class="info-item__value">刘旭</span></div>
|
||||
<div class="info-item"><span class="info-item__label">专业人员</span><span class="info-item__value">刘旭</span></div>
|
||||
<div class="info-item"><span class="info-item__label">工单状态</span><span class="info-item__value"><span class="tag tag-green">已完成</span></span></div>
|
||||
<div class="info-item"><span class="info-item__label">完成时间</span><span class="info-item__value">2026-03-21 16:45:00</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="rescue-info-card">
|
||||
<div class="card__header" onclick="toggleCard('rescue-info-card', this)">
|
||||
<div class="card__title">救助信息</div>
|
||||
<div class="card__toggle"><span>收起</span><svg class="card__arrow" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6l5 5 5-5"/></svg></div>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<div class="desc-block">
|
||||
<div class="desc-block__label">救助意见:</div>
|
||||
<div class="desc-block__text">建议立即送往最近医院急诊科进行头颅CT检查,排除脑血管意外可能。</div>
|
||||
</div>
|
||||
<div class="desc-block">
|
||||
<div class="desc-block__label">救助结果:</div>
|
||||
<div id="result-display" class="result-filled"></div>
|
||||
</div>
|
||||
<div class="desc-block">
|
||||
<div class="desc-block__label">相关照片:</div>
|
||||
<div class="photo-list" id="photo-list-done">
|
||||
<div class="photo-item">现场照片 1</div>
|
||||
<div class="photo-item">现场照片 2</div>
|
||||
<div class="photo-item">现场照片 3</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desc-block">
|
||||
<div class="desc-block__label">补充小结:</div>
|
||||
<div id="summary-list-done" class="summary-list"></div>
|
||||
</div>
|
||||
<!-- <div class="action-row">-->
|
||||
<!-- <button class="save-btn" onclick="saveSection('救助信息')">保存救助信息</button>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="dispatch-info-card">
|
||||
<div class="card__header" onclick="toggleCard('dispatch-info-card', this)">
|
||||
<div class="card__title">派单信息</div>
|
||||
<div class="card__toggle"><span>收起</span><svg class="card__arrow" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6l5 5 5-5"/></svg></div>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<div class="info-item"><span class="info-item__label">派单时间</span><span class="info-item__value">2026-03-21 15:18:00</span></div>
|
||||
<div class="info-item"><span class="info-item__label">驻场人员</span><span class="info-item__value">王希(138-9201-3001)</span></div>
|
||||
<div class="info-item"><span class="info-item__label">派单状态</span><span class="info-item__value"><span class="tag tag-green">已完成</span></span></div>
|
||||
<div class="info-item"><span class="info-item__label">结束时间</span><span class="info-item__value">2026-03-21 16:40:00</span></div>
|
||||
<div class="desc-block">
|
||||
<div class="desc-block__label">派单说明:</div>
|
||||
<div class="desc-block__text">给这个员工紧急挂号就诊,需要拍头颅CT</div>
|
||||
</div>
|
||||
<div class="desc-block">
|
||||
<div class="desc-block__label">驻场说明:</div>
|
||||
<div class="desc-block__text">已经挂号并且开了头颅CT检查单,已陪同就诊完毕,员工状态稳定</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="ambulance-card">
|
||||
<div class="card__header" onclick="toggleCard('ambulance-card', this)">
|
||||
<div class="card__title">救护车支援</div>
|
||||
<div class="card__toggle"><span>收起</span><svg class="card__arrow" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6l5 5 5-5"/></svg></div>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<div class="ambulance-toolbar">
|
||||
<div class="desc-block__label" style="margin-bottom:0;">支援详情</div>
|
||||
</div>
|
||||
<div id="ambulance-display" class="ambulance-display">
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">车牌号</span><span id="amb-d-plate" class="ambulance-display__value">陕A·120-01</span></div>
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">所属机构</span><span id="amb-d-hospital" class="ambulance-display__value">CQ能源职工医院</span></div>
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">驾驶人</span><span id="amb-d-driver" class="ambulance-display__value">李强</span></div>
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">驾驶人电话</span><span id="amb-d-driverphone" class="ambulance-display__value">138-9101-2001</span></div>
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">随行人</span><span id="amb-d-escort" class="ambulance-display__value">张伟</span></div>
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">随行人电话</span><span id="amb-d-escortphone" class="ambulance-display__value">138-9201-5001</span></div>
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">目的地</span><span id="amb-d-dest" class="ambulance-display__value">CQ能源职工医院急诊科</span></div>
|
||||
<div class="ambulance-display__row"><span class="ambulance-display__label">备注</span><span id="amb-d-remark" class="ambulance-display__value">患者疑似脑血管意外,请携带心电监护设备</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
sessionStorage.setItem('appRole', 'admin');
|
||||
|
||||
function toggleCard(cardId, headerEl) {
|
||||
var card = document.getElementById(cardId);
|
||||
card.classList.toggle('card--collapsed');
|
||||
var label = headerEl.querySelector('.card__toggle span');
|
||||
label.textContent = card.classList.contains('card--collapsed') ? '展开' : '收起';
|
||||
}
|
||||
|
||||
var summaryItems = [
|
||||
'接到应急求助后立即抵达现场,员工突发头晕伴恶心呕吐。',
|
||||
'现场监测体征异常,初步处置无效,联系驻场人员陪同就医。',
|
||||
'已完成头颅CT检测,员工由同事留守陪护,已告知家属及部门负责人。'
|
||||
];
|
||||
var resultText = '员工已送达CQ能源职工医院急诊科,完成头颅CT检查,排除脑出血,诊断为高血压引起的眩晕,已开具降压药物并留观。';
|
||||
|
||||
function renderResult() {
|
||||
var el = document.getElementById('result-display');
|
||||
el.className = resultText ? 'result-filled' : 'result-empty';
|
||||
el.innerHTML = resultText ? escapeHtml(resultText).replace(/\n/g, '<br>') : '暂未填写救助结果';
|
||||
}
|
||||
|
||||
function renderSummaryList() {
|
||||
var container = document.getElementById('summary-list-done');
|
||||
container.innerHTML = '';
|
||||
for (var i = 0; i < summaryItems.length; i++) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'summary-item';
|
||||
item.innerHTML = '<div class="summary-item__top"><span class="summary-item__idx">小结 ' + (i + 1) + '</span></div><div class="summary-item__text">' + escapeHtml(summaryItems[i]).replace(/\n/g, '<br>') + '</div>';
|
||||
container.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
renderResult();
|
||||
renderSummaryList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,310 @@
|
||||
<!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; 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; }
|
||||
|
||||
/* 主Tab */
|
||||
.main-tabs { display: flex; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
|
||||
.main-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; }
|
||||
.main-tab.active { color: #1677ff; border-bottom-color: #1677ff; font-weight: 600; }
|
||||
.main-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; }
|
||||
.main-tab.active .badge { background: #e6f4ff; color: #1677ff; }
|
||||
|
||||
/* 筛选区 */
|
||||
.filter-bar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
|
||||
.filter-bar__select { display: flex; align-items: center; gap: 4px; padding: 6px 10px; background: #f7f8fa; border-radius: 6px; border: 1px solid #e8e8e8; cursor: pointer; font-size: 13px; color: #333; flex-shrink: 0; position: relative; }
|
||||
.filter-bar__select--month { flex-shrink: 0; }
|
||||
.filter-bar__select--method { flex-shrink: 0; }
|
||||
.filter-bar__select-arrow { font-size: 10px; color: #bbb; transition: transform 0.2s; }
|
||||
.filter-bar__select-arrow.open { transform: rotate(180deg); }
|
||||
|
||||
/* 月份下拉 */
|
||||
.month-dropdown { display: none; position: absolute; top: 100%; left: 0; background: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); z-index: 50; margin-top: 4px; width: 140px; max-height: 180px; overflow-y: auto; }
|
||||
.month-dropdown.active { display: block; }
|
||||
.month-dropdown__item { padding: 10px 12px; font-size: 13px; color: #333; cursor: pointer; border-bottom: 1px solid #f5f5f5; }
|
||||
.month-dropdown__item:hover { background: #f7f8fa; }
|
||||
.month-dropdown__item.selected { background: #e6f4ff; color: #1677ff; font-weight: 600; }
|
||||
|
||||
/* 工单卡片 */
|
||||
.order-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; }
|
||||
.order-card:active { transform: scale(0.98); }
|
||||
.order-card__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.order-card__time { font-size: 12px; color: #999; }
|
||||
|
||||
/* 求助方式标签 */
|
||||
.method-tag { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.method-tag--video { background: #fff0f6; color: #eb2f96; }
|
||||
.method-tag--phone { background: #fff7e6; color: #fa8c16; }
|
||||
.method-tag--120 { background: #fff1f0; color: #ff4d4f; }
|
||||
|
||||
/* 员工信息行 */
|
||||
.order-card__emp { display: flex; align-items: center; gap: 8px; padding: 10px; background: #f9fafb; border-radius: 8px; margin-bottom: 8px; }
|
||||
.order-card__emp-avatar { width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #e6f4ff, #b7d8ff); display: flex; align-items: center; justify-content: center; font-size: 16px; color: #1677ff; flex-shrink: 0; font-weight: 700; }
|
||||
.order-card__emp-info { flex: 1; min-width: 0; }
|
||||
.order-card__emp-name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.order-card__emp-meta { font-size: 11px; color: #999; margin-top: 2px; display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.order-card__emp-phone { font-size: 12px; color: #1677ff; flex-shrink: 0; }
|
||||
|
||||
/* 处理信息行 */
|
||||
.order-card__detail { display: flex; gap: 0; }
|
||||
.order-card__detail-item { flex: 1; }
|
||||
.order-card__detail-item + .order-card__detail-item { border-left: 1px solid #f0f0f0; padding-left: 10px; margin-left: 10px; }
|
||||
.order-card__detail-label { font-size: 10px; color: #999; margin-bottom: 2px; }
|
||||
.order-card__detail-val { font-size: 13px; color: #333; font-weight: 600; }
|
||||
|
||||
/* 已完成态 */
|
||||
.order-card--done .order-card__header { opacity: 0.8; }
|
||||
</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>
|
||||
|
||||
<!-- Tab:进行中 / 已完成 -->
|
||||
<div class="main-tabs">
|
||||
<div class="main-tab active" onclick="switchMainTab(this,'panel-active')">进行中<span class="badge" id="badgeActive">3</span></div>
|
||||
<div class="main-tab" onclick="switchMainTab(this,'panel-done')">已完成<span class="badge" id="badgeDone">12</span></div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<!-- [交互] 点击展开月份下拉 -->
|
||||
<div class="filter-bar__select filter-bar__select--month" onclick="toggleMonthDropdown()" style="position:relative;">
|
||||
<span id="monthText">月份筛选</span>
|
||||
<span class="filter-bar__select-arrow" id="monthArrow">▾</span>
|
||||
<div class="month-dropdown" id="monthDropdown">
|
||||
<div class="month-dropdown__item selected" onclick="selectMonth(event, '全部', '')">全部</div>
|
||||
<div class="month-dropdown__item" onclick="selectMonth(event, '2026年6月', '2026-06')">2026年6月</div>
|
||||
<div class="month-dropdown__item" onclick="selectMonth(event, '2026年5月', '2026-05')">2026年5月</div>
|
||||
<div class="month-dropdown__item" onclick="selectMonth(event, '2026年4月', '2026-04')">2026年4月</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [交互] 点击展开求助方式下拉 -->
|
||||
<div class="filter-bar__select filter-bar__select--method" onclick="toggleMethodDropdown()" style="position:relative;">
|
||||
<span id="methodText">求助方式</span>
|
||||
<span class="filter-bar__select-arrow" id="methodArrow">▾</span>
|
||||
<div class="month-dropdown" id="methodDropdown">
|
||||
<div class="month-dropdown__item selected" onclick="selectMethod(event, '全部', 'all')">全部</div>
|
||||
<div class="month-dropdown__item" onclick="selectMethod(event, '应急视频', 'video')">应急视频</div>
|
||||
<div class="month-dropdown__item" onclick="selectMethod(event, '应急电话', 'phone')">应急电话</div>
|
||||
<div class="month-dropdown__item" onclick="selectMethod(event, '拨打120', '120')">拨打120</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
|
||||
<!-- 进行中列表 -->
|
||||
<div id="panel-active">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<!-- 已完成列表 -->
|
||||
<div id="panel-done" style="display:none;">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<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:'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:'周伟' }
|
||||
];
|
||||
|
||||
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:'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:'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:'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:'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:'黄志强' }
|
||||
];
|
||||
|
||||
/* ===== 当前筛选状态 ===== */
|
||||
var currentMonth = '';
|
||||
var currentMethod = 'all';
|
||||
|
||||
/* ===== 渲染工具函数 ===== */
|
||||
function getAvatarChar(name) { return name.charAt(0); }
|
||||
|
||||
function getMethodTagClass(method) {
|
||||
if (method === 'video') return 'method-tag--video';
|
||||
if (method === 'phone') return 'method-tag--phone';
|
||||
return 'method-tag--120';
|
||||
}
|
||||
|
||||
function filterByMonth(list) {
|
||||
if (!currentMonth) return list;
|
||||
return list.filter(function(item) {
|
||||
return item.time.indexOf(currentMonth) === 0;
|
||||
});
|
||||
}
|
||||
|
||||
function filterByMethod(list) {
|
||||
if (currentMethod === 'all') return list;
|
||||
return list.filter(function(item) {
|
||||
return item.method === currentMethod;
|
||||
});
|
||||
}
|
||||
|
||||
function getFilteredList(list) {
|
||||
return filterByMethod(filterByMonth(list));
|
||||
}
|
||||
|
||||
function renderOrderCard(item) {
|
||||
var html = '<div class="order-card" onclick="location.href=\'./emergency-order-detail.html\'">';
|
||||
html += ' <div class="order-card__header">';
|
||||
html += ' <span class="method-tag ' + getMethodTagClass(item.method) + '">' + (item.method === 'video' ? '📹 ' : item.method === 'phone' ? '📞 ' : '🚑 ') + item.methodLabel + '</span>';
|
||||
html += ' <span class="order-card__time">' + item.time + '</span>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="order-card__emp">';
|
||||
html += ' <div class="order-card__emp-avatar">' + getAvatarChar(item.empName) + '</div>';
|
||||
html += ' <div class="order-card__emp-info">';
|
||||
html += ' <div class="order-card__emp-name">' + item.empName + ' <span style="font-weight:400;font-size:11px;color:#999;">' + item.empNo + '</span></div>';
|
||||
html += ' <div class="order-card__emp-meta"><span>' + item.empGender + '</span><span>' + item.empAge + '岁</span><span>' + item.empUnit + '</span><span>' + item.empDept + '</span></div>';
|
||||
html += ' </div>';
|
||||
html += ' <span class="order-card__emp-phone">' + item.empPhone + '</span>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="order-card__detail">';
|
||||
html += ' <div class="order-card__detail-item">';
|
||||
html += ' <div class="order-card__detail-label">应急中心</div>';
|
||||
html += ' <div class="order-card__detail-val">' + item.center + '</div>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="order-card__detail-item">';
|
||||
html += ' <div class="order-card__detail-label">操作人员</div>';
|
||||
html += ' <div class="order-card__detail-val">' + item.operator + '</div>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="order-card__detail-item">';
|
||||
html += ' <div class="order-card__detail-label">专业人员</div>';
|
||||
html += ' <div class="order-card__detail-val">' + item.professional + '</div>';
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList(listId, data) {
|
||||
var filtered = getFilteredList(data);
|
||||
var html = '';
|
||||
if (filtered.length === 0) {
|
||||
html = '<div style="text-align:center;padding:40px 16px;color:#999;font-size:14px;">暂无数据</div>';
|
||||
} else {
|
||||
filtered.forEach(function(item) {
|
||||
html += renderOrderCard(item);
|
||||
});
|
||||
}
|
||||
document.getElementById(listId).innerHTML = html;
|
||||
}
|
||||
|
||||
/* ===== 主Tab切换 ===== */
|
||||
function switchMainTab(el, panelId) {
|
||||
el.closest('.main-tabs').querySelectorAll('.main-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
document.getElementById('panel-active').style.display = panelId === 'panel-active' ? 'block' : 'none';
|
||||
document.getElementById('panel-done').style.display = panelId === 'panel-done' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/* ===== 月份筛选 ===== */
|
||||
function toggleMonthDropdown() {
|
||||
var dd = document.getElementById('monthDropdown');
|
||||
var arrow = document.getElementById('monthArrow');
|
||||
var isOpen = dd.classList.contains('active');
|
||||
dd.classList.toggle('active');
|
||||
if (isOpen) { arrow.classList.remove('open'); } else { arrow.classList.add('open'); }
|
||||
}
|
||||
|
||||
function selectMonth(e, label, value) {
|
||||
e.stopPropagation();
|
||||
currentMonth = value;
|
||||
document.getElementById('monthText').textContent = label;
|
||||
document.querySelectorAll('.month-dropdown__item').forEach(function(item) {
|
||||
item.classList.toggle('selected', item.textContent === label);
|
||||
});
|
||||
document.getElementById('monthDropdown').classList.remove('active');
|
||||
document.getElementById('monthArrow').classList.remove('open');
|
||||
// 重新渲染两个列表
|
||||
renderList('panel-active', activeOrders);
|
||||
renderList('panel-done', doneOrders);
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.filter-bar__select--month')) {
|
||||
document.getElementById('monthDropdown').classList.remove('active');
|
||||
document.getElementById('monthArrow').classList.remove('open');
|
||||
}
|
||||
if (!e.target.closest('.filter-bar__select--method')) {
|
||||
document.getElementById('methodDropdown').classList.remove('active');
|
||||
document.getElementById('methodArrow').classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== 求助方式筛选 ===== */
|
||||
function toggleMethodDropdown() {
|
||||
var dd = document.getElementById('methodDropdown');
|
||||
var arrow = document.getElementById('methodArrow');
|
||||
var isOpen = dd.classList.contains('active');
|
||||
dd.classList.toggle('active');
|
||||
if (isOpen) { arrow.classList.remove('open'); } else { arrow.classList.add('open'); }
|
||||
}
|
||||
|
||||
function selectMethod(e, label, value) {
|
||||
e.stopPropagation();
|
||||
currentMethod = value;
|
||||
document.getElementById('methodText').textContent = label;
|
||||
document.querySelectorAll('#methodDropdown .month-dropdown__item').forEach(function(item) {
|
||||
item.classList.toggle('selected', item.textContent === label);
|
||||
});
|
||||
document.getElementById('methodDropdown').classList.remove('active');
|
||||
document.getElementById('methodArrow').classList.remove('open');
|
||||
// 重新渲染两个列表
|
||||
renderList('panel-active', activeOrders);
|
||||
renderList('panel-done', doneOrders);
|
||||
}
|
||||
|
||||
/* ===== 初始化 ===== */
|
||||
renderList('panel-active', activeOrders);
|
||||
renderList('panel-done', doneOrders);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,214 @@
|
||||
<!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; 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; }
|
||||
|
||||
.date-nav { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; background: #fff; border-bottom: 1px solid #f0f0f0; position: relative; }
|
||||
.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; }
|
||||
.date-nav__cal:active { background: #e6f4ff; }
|
||||
|
||||
.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__nav:active { background: #e6f4ff; }
|
||||
.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; }
|
||||
.cal-panel__day--selected:hover { background: #1677ff; }
|
||||
|
||||
.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; }
|
||||
|
||||
.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__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__arrow { font-size: 12px; color: #1677ff; flex-shrink: 0; white-space: nowrap; }
|
||||
</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='./weight-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="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="seg-tabs">
|
||||
<div class="seg-tab active" onclick="switchTab(this,'panel-recorded')">已记录<span class="badge" id="badgeRecorded">65</span></div>
|
||||
<div class="seg-tab" onclick="switchTab(this,'panel-unrecorded')">未记录<span class="badge" id="badgeUnrecorded">21</span></div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
<div id="panel-recorded"><!-- JS动态渲染 --></div>
|
||||
<div id="panel-unrecorded" style="display:none;"><!-- JS动态渲染 --></div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var recordedList = [
|
||||
{ name:'陈伟', gender:'男', age:36, dept:'安全环保部 · 安环部', height:178 },
|
||||
{ name:'王芳', gender:'女', age:28, dept:'信息技术中心 · 数据部', height:160 },
|
||||
{ name:'赵志强', gender:'男', age:61, dept:'公司机关 · 后勤部', height:170 },
|
||||
{ name:'张小红', gender:'女', age:32, dept:'公司机关 · 办公室', height:162 },
|
||||
{ name:'周明', gender:'男', age:33, dept:'信息技术中心 · 技术部', height:182 },
|
||||
{ name:'李建国', gender:'男', age:45, dept:'采油一厂 · 技术部', height:175 },
|
||||
{ name:'孙丽', gender:'女', age:41, dept:'采油一厂 · 作业区', height:158 },
|
||||
{ name:'刘海波', gender:'男', age:55, dept:'公司机关 · 行政部', height:168 }
|
||||
];
|
||||
|
||||
var unrecordedList = [
|
||||
{ name:'吴静', gender:'女', age:26, dept:'公司机关 · 人力部', height:165 },
|
||||
{ name:'黄晓明', gender:'男', age:42, dept:'采油一厂 · 工程部', height:176 },
|
||||
{ name:'马燕', gender:'女', age:35, dept:'信息技术中心 · 运维部', height:159 },
|
||||
{ name:'林伟东', gender:'男', age:44, dept:'采油一厂 · 设备部', height:180 }
|
||||
];
|
||||
|
||||
function switchTab(el, panelId) {
|
||||
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
['panel-recorded','panel-unrecorded'].forEach(function(id) {
|
||||
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function renderRecordedList() {
|
||||
var html = '';
|
||||
recordedList.forEach(function(item) {
|
||||
html += '<div class="employee-card" onclick="location.href=\'./exercise-employee-detail.html\'">';
|
||||
html += ' <div class="employee-card__info">';
|
||||
html += ' <div class="employee-card__name">' + item.name + '</div>';
|
||||
html += ' <div class="employee-card__meta"><span>' + item.gender + '</span><span>' + item.age + '岁</span><span>' + item.dept + '</span><span>身高 ' + item.height + 'cm</span></div>';
|
||||
html += ' </div>';
|
||||
html += ' <span class="employee-card__arrow">记录明细 ›</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
document.getElementById('panel-recorded').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderUnrecordedList() {
|
||||
var html = '';
|
||||
unrecordedList.forEach(function(item) {
|
||||
html += '<div class="employee-card">';
|
||||
html += ' <div class="employee-card__info">';
|
||||
html += ' <div class="employee-card__name">' + item.name + '</div>';
|
||||
html += ' <div class="employee-card__meta"><span>' + item.gender + '</span><span>' + item.age + '岁</span><span>' + item.dept + '</span><span>身高 ' + item.height + 'cm</span></div>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
});
|
||||
document.getElementById('panel-unrecorded').innerHTML = html;
|
||||
}
|
||||
|
||||
var currentDate = new Date(2026, 5, 22);
|
||||
var calViewYear = currentDate.getFullYear();
|
||||
var calViewMonth = currentDate.getMonth();
|
||||
|
||||
function changeDate(delta) {
|
||||
currentDate.setDate(currentDate.getDate() + delta);
|
||||
calViewYear = currentDate.getFullYear(); calViewMonth = currentDate.getMonth();
|
||||
renderDate();
|
||||
}
|
||||
function renderDate() {
|
||||
var y = currentDate.getFullYear();
|
||||
var m = String(currentDate.getMonth() + 1).padStart(2, '0');
|
||||
var d = String(currentDate.getDate()).padStart(2, '0');
|
||||
document.getElementById('dateText').textContent = y + '-' + m + '-' + d;
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
renderDate(); renderCal();
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
renderRecordedList();
|
||||
renderUnrecordedList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,191 @@
|
||||
<!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: #fff; padding: 14px 16px; margin: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.emp-info__name { font-size: 17px; font-weight: 700; color: #1a1a1a; }
|
||||
.emp-info__meta { font-size: 12px; color: #999; margin-top: 4px; display: flex; gap: 8px; }
|
||||
|
||||
.data-compare { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.data-compare__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; }
|
||||
.data-compare__table { display: flex; gap: 0; }
|
||||
.data-compare__col { flex: 1; text-align: center; }
|
||||
.data-compare__col-label { font-size: 11px; color: #999; margin-bottom: 6px; font-weight: 600; }
|
||||
.data-compare__row { font-size: 12px; color: #666; padding: 4px 0; border-bottom: 1px solid #f8f8f8; }
|
||||
.data-compare__row:last-child { border-bottom: none; }
|
||||
.data-compare__row .val { font-weight: 600; }
|
||||
.data-compare__col--initial .val { color: #666; }
|
||||
.data-compare__col--target .val { color: #52c41a; }
|
||||
.data-compare__col--current .val { color: #1677ff; }
|
||||
|
||||
.change-row { display: flex; gap: 0; background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 10px 0; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.change-item { flex: 1; text-align: center; }
|
||||
.change-item + .change-item { border-left: 1px solid #f0f0f0; }
|
||||
.change-item__val { font-size: 16px; font-weight: 700; }
|
||||
.change-item__val--down { color: #52c41a; }
|
||||
.change-item__val--up { color: #ff4d4f; }
|
||||
.change-item__label { font-size: 10px; color: #999; margin-top: 2px; }
|
||||
|
||||
.section-title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin: 14px 12px 6px; }
|
||||
|
||||
.exercise-card { background: #fff; border-radius: 12px; margin: 8px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.exercise-card__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.exercise-card__type { font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.exercise-card__time { font-size: 12px; color: #999; }
|
||||
.exercise-card__row { display: flex; align-items: center; gap: 16px; }
|
||||
.exercise-card__duration { font-size: 15px; font-weight: 700; color: #1677ff; }
|
||||
.exercise-card__duration span { font-size: 11px; font-weight: 400; color: #999; }
|
||||
.exercise-card__kcal { font-size: 15px; font-weight: 700; color: #fa8c16; }
|
||||
.exercise-card__kcal span { font-size: 11px; font-weight: 400; color: #999; }
|
||||
.exercise-card__source { margin-left: auto; }
|
||||
.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; }
|
||||
.tag-orange { background: #fff7e6; color: #fa8c16; }
|
||||
.tag-cyan { background: #e6fffb; color: #13c2c2; }
|
||||
</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='./exercise-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 style="padding:10px 16px;background:#fff;border-bottom:1px solid #f0f0f0;font-size:14px;color:#333;font-weight:600;">2026-06-22</div>
|
||||
|
||||
<div class="emp-info">
|
||||
<div class="emp-info__name">陈伟</div>
|
||||
<div class="emp-info__meta">
|
||||
<span>男</span><span>36岁</span><span>安全环保部 · 安环部</span><span>身高 178cm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-compare">
|
||||
<div class="data-compare__title">数据对比</div>
|
||||
<div class="data-compare__table">
|
||||
<div class="data-compare__col data-compare__col--initial">
|
||||
<div class="data-compare__col-label">初始</div>
|
||||
<div class="data-compare__row">体重 <span class="val">92kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">29.0</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">30.2%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">102cm</span></div>
|
||||
</div>
|
||||
<div class="data-compare__col data-compare__col--target">
|
||||
<div class="data-compare__col-label">目标</div>
|
||||
<div class="data-compare__row">体重 <span class="val">80kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">25.2</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">22%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">90cm</span></div>
|
||||
</div>
|
||||
<div class="data-compare__col data-compare__col--current">
|
||||
<div class="data-compare__col-label">当前</div>
|
||||
<div class="data-compare__row">体重 <span class="val">86.5kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">27.3</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">27.8%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">96cm</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="change-row">
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-5.5kg</div>
|
||||
<div class="change-item__label">体重变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-1.7</div>
|
||||
<div class="change-item__label">BMI变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-2.4%</div>
|
||||
<div class="change-item__label">体脂变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-6cm</div>
|
||||
<div class="change-item__label">腰围变化</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">当日运动明细</div>
|
||||
|
||||
<div class="exercise-card">
|
||||
<div class="exercise-card__head">
|
||||
<span class="exercise-card__type">🏃 跑步</span>
|
||||
<span class="exercise-card__time">2026-06-22 06:30:00</span>
|
||||
</div>
|
||||
<div class="exercise-card__row">
|
||||
<div class="exercise-card__duration">45 <span>分钟</span></div>
|
||||
<div class="exercise-card__kcal">320 <span>千卡</span></div>
|
||||
<span class="exercise-card__source tag tag-cyan">穿戴</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exercise-card">
|
||||
<div class="exercise-card__head">
|
||||
<span class="exercise-card__type">🚴 骑行</span>
|
||||
<span class="exercise-card__time">2026-06-22 12:40:00</span>
|
||||
</div>
|
||||
<div class="exercise-card__row">
|
||||
<div class="exercise-card__duration">30 <span>分钟</span></div>
|
||||
<div class="exercise-card__kcal">180 <span>千卡</span></div>
|
||||
<span class="exercise-card__source tag tag-purple">自记</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exercise-card">
|
||||
<div class="exercise-card__head">
|
||||
<span class="exercise-card__type">🏊 游泳</span>
|
||||
<span class="exercise-card__time">2026-06-22 18:00:00</span>
|
||||
</div>
|
||||
<div class="exercise-card__row">
|
||||
<div class="exercise-card__duration">60 <span>分钟</span></div>
|
||||
<div class="exercise-card__kcal">480 <span>千卡</span></div>
|
||||
<span class="exercise-card__source tag tag-orange">器械</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exercise-card">
|
||||
<div class="exercise-card__head">
|
||||
<span class="exercise-card__type">🚶 步行</span>
|
||||
<span class="exercise-card__time">2026-06-22 20:00:00</span>
|
||||
</div>
|
||||
<div class="exercise-card__row">
|
||||
<div class="exercise-card__duration">8,520 <span>步</span></div>
|
||||
<div class="exercise-card__kcal">150 <span>千卡</span></div>
|
||||
<span class="exercise-card__source tag tag-blue">微信</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:20px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,460 @@
|
||||
<!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; 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; padding-bottom: 20px; }
|
||||
.scroll-content::-webkit-scrollbar { display: none; }
|
||||
|
||||
/* 标签 */
|
||||
.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; }
|
||||
.tag-red { background: #fff1f0; color: #ff4d4f; }
|
||||
|
||||
/* 申请时间顶栏 */
|
||||
.time-bar { background: #fff; padding: 10px 16px; border-bottom: 1px solid #f0f0f0; font-size: 12px; color: #666; display: flex; align-items: center; justify-content: space-between; }
|
||||
.time-bar__label { color: #999; }
|
||||
|
||||
/* 信息卡片 */
|
||||
.info-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.info-card__title { font-size: 13px; font-weight: 600; color: #999; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }
|
||||
|
||||
/* 基本信息 */
|
||||
.info-card__name { font-size: 17px; font-weight: 700; color: #1a1a1a; }
|
||||
.info-card__meta { display: flex; gap: 12px; font-size: 12px; color: #666; margin-top: 6px; }
|
||||
|
||||
/* 数据行 */
|
||||
.data-section { margin-bottom: 12px; }
|
||||
.data-section:last-child { margin-bottom: 0; }
|
||||
.data-section__label { font-size: 11px; color: #999; margin-bottom: 4px; }
|
||||
.data-section__vals { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.data-section__val { font-size: 13px; font-weight: 600; color: #333; }
|
||||
.data-section__val--target { color: #52c41a; }
|
||||
|
||||
/* 分隔线 */
|
||||
.section-divider { height: 1px; background: #f0f0f0; margin: 10px 0; }
|
||||
|
||||
/* 推荐范围 */
|
||||
.range-row { display: flex; gap: 0; }
|
||||
.range-col { flex: 1; text-align: center; padding: 8px 4px; background: #f9fafb; border-radius: 8px; margin: 0 3px; }
|
||||
.range-col__label { font-size: 11px; color: #999; margin-bottom: 4px; }
|
||||
.range-col__range { font-size: 13px; font-weight: 600; color: #1677ff; }
|
||||
|
||||
/* 日期区间 */
|
||||
.date-range-row { display: flex; align-items: center; justify-content: center; gap: 12px; font-size: 14px; color: #333; }
|
||||
.date-range-row .arrow { color: #bbb; font-size: 16px; }
|
||||
|
||||
/* 审核结果信息 */
|
||||
.result-info { display: none; background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.result-info.active { display: block; }
|
||||
.result-info__row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 13px; color: #666; }
|
||||
.result-info__row:last-child { margin-bottom: 0; }
|
||||
.result-info__label { color: #999; font-size: 12px; }
|
||||
|
||||
/* 操作区 - 单选按钮组 */
|
||||
.action-area { display: block; }
|
||||
.action-area.hidden { display: none; }
|
||||
.action-bar { display: flex; gap: 24px; padding: 16px 12px 0; }
|
||||
.radio-item { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; font-weight: 600; color: #333; }
|
||||
.radio-dot { width: 20px; height: 20px; border-radius: 50%; border: 2px solid #d9d9d9; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all 0.2s; }
|
||||
.radio-dot::after { content: ''; width: 10px; height: 10px; border-radius: 50%; background: transparent; transition: all 0.2s; }
|
||||
.radio-item--approve.selected .radio-dot { border-color: #1677ff; }
|
||||
.radio-item--approve.selected .radio-dot::after { background: #1677ff; }
|
||||
.radio-item--reject.selected .radio-dot { border-color: #ff4d4f; }
|
||||
.radio-item--reject.selected .radio-dot::after { background: #ff4d4f; }
|
||||
|
||||
/* 驳回原因 */
|
||||
.reject-reason { display: none; background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.reject-reason.active { display: block; }
|
||||
.reject-reason__label { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 8px; }
|
||||
.reject-reason__textarea { width: 100%; height: 80px; border: 1px solid #d9d9d9; border-radius: 8px; padding: 10px; font-size: 13px; color: #333; resize: none; font-family: inherit; outline: none; }
|
||||
.reject-reason__textarea:focus { border-color: #1677ff; }
|
||||
.reject-reason__textarea::placeholder { color: #ccc; }
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-bar { padding: 12px; }
|
||||
.submit-btn { width: 100%; padding: 12px 0; border-radius: 8px; font-size: 15px; font-weight: 600; text-align: center; cursor: pointer; border: none; background: #1677ff; color: #fff; }
|
||||
.submit-btn:active { background: #0958d9; }
|
||||
.submit-btn:disabled { background: #d9d9d9; color: #999; cursor: not-allowed; }
|
||||
|
||||
/* 确认弹窗 */
|
||||
.overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.45); z-index: 200; justify-content: center; align-items: center; }
|
||||
.overlay.active { display: flex; }
|
||||
.dialog { width: 300px; background: #fff; border-radius: 16px; padding: 24px 20px 16px; text-align: center; }
|
||||
.dialog__title { font-size: 17px; font-weight: 700; color: #1a1a1a; margin-bottom: 8px; }
|
||||
.dialog__msg { font-size: 14px; color: #666; margin-bottom: 20px; line-height: 1.6; }
|
||||
.dialog__actions { display: flex; gap: 10px; }
|
||||
.dialog__btn { flex: 1; padding: 10px 0; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid #d9d9d9; background: #fff; color: #333; }
|
||||
.dialog__btn--primary { background: #1677ff; color: #fff; border-color: #1677ff; }
|
||||
.dialog__btn--danger { background: #ff4d4f; color: #fff; border-color: #ff4d4f; }
|
||||
.dialog__btn:active { opacity: 0.8; }
|
||||
</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='./weight-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="time-bar">
|
||||
<span class="time-bar__label">申请时间</span>
|
||||
<span id="applyTime">2026-06-20 14:30</span>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
|
||||
<!-- 基本信息卡片 -->
|
||||
<div class="info-card">
|
||||
<div class="info-card__name" id="personName">张小红</div>
|
||||
<div class="info-card__meta" id="personMeta">
|
||||
<span>女</span><span>32岁</span><span>身高 162cm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 初始 / 目标 / 推荐范围 合并在一个卡片 -->
|
||||
<div class="info-card">
|
||||
|
||||
<!-- 初始数据 -->
|
||||
<div class="data-section">
|
||||
<div class="data-section__label">初始数据</div>
|
||||
<div class="data-section__vals" id="initialVals">
|
||||
<span class="data-section__val">体重 72.5kg</span>
|
||||
<span class="data-section__val">BMI 27.6</span>
|
||||
<span class="data-section__val">体脂 32.1%</span>
|
||||
<span class="data-section__val">腰围 86cm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- 目标数据 -->
|
||||
<div class="data-section">
|
||||
<div class="data-section__label">目标数据</div>
|
||||
<div class="data-section__vals" id="targetVals">
|
||||
<span class="data-section__val data-section__val--target">体重 62kg</span>
|
||||
<span class="data-section__val data-section__val--target">BMI 23.6</span>
|
||||
<span class="data-section__val data-section__val--target">体脂 26%</span>
|
||||
<span class="data-section__val data-section__val--target">腰围 76cm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- 推荐范围 -->
|
||||
<div class="data-section">
|
||||
<div class="data-section__label">推荐数据范围</div>
|
||||
<div class="range-row" id="rangeVals">
|
||||
<div class="range-col">
|
||||
<div class="range-col__label">体重</div>
|
||||
<div class="range-col__range">58 ~ 65kg</div>
|
||||
</div>
|
||||
<div class="range-col">
|
||||
<div class="range-col__label">体脂</div>
|
||||
<div class="range-col__range">24% ~ 28%</div>
|
||||
</div>
|
||||
<div class="range-col">
|
||||
<div class="range-col__label">腰围</div>
|
||||
<div class="range-col__range">72 ~ 80cm</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 目标起止日期 -->
|
||||
<div class="info-card">
|
||||
<div class="info-card__title">目标起止日期</div>
|
||||
<div class="date-range-row" id="dateRange">
|
||||
<span>2026-06-20</span>
|
||||
<span class="arrow">→</span>
|
||||
<span>2026-09-20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审核操作(待审核时显示) -->
|
||||
<div class="action-area" id="actionArea">
|
||||
<!-- [交互] 点击选择审核结果 -->
|
||||
<div class="action-bar">
|
||||
<div class="radio-item radio-item--approve selected" id="btnApprove" onclick="selectApprove()">
|
||||
<span class="radio-dot"></span><span>审核通过</span>
|
||||
</div>
|
||||
<div class="radio-item radio-item--reject" id="btnReject" onclick="selectReject()">
|
||||
<span class="radio-dot"></span><span>审核驳回</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 驳回原因 -->
|
||||
<div class="reject-reason" id="rejectReason">
|
||||
<div class="reject-reason__label">驳回原因</div>
|
||||
<textarea class="reject-reason__textarea" id="rejectTextarea" placeholder="请输入驳回原因…"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<!-- [交互] 点击提交确认审核结果 -->
|
||||
<div class="submit-bar">
|
||||
<button class="submit-btn" id="submitBtn" onclick="submitReview()">提交审核</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审核结果信息(已审核状态时显示) -->
|
||||
<div class="result-info" id="resultInfo">
|
||||
<div class="result-info__row" id="resultStatus"></div>
|
||||
<div class="result-info__row">
|
||||
<span class="result-info__label">审核人</span>
|
||||
<span id="resultReviewer">刘主管</span>
|
||||
</div>
|
||||
<div class="result-info__row">
|
||||
<span class="result-info__label">审核时间</span>
|
||||
<span id="resultTime">2026-06-16</span>
|
||||
</div>
|
||||
<div class="result-info__row" id="resultReasonRow" style="display:none;">
|
||||
<span class="result-info__label">驳回原因</span>
|
||||
<span id="resultReason">目标设定偏低,建议调整</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<div class="overlay" id="confirmOverlay">
|
||||
<div class="dialog">
|
||||
<div class="dialog__title" id="dialogTitle">确认审核</div>
|
||||
<div class="dialog__msg" id="dialogMsg"></div>
|
||||
<div class="dialog__actions">
|
||||
<button class="dialog__btn" onclick="closeDialog()">取消</button>
|
||||
<button class="dialog__btn dialog__btn--primary" id="dialogConfirmBtn" onclick="confirmAction()">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成功弹窗 -->
|
||||
<div class="overlay" id="successOverlay">
|
||||
<div class="dialog">
|
||||
<div class="dialog__title">审核完成</div>
|
||||
<div class="dialog__msg" id="successMsg"></div>
|
||||
<div class="dialog__actions" style="justify-content:center;">
|
||||
<button class="dialog__btn dialog__btn--primary" style="flex:0 0 auto;padding:10px 40px;" onclick="goBack()">返回列表</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== 人员数据 ===== */
|
||||
var personData = {
|
||||
pending: {
|
||||
name: '张小红', meta: '<span>女</span><span>32岁</span><span>身高 162cm</span>',
|
||||
applyTime: '2026-06-20 14:30',
|
||||
initial: '体重 72.5kg | BMI 27.6 | 体脂 32.1% | 腰围 86cm',
|
||||
target: '体重 62kg | BMI 23.6 | 体脂 26% | 腰围 76cm',
|
||||
range: [
|
||||
{ label:'体重', val:'58 ~ 65kg' },
|
||||
{ label:'体脂', val:'24% ~ 28%' },
|
||||
{ label:'腰围', val:'72 ~ 80cm' }
|
||||
],
|
||||
dateStart: '2026-06-20', dateEnd: '2026-09-20'
|
||||
},
|
||||
approved: {
|
||||
name: '王芳', meta: '<span>女</span><span>28岁</span><span>身高 160cm</span>',
|
||||
applyTime: '2026-06-15 09:00',
|
||||
initial: '体重 65kg | BMI 25.4 | 体脂 30% | 腰围 78cm',
|
||||
target: '体重 56kg | BMI 21.9 | 体脂 24% | 腰围 70cm',
|
||||
range: [
|
||||
{ label:'体重', val:'53 ~ 60kg' },
|
||||
{ label:'体脂', val:'22% ~ 26%' },
|
||||
{ label:'腰围', val:'66 ~ 74cm' }
|
||||
],
|
||||
dateStart: '2026-06-15', dateEnd: '2026-09-15',
|
||||
result: {
|
||||
status: 'approved', reviewer: '刘主管', time: '2026-06-16 10:25:38'
|
||||
}
|
||||
},
|
||||
signed: {
|
||||
name: '陈伟', meta: '<span>男</span><span>36岁</span><span>身高 178cm</span>',
|
||||
applyTime: '2026-06-12 16:20',
|
||||
initial: '体重 92kg | BMI 29.0 | 体脂 30.2% | 腰围 102cm',
|
||||
target: '体重 80kg | BMI 25.2 | 体脂 22% | 腰围 90cm',
|
||||
range: [
|
||||
{ label:'体重', val:'78 ~ 85kg' },
|
||||
{ label:'体脂', val:'20% ~ 24%' },
|
||||
{ label:'腰围', val:'86 ~ 94cm' }
|
||||
],
|
||||
dateStart: '2026-06-12', dateEnd: '2026-09-12',
|
||||
result: {
|
||||
status: 'signed', reviewer: '刘主管', time: '2026-06-14 14:30:15'
|
||||
}
|
||||
},
|
||||
rejected: {
|
||||
name: '刘海波', meta: '<span>男</span><span>52岁</span><span>身高 170cm</span>',
|
||||
applyTime: '2026-06-10 11:00',
|
||||
initial: '体重 85kg | BMI 29.4 | 体脂 33% | 腰围 100cm',
|
||||
target: '体重 70kg | BMI 24.2 | 体脂 24% | 腰围 88cm',
|
||||
range: [
|
||||
{ label:'体重', val:'68 ~ 75kg' },
|
||||
{ label:'体脂', val:'22% ~ 26%' },
|
||||
{ label:'腰围', val:'84 ~ 92cm' }
|
||||
],
|
||||
dateStart: '2026-06-10', dateEnd: '2026-09-10',
|
||||
result: {
|
||||
status: 'rejected', reviewer: '刘主管', time: '2026-06-17 09:12:47', reason: '目标设定偏低,建议调整'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* ===== 解析 URL 参数 ===== */
|
||||
function getQueryParam(name) {
|
||||
var match = location.search.match(new RegExp('[?&]' + name + '=([^&]*)'));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
/* ===== 初始化页面 ===== */
|
||||
function initPage() {
|
||||
var status = getQueryParam('status') || 'pending';
|
||||
var data = personData[status] || personData.pending;
|
||||
|
||||
// 基本信息
|
||||
document.getElementById('personName').textContent = data.name;
|
||||
document.getElementById('personMeta').innerHTML = data.meta;
|
||||
document.getElementById('applyTime').textContent = data.applyTime;
|
||||
|
||||
// 初始数据
|
||||
document.getElementById('initialVals').innerHTML = data.initial.split('|').map(function(s) {
|
||||
return '<span class="data-section__val">' + s.trim() + '</span>';
|
||||
}).join('');
|
||||
|
||||
// 目标数据
|
||||
document.getElementById('targetVals').innerHTML = data.target.split('|').map(function(s) {
|
||||
return '<span class="data-section__val data-section__val--target">' + s.trim() + '</span>';
|
||||
}).join('');
|
||||
|
||||
// 推荐范围
|
||||
document.getElementById('rangeVals').innerHTML = data.range.map(function(r) {
|
||||
return '<div class="range-col"><div class="range-col__label">' + r.label + '</div><div class="range-col__range">' + r.val + '</div></div>';
|
||||
}).join('');
|
||||
|
||||
// 日期
|
||||
document.getElementById('dateRange').innerHTML = '<span>' + data.dateStart + '</span><span class="arrow">→</span><span>' + data.dateEnd + '</span>';
|
||||
|
||||
// 审核结果 / 操作区
|
||||
if (status === 'pending') {
|
||||
document.getElementById('actionArea').classList.remove('hidden');
|
||||
document.getElementById('resultInfo').classList.remove('active');
|
||||
selectApprove();
|
||||
} else {
|
||||
document.getElementById('actionArea').classList.add('hidden');
|
||||
document.getElementById('resultInfo').classList.add('active');
|
||||
|
||||
var r = data.result;
|
||||
// 状态标签
|
||||
var tagHtml = '';
|
||||
if (r.status === 'approved') tagHtml = '<span class="tag tag-green">已通过</span>';
|
||||
else if (r.status === 'signed') tagHtml = '<span class="tag tag-orange">待签署</span>';
|
||||
else if (r.status === 'rejected') tagHtml = '<span class="tag tag-red">已驳回</span>';
|
||||
document.getElementById('resultStatus').innerHTML = tagHtml;
|
||||
|
||||
// 审核人 & 时间
|
||||
document.getElementById('resultReviewer').textContent = r.reviewer;
|
||||
document.getElementById('resultTime').textContent = r.time;
|
||||
|
||||
// 驳回原因
|
||||
if (r.reason) {
|
||||
document.getElementById('resultReasonRow').style.display = 'flex';
|
||||
document.getElementById('resultReason').textContent = r.reason;
|
||||
} else {
|
||||
document.getElementById('resultReasonRow').style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectedAction = null;
|
||||
|
||||
function selectApprove() {
|
||||
selectedAction = 'approve';
|
||||
document.getElementById('btnApprove').classList.add('selected');
|
||||
document.getElementById('btnReject').classList.remove('selected');
|
||||
document.getElementById('rejectReason').classList.remove('active');
|
||||
}
|
||||
|
||||
function selectReject() {
|
||||
selectedAction = 'reject';
|
||||
document.getElementById('btnReject').classList.add('selected');
|
||||
document.getElementById('btnApprove').classList.remove('selected');
|
||||
document.getElementById('rejectReason').classList.add('active');
|
||||
document.getElementById('rejectTextarea').focus();
|
||||
}
|
||||
|
||||
function submitReview() {
|
||||
if (!selectedAction) {
|
||||
alert('请选择审核结果(通过或驳回)');
|
||||
return;
|
||||
}
|
||||
if (selectedAction === 'reject') {
|
||||
var reason = document.getElementById('rejectTextarea').value.trim();
|
||||
if (!reason) { alert('请输入驳回原因'); return; }
|
||||
showConfirmDialog('驳回', '确认驳回该目标申请?<br>驳回原因:' + reason, 'reject');
|
||||
} else {
|
||||
showConfirmDialog('通过', '确认通过该目标申请?', 'approve');
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirmDialog(title, msg, action) {
|
||||
document.getElementById('dialogTitle').textContent = title;
|
||||
document.getElementById('dialogMsg').innerHTML = msg;
|
||||
var confirmBtn = document.getElementById('dialogConfirmBtn');
|
||||
if (action === 'reject') {
|
||||
confirmBtn.className = 'dialog__btn dialog__btn--danger';
|
||||
confirmBtn.textContent = '确认驳回';
|
||||
} else {
|
||||
confirmBtn.className = 'dialog__btn dialog__btn--primary';
|
||||
confirmBtn.textContent = '确认通过';
|
||||
}
|
||||
confirmBtn.setAttribute('data-action', action);
|
||||
document.getElementById('confirmOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
document.getElementById('confirmOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
function confirmAction() {
|
||||
var action = document.getElementById('dialogConfirmBtn').getAttribute('data-action');
|
||||
document.getElementById('confirmOverlay').classList.remove('active');
|
||||
var msg = action === 'approve' ? '该目标申请已审核通过' : '该目标申请已驳回';
|
||||
document.getElementById('successMsg').textContent = msg;
|
||||
document.getElementById('successOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
location.href = './weight-detail.html';
|
||||
}
|
||||
|
||||
/* 启动 */
|
||||
initPage();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,131 @@
|
||||
<!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; }
|
||||
/* 功能入口卡片 */
|
||||
.func-card { background: #fff; border-radius: 12px; margin: 10px 16px 0; padding: 16px; cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.06); transition: transform 0.15s; }
|
||||
.func-card:active { transform: scale(0.98); }
|
||||
.func-card__header { display: flex; align-items: center; gap: 12px; }
|
||||
.func-card__icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; flex-shrink: 0; }
|
||||
.func-card__icon--blue { background: linear-gradient(135deg, #e6f4ff, #b7d8ff); }
|
||||
.func-card__icon--green { background: linear-gradient(135deg, #f6ffed, #b7eb8f); }
|
||||
.func-card__icon--orange { background: linear-gradient(135deg, #fff7e6, #ffd591); }
|
||||
.func-card__icon--red { background: linear-gradient(135deg, #fff1f0, #ffccc7); }
|
||||
.func-card__info { flex: 1; min-width: 0; }
|
||||
.func-card__title { font-size: 16px; font-weight: 600; color: #1a1a1a; }
|
||||
.func-card__desc { font-size: 12px; color: #999; margin-top: 2px; }
|
||||
.func-card__arrow { font-size: 14px; color: #bbb; 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 style="background:linear-gradient(135deg,#1677ff,#0958d9);padding:16px 20px 20px;flex-shrink:0;">
|
||||
<!-- 个人信息行 -->
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<!-- [交互] 点击头像可跳转至个人中心 -->
|
||||
<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: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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能入口卡片列表 -->
|
||||
<div class="scroll-content">
|
||||
|
||||
<!-- [交互] 点击跳转至: ./weight-detail.html -->
|
||||
<div class="func-card" onclick="location.href='./weight-detail.html'">
|
||||
<div class="func-card__header">
|
||||
<div class="func-card__icon func-card__icon--blue">🏋️</div>
|
||||
<div class="func-card__info">
|
||||
<div class="func-card__title">体重管理详情</div>
|
||||
<div class="func-card__desc">查看员工每日记录数据、减重排名及目标审核</div>
|
||||
</div>
|
||||
<span class="func-card__arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- [交互] 点击跳转至: ./checkup-progress.html -->
|
||||
<div class="func-card" onclick="location.href='./checkup-progress.html'">
|
||||
<div class="func-card__header">
|
||||
<div class="func-card__icon func-card__icon--green">🏥</div>
|
||||
<div class="func-card__info">
|
||||
<div class="func-card__title">体检进度跟踪</div>
|
||||
<div class="func-card__desc">查看体检计划下员工预约、体检具体进度</div>
|
||||
</div>
|
||||
<span class="func-card__arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- [交互] 点击跳转至: ./emergency-orders.html -->
|
||||
<div class="func-card" onclick="location.href='./emergency-orders.html'">
|
||||
<div class="func-card__header">
|
||||
<div class="func-card__icon func-card__icon--orange">🚑</div>
|
||||
<div class="func-card__info">
|
||||
<div class="func-card__title">应急求助工单</div>
|
||||
<div class="func-card__desc">查看员工发起应急求助记录及跟踪过程</div>
|
||||
</div>
|
||||
<span class="func-card__arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- [交互] 点击跳转至: ./wearable-alerts.html -->
|
||||
<div class="func-card" onclick="location.href='./wearable-alerts.html'">
|
||||
<div class="func-card__header">
|
||||
<div class="func-card__icon func-card__icon--red">⌚</div>
|
||||
<div class="func-card__info">
|
||||
<div class="func-card__title">穿戴预警记录</div>
|
||||
<div class="func-card__desc">查看佩戴穿戴设备的员工预警记录</div>
|
||||
</div>
|
||||
<span class="func-card__arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:16px;"></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></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', 'admin');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,268 @@
|
||||
<!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; 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; }
|
||||
|
||||
/* 筛选区 */
|
||||
.filter-bar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
|
||||
.filter-bar__select { display: flex; align-items: center; gap: 4px; padding: 6px 10px; background: #f7f8fa; border-radius: 6px; border: 1px solid #e8e8e8; cursor: pointer; font-size: 13px; color: #333; flex-shrink: 0; position: relative; }
|
||||
.filter-bar__select-arrow { font-size: 10px; color: #bbb; transition: transform 0.2s; }
|
||||
.filter-bar__select-arrow.open { transform: rotate(180deg); }
|
||||
|
||||
/* 下拉面板 */
|
||||
.type-dropdown { display: none; position: absolute; top: 100%; left: 0; background: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); z-index: 50; margin-top: 4px; width: 140px; max-height: 220px; overflow-y: auto; }
|
||||
.type-dropdown.active { display: block; }
|
||||
.type-dropdown__item { padding: 10px 12px; font-size: 13px; color: #333; cursor: pointer; border-bottom: 1px solid #f5f5f5; }
|
||||
.type-dropdown__item:hover { background: #f7f8fa; }
|
||||
.type-dropdown__item.selected { background: #e6f4ff; color: #1677ff; font-weight: 600; }
|
||||
|
||||
/* 预警标签 */
|
||||
.alert-tag { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }
|
||||
.alert-tag--sos { background: #fff1f0; color: #ff4d4f; }
|
||||
.alert-tag--temp { background: #fff7e6; color: #fa8c16; }
|
||||
.alert-tag--heart { background: #fff0f6; color: #eb2f96; }
|
||||
.alert-tag--spo2 { background: #f6ffed; color: #52c41a; }
|
||||
.alert-tag--stress { background: #f9f0ff; color: #722ed1; }
|
||||
.alert-tag--fall { background: #fffbe6; color: #d4b106; }
|
||||
|
||||
/* 预警卡片 */
|
||||
.alert-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.alert-card__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.alert-card__time { font-size: 12px; color: #999; }
|
||||
.alert-card__abnormal { padding: 8px 10px; background: #f9fafb; border-radius: 8px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.alert-card__abnormal-label { font-size: 12px; color: #999; flex-shrink: 0; }
|
||||
.alert-card__abnormal-val { font-size: 14px; font-weight: 700; color: #ff4d4f; }
|
||||
.alert-card__abnormal-unit { font-size: 11px; color: #999; font-weight: 400; margin-left: 2px; }
|
||||
|
||||
/* 员工信息行 */
|
||||
.alert-card__emp { display: flex; align-items: center; gap: 8px; padding: 10px; background: #f9fafb; border-radius: 8px; margin-bottom: 8px; }
|
||||
.alert-card__emp-avatar { width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #e6f4ff, #b7d8ff); display: flex; align-items: center; justify-content: center; font-size: 16px; color: #1677ff; flex-shrink: 0; font-weight: 700; }
|
||||
.alert-card__emp-info { flex: 1; min-width: 0; }
|
||||
.alert-card__emp-name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.alert-card__emp-meta { font-size: 11px; color: #999; margin-top: 2px; display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.alert-card__address { font-size: 12px; color: #666; padding: 6px 0 0; display: flex; align-items: flex-start; gap: 4px; }
|
||||
.alert-card__address-label { color: #999; flex-shrink: 0; font-size: 12px; }
|
||||
.alert-card__address-text { color: #666; font-size: 12px; }
|
||||
</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="filter-bar">
|
||||
<!-- [交互] 点击展开月份下拉 -->
|
||||
<div class="filter-bar__select" onclick="toggleMonthDropdown()" style="position:relative;">
|
||||
<span id="monthText">月份筛选</span>
|
||||
<span class="filter-bar__select-arrow" id="monthArrow">▾</span>
|
||||
<div class="type-dropdown" id="monthDropdown">
|
||||
<div class="type-dropdown__item" onclick="selectMonth(event, '全部', '')">全部</div>
|
||||
<div class="type-dropdown__item" onclick="selectMonth(event, '2026年6月', '2026-06')">2026年6月</div>
|
||||
<div class="type-dropdown__item" onclick="selectMonth(event, '2026年5月', '2026-05')">2026年5月</div>
|
||||
<div class="type-dropdown__item" onclick="selectMonth(event, '2026年4月', '2026-04')">2026年4月</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [交互] 点击展开预警类型下拉 -->
|
||||
<div class="filter-bar__select" onclick="toggleTypeDropdown()" style="position:relative;">
|
||||
<span id="typeText">预警类型</span>
|
||||
<span class="filter-bar__select-arrow" id="typeArrow">▾</span>
|
||||
<div class="type-dropdown" id="typeDropdown">
|
||||
<div class="type-dropdown__item" onclick="selectType(event, '全部', 'all')">全部</div>
|
||||
<div class="type-dropdown__item" onclick="selectType(event, '一键告警', 'sos')">一键告警</div>
|
||||
<div class="type-dropdown__item" onclick="selectType(event, '体温异常', 'temp')">体温异常</div>
|
||||
<div class="type-dropdown__item" onclick="selectType(event, '心率异常', 'heart')">心率异常</div>
|
||||
<div class="type-dropdown__item" onclick="selectType(event, '血氧异常', 'spo2')">血氧异常</div>
|
||||
<div class="type-dropdown__item" onclick="selectType(event, '压力异常', 'stress')">压力异常</div>
|
||||
<div class="type-dropdown__item" onclick="selectType(event, '摔倒', 'fall')">摔倒</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
|
||||
<!-- 统计条 -->
|
||||
<div style="padding:10px 16px;font-size:12px;color:#999;" id="alertCount">共 12 条预警</div>
|
||||
|
||||
<div id="alert-list">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== Mock数据 ===== */
|
||||
var alertRecords = [
|
||||
{ type:'sos', typeLabel:'一键告警', time:'2026-06-22 14:35:00', abnormal:'一键告警', abnormalUnit:'', empName:'张小红', empGender:'女', empAge:32, empNo:'CQ001256', empUnit:'公司机关', empDept:'办公室', address:'陕西省西安市雁塔区电子一路56号办公楼3层' },
|
||||
{ type:'temp', typeLabel:'体温异常', time:'2026-06-22 10:12:00', abnormal:'38.7', abnormalUnit:'℃', empName:'李建国', empGender:'男', empAge:45, empNo:'CQ000832', empUnit:'采油一厂', empDept:'技术部', address:'陕西省西安市未央区凤城八路108号采油一厂A区' },
|
||||
{ type:'heart', typeLabel:'心率异常', time:'2026-06-21 08:20:00', abnormal:'128', abnormalUnit:'次/分', empName:'赵志强', empGender:'男', empAge:61, empNo:'CQ000089', empUnit:'公司机关', empDept:'后勤部', address:'陕西省西安市雁塔区电子一路56号' },
|
||||
{ type:'spo2', typeLabel:'血氧异常', time:'2026-06-20 16:05:00', abnormal:'91', abnormalUnit:'%', empName:'王芳', empGender:'女', empAge:28, empNo:'CQ001103', empUnit:'信息技术中心', empDept:'数据部', address:'陕西省西安市高新区高新路88号信息大厦12层' },
|
||||
{ type:'stress', typeLabel:'压力异常', time:'2026-06-19 11:30:00', abnormal:'高', abnormalUnit:'', empName:'陈伟', empGender:'男', empAge:36, empNo:'CQ000567', empUnit:'安全环保部', empDept:'安环部', address:'陕西省西安市碑林区友谊西路127号' },
|
||||
{ type:'fall', typeLabel:'摔倒', time:'2026-06-18 15:45:00', abnormal:'检测到摔倒', abnormalUnit:'', empName:'孙丽', empGender:'女', empAge:41, empNo:'CQ001340', empUnit:'采油一厂', empDept:'作业区', address:'陕西省西安市未央区凤城八路108号采油一厂B区2层' },
|
||||
{ type:'temp', typeLabel:'体温异常', time:'2026-06-17 09:10:00', abnormal:'39.1', abnormalUnit:'℃', empName:'周明', empGender:'男', empAge:33, empNo:'CQ000921', empUnit:'信息技术中心', empDept:'技术部', address:'陕西省西安市高新区高新路88号信息大厦15层' },
|
||||
{ type:'heart', typeLabel:'心率异常', time:'2026-06-16 22:30:00', abnormal:'135', abnormalUnit:'次/分', empName:'刘海波', empGender:'男', empAge:55, empNo:'CQ000234', empUnit:'公司机关', empDept:'行政部', address:'陕西省西安市雁塔区电子一路56号办公楼5层' },
|
||||
{ type:'sos', typeLabel:'一键告警', time:'2026-06-15 07:55:00', abnormal:'一键告警', abnormalUnit:'', empName:'吴静', empGender:'女', empAge:26, empNo:'CQ001412', empUnit:'公司机关', empDept:'人力部', address:'陕西省西安市雁塔区电子一路56号办公楼2层' },
|
||||
{ type:'spo2', typeLabel:'血氧异常', time:'2026-06-14 13:20:00', abnormal:'89', abnormalUnit:'%', empName:'黄晓明', empGender:'男', empAge:42, empNo:'CQ000789', empUnit:'采油一厂', empDept:'工程部', address:'陕西省西安市未央区凤城八路108号采油一厂C区' },
|
||||
{ type:'fall', typeLabel:'摔倒', time:'2026-06-13 18:15:00', abnormal:'检测到摔倒', abnormalUnit:'', empName:'杨丽华', empGender:'女', empAge:39, empNo:'CQ001098', empUnit:'安全环保部', empDept:'安环部', address:'陕西省西安市碑林区友谊西路127号' },
|
||||
{ type:'stress', typeLabel:'压力异常', time:'2026-06-12 10:40:00', abnormal:'极高', abnormalUnit:'', empName:'郑国强', empGender:'男', empAge:50, empNo:'CQ000456', empUnit:'公司机关', empDept:'财务部', address:'陕西省西安市雁塔区电子一路56号办公楼6层' }
|
||||
];
|
||||
|
||||
/* ===== 工具函数 ===== */
|
||||
function getAvatarChar(name) { return name.charAt(0); }
|
||||
|
||||
function getTypeTagClass(type) {
|
||||
var map = {
|
||||
sos: 'alert-tag--sos', temp: 'alert-tag--temp', heart: 'alert-tag--heart',
|
||||
spo2: 'alert-tag--spo2', stress: 'alert-tag--stress', fall: 'alert-tag--fall'
|
||||
};
|
||||
return map[type] || '';
|
||||
}
|
||||
|
||||
function getTypeIcon(type) {
|
||||
var map = { sos:'🆘', temp:'🌡️', heart:'💓', spo2:'🫁', stress:'😰', fall:'🚨' };
|
||||
return map[type] || '';
|
||||
}
|
||||
|
||||
/* ===== 筛选状态 ===== */
|
||||
var currentMonth = '';
|
||||
var currentType = 'all';
|
||||
|
||||
function filterByMonth(list) {
|
||||
if (!currentMonth) return list;
|
||||
return list.filter(function(item) { return item.time.indexOf(currentMonth) === 0; });
|
||||
}
|
||||
|
||||
function filterByType(list) {
|
||||
if (currentType === 'all') return list;
|
||||
return list.filter(function(item) { return item.type === currentType; });
|
||||
}
|
||||
|
||||
/* ===== 渲染 ===== */
|
||||
function renderAlertCard(item) {
|
||||
var html = '<div class="alert-card">';
|
||||
html += ' <div class="alert-card__header">';
|
||||
html += ' <span style="font-size:14px;font-weight:700;color:#1a1a1a;">' + item.typeLabel + '</span>';
|
||||
html += ' <span class="alert-card__time">' + item.time + '</span>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="alert-card__abnormal">';
|
||||
html += ' <span class="alert-card__abnormal-label">异常值</span>';
|
||||
html += ' <span class="alert-card__abnormal-val">' + item.abnormal + '</span>';
|
||||
if (item.abnormalUnit) {
|
||||
html += ' <span class="alert-card__abnormal-unit">' + item.abnormalUnit + '</span>';
|
||||
}
|
||||
html += ' </div>';
|
||||
html += ' <div class="alert-card__emp">';
|
||||
html += ' <div class="alert-card__emp-avatar">' + getAvatarChar(item.empName) + '</div>';
|
||||
html += ' <div class="alert-card__emp-info">';
|
||||
html += ' <div class="alert-card__emp-name">' + item.empName + ' <span style="font-weight:400;font-size:11px;color:#999;">' + item.empNo + '</span></div>';
|
||||
html += ' <div class="alert-card__emp-meta"><span>' + item.empGender + '</span><span>' + item.empAge + '岁</span><span>' + item.empUnit + '</span><span>' + item.empDept + '</span></div>';
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="alert-card__address">';
|
||||
html += ' <span class="alert-card__address-label">📍</span>';
|
||||
html += ' <span class="alert-card__address-text">' + item.address + '</span>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
var filtered = filterByType(filterByMonth(alertRecords));
|
||||
document.getElementById('alertCount').textContent = '共 ' + filtered.length + ' 条预警';
|
||||
var html = '';
|
||||
if (filtered.length === 0) {
|
||||
html = '<div style="text-align:center;padding:40px 16px;color:#999;font-size:14px;">暂无预警记录</div>';
|
||||
} else {
|
||||
filtered.forEach(function(item) {
|
||||
html += renderAlertCard(item);
|
||||
});
|
||||
}
|
||||
document.getElementById('alert-list').innerHTML = html;
|
||||
}
|
||||
|
||||
/* ===== 月份筛选 ===== */
|
||||
function toggleMonthDropdown() {
|
||||
var dd = document.getElementById('monthDropdown');
|
||||
var arrow = document.getElementById('monthArrow');
|
||||
var isOpen = dd.classList.contains('active');
|
||||
dd.classList.toggle('active');
|
||||
if (isOpen) { arrow.classList.remove('open'); } else { arrow.classList.add('open'); }
|
||||
}
|
||||
|
||||
function selectMonth(e, label, value) {
|
||||
e.stopPropagation();
|
||||
currentMonth = value;
|
||||
document.getElementById('monthText').textContent = value ? label : '月份筛选';
|
||||
document.querySelectorAll('#monthDropdown .type-dropdown__item').forEach(function(item) {
|
||||
item.classList.toggle('selected', item.textContent === label);
|
||||
});
|
||||
document.getElementById('monthDropdown').classList.remove('active');
|
||||
document.getElementById('monthArrow').classList.remove('open');
|
||||
renderList();
|
||||
}
|
||||
|
||||
/* ===== 预警类型筛选 ===== */
|
||||
function toggleTypeDropdown() {
|
||||
var dd = document.getElementById('typeDropdown');
|
||||
var arrow = document.getElementById('typeArrow');
|
||||
var isOpen = dd.classList.contains('active');
|
||||
dd.classList.toggle('active');
|
||||
if (isOpen) { arrow.classList.remove('open'); } else { arrow.classList.add('open'); }
|
||||
}
|
||||
|
||||
function selectType(e, label, value) {
|
||||
e.stopPropagation();
|
||||
currentType = value;
|
||||
document.getElementById('typeText').textContent = value === 'all' ? '预警类型' : label;
|
||||
document.querySelectorAll('#typeDropdown .type-dropdown__item').forEach(function(item) {
|
||||
item.classList.toggle('selected', item.textContent === label);
|
||||
});
|
||||
document.getElementById('typeDropdown').classList.remove('active');
|
||||
document.getElementById('typeArrow').classList.remove('open');
|
||||
renderList();
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.filter-bar__select')) {
|
||||
document.getElementById('monthDropdown').classList.remove('active');
|
||||
document.getElementById('monthArrow').classList.remove('open');
|
||||
document.getElementById('typeDropdown').classList.remove('active');
|
||||
document.getElementById('typeArrow').classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== 初始化 ===== */
|
||||
renderList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,667 @@
|
||||
<!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; 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; }
|
||||
|
||||
/* 主Tab */
|
||||
.main-tabs { display: flex; background: #fff; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
|
||||
.main-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; }
|
||||
.main-tab.active { color: #1677ff; border-bottom-color: #1677ff; font-weight: 600; }
|
||||
|
||||
/* 子Tab */
|
||||
.sub-tabs { display: flex; gap: 8px; padding: 10px 12px; background: #fff; flex-shrink: 0; flex-wrap: wrap; }
|
||||
.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; }
|
||||
.sub-tab .badge { display: inline-block; background: #ff4d4f; color: #fff; 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; }
|
||||
|
||||
/* 标签 */
|
||||
.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; }
|
||||
|
||||
/* 计划信息区 */
|
||||
.plan-info { background: #fff; padding: 12px 16px; margin: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.plan-title-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.plan-title-row__title { font-size: 14px; font-weight: 700; color: #1a1a1a; }
|
||||
.plan-title-row__date { font-size: 12px; color: #666; }
|
||||
.plan-select { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f7f8fa; border-radius: 8px; cursor: pointer; position: relative; border: 1px solid #e8e8e8; }
|
||||
.plan-select__text { flex: 1; font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.plan-select__arrow { font-size: 12px; color: #bbb; transition: transform 0.2s; }
|
||||
.plan-select__arrow.open { transform: rotate(180deg); }
|
||||
.plan-meta { display: flex; gap: 0; margin-top: 10px; }
|
||||
.plan-meta__item { flex: 1; text-align: center; padding: 0 6px; }
|
||||
.plan-meta__item + .plan-meta__item { border-left: 1px solid #f0f0f0; }
|
||||
.plan-meta__val { font-size: 15px; font-weight: 700; color: #1a1a1a; }
|
||||
.plan-meta__label { font-size: 10px; color: #999; margin-top: 2px; }
|
||||
|
||||
/* 计划下拉面板 */
|
||||
.plan-dropdown { display: none; position: absolute; top: 100%; left: 0; right: 0; background: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); z-index: 50; margin-top: 4px; max-height: 200px; overflow-y: auto; }
|
||||
.plan-dropdown.active { display: block; }
|
||||
.plan-dropdown__item { padding: 10px 12px; font-size: 13px; color: #333; cursor: pointer; border-bottom: 1px solid #f5f5f5; }
|
||||
.plan-dropdown__item:hover { background: #f7f8fa; }
|
||||
.plan-dropdown__item.selected { background: #e6f4ff; color: #1677ff; font-weight: 600; }
|
||||
|
||||
/* 日期导航 */
|
||||
.date-nav { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; background: #fff; border-bottom: 1px solid #f0f0f0; position: relative; }
|
||||
.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; }
|
||||
.date-nav__cal:active { background: #e6f4ff; }
|
||||
|
||||
/* 日历面板 */
|
||||
.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__nav:active { background: #e6f4ff; }
|
||||
.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; }
|
||||
.cal-panel__day--selected:hover { background: #1677ff; }
|
||||
|
||||
/* 每日记录卡片 */
|
||||
.day-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.day-card__header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
||||
.day-card__icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
|
||||
.day-card__icon--blue { background: #e6f4ff; }
|
||||
.day-card__icon--green { background: #f6ffed; }
|
||||
.day-card__icon--orange { background: #fff7e6; }
|
||||
.day-card__title { font-size: 15px; font-weight: 600; color: #1a1a1a; flex: 1; }
|
||||
.day-card__link { font-size: 12px; color: #1677ff; cursor: pointer; flex-shrink: 0; white-space: nowrap; background: none; border: none; }
|
||||
.day-card__body { display: flex; gap: 0; background: #f9fafb; border-radius: 8px; padding: 10px 0; margin-bottom: 10px; }
|
||||
.day-card__stat { flex: 1; text-align: center; }
|
||||
.day-card__stat-num { font-size: 20px; font-weight: 700; }
|
||||
.day-card__stat-num--green { color: #52c41a; }
|
||||
.day-card__stat-num--red { color: #ff4d4f; }
|
||||
.day-card__stat-label { font-size: 11px; color: #999; margin-top: 2px; }
|
||||
.day-card__stat-divider { width: 1px; background: #e8e8e8; align-self: stretch; }
|
||||
|
||||
/* 排名Tab(参考 employee weight-ranking) */
|
||||
.rank-tabs { display: flex; border-bottom: 1px solid #f0f0f0; background: #fff; }
|
||||
.rank-tab { flex: 1; text-align: center; padding: 12px 0 10px; font-size: 13px; font-weight: 600; color: #bbb; cursor: pointer; position: relative; white-space: nowrap; }
|
||||
.rank-tab.active { color: #1677ff; }
|
||||
.rank-tab.active::after { content: ''; position: absolute; bottom: -1px; left: 50%; transform: translateX(-50%); width: 36px; height: 3px; background: #1677ff; border-radius: 2px; }
|
||||
|
||||
.rank-list-header { display: flex; align-items: center; padding: 8px 12px; border-bottom: 1px solid #f0f0f0; background: #fff; }
|
||||
.rank-list-header span { font-size: 11px; color: #999; }
|
||||
.rank-list-header__rank { width: 36px; text-align: center; flex-shrink: 0; }
|
||||
.rank-list-header__user { flex: 1; padding-left: 4px; }
|
||||
.rank-list-header__score { width: 56px; text-align: right; flex-shrink: 0; }
|
||||
|
||||
.rank-list { background: #fff; padding: 4px 12px 12px; }
|
||||
.rank-item { display: flex; align-items: center; padding: 10px 4px; border-bottom: 1px solid #f8f8f8; gap: 4px; }
|
||||
.rank-item:last-child { border-bottom: none; }
|
||||
.rank-item__num { width: 36px; height: 30px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.rank-item__num-text { font-size: 14px; font-weight: 700; color: #666; }
|
||||
.rank-item__num--gold { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(135deg, #ffd700, #ffb300); margin: 0 auto; }
|
||||
.rank-item__num--silver { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(135deg, #b0bec5, #90a4ae); margin: 0 auto; }
|
||||
.rank-item__num--bronze { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(135deg, #cd7f32, #a0522d); margin: 0 auto; }
|
||||
.rank-item__user { flex: 1; min-width: 0; padding-left: 2px; }
|
||||
.rank-item__name { font-size: 14px; font-weight: 600; color: #1a1a1a; }
|
||||
.rank-item__dept { font-size: 11px; color: #999; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.rank-item__score { width: 56px; text-align: right; flex-shrink: 0; }
|
||||
.rank-item__score-val { font-size: 14px; font-weight: 700; color: #1a1a1a; }
|
||||
.rank-item__score-unit { font-size: 10px; color: #999; margin-left: 1px; }
|
||||
|
||||
/* 目标审核卡片 */
|
||||
.review-card { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.review-card__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.review-card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
|
||||
.review-card__time { font-size: 11px; color: #bbb; }
|
||||
.review-card__basic { display: flex; gap: 12px; font-size: 12px; color: #666; margin-bottom: 10px; }
|
||||
.review-card__row { display: flex; gap: 0; margin-bottom: 8px; }
|
||||
.review-card__col { flex: 1; }
|
||||
.review-card__col-title { font-size: 11px; color: #999; margin-bottom: 4px; }
|
||||
.review-card__col-vals { display: flex; gap: 8px; }
|
||||
.review-card__col-val { font-size: 13px; font-weight: 600; color: #333; }
|
||||
.review-card__actions { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.review-card__btn { flex: 1; padding: 8px 0; border-radius: 6px; font-size: 13px; font-weight: 600; text-align: center; cursor: pointer; border: 1px solid #d9d9d9; background: #fff; color: #666; }
|
||||
.review-card__btn:active { opacity: 0.8; }
|
||||
.review-card__btn--primary { background: #1677ff; color: #fff; border-color: #1677ff; }
|
||||
.review-card__btn--danger { background: #fff; color: #ff4d4f; border-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="plan-info">
|
||||
<!-- 标题行:当前活动 + 起止日期 -->
|
||||
<div class="plan-title-row">
|
||||
<span class="plan-title-row__title">当前活动</span>
|
||||
<span class="plan-title-row__date" id="planDateRange">2026-04-01 至 2026-06-29</span>
|
||||
</div>
|
||||
<!-- [交互] 点击展开计划选择下拉 -->
|
||||
<div class="plan-select" onclick="togglePlanDropdown()" style="position:relative;">
|
||||
<span class="plan-select__text" id="planSelectText">2026年二季度体重管理活动</span>
|
||||
<span class="plan-select__arrow" id="planSelectArrow">▾</span>
|
||||
<div class="plan-dropdown" id="planDropdown">
|
||||
<div class="plan-dropdown__item selected" onclick="selectPlan(event, '2026年二季度体重管理活动', '2026-04-01', '90', '120', '86')">2026年二季度体重管理活动</div>
|
||||
<div class="plan-dropdown__item" onclick="selectPlan(event, '2026年一季度体重管理活动', '2026-01-01', '90', '100', '100')">2026年一季度体重管理活动</div>
|
||||
<div class="plan-dropdown__item" onclick="selectPlan(event, '2025年四季度减重挑战', '2025-10-01', '92', '80', '80')">2025年四季度减重挑战</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-meta">
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planDays">90/90</div>
|
||||
<div class="plan-meta__label">活动进行天数</div>
|
||||
</div>
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planExpected">120</div>
|
||||
<div class="plan-meta__label">应参加人数</div>
|
||||
</div>
|
||||
<div class="plan-meta__item">
|
||||
<div class="plan-meta__val" id="planActual">86</div>
|
||||
<div class="plan-meta__label">已参加人数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主Tab:每日记录 / 减重排名 / 目标审核 -->
|
||||
<div class="main-tabs">
|
||||
<div class="main-tab active" onclick="switchMainTab(this,'panel-record')">每日记录</div>
|
||||
<div class="main-tab" onclick="switchMainTab(this,'panel-ranking')">减重排名</div>
|
||||
<div class="main-tab" onclick="switchMainTab(this,'panel-review')">目标审核</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
|
||||
<!-- ==================== 每日记录面板 ==================== -->
|
||||
<div id="panel-record">
|
||||
<!-- 日期导航 -->
|
||||
<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="day-card">
|
||||
<div class="day-card__header">
|
||||
<div class="day-card__icon day-card__icon--blue">🍽️</div>
|
||||
<div class="day-card__title">当日饮食</div>
|
||||
<span class="day-card__link" onclick="location.href='./eat-detail.html'">查看详情 ›</span>
|
||||
</div>
|
||||
<div class="day-card__body">
|
||||
<div class="day-card__stat">
|
||||
<div class="day-card__stat-num day-card__stat-num--green">78</div>
|
||||
<div class="day-card__stat-label">已打卡人数</div>
|
||||
</div>
|
||||
<div class="day-card__stat-divider"></div>
|
||||
<div class="day-card__stat">
|
||||
<div class="day-card__stat-num day-card__stat-num--red">8</div>
|
||||
<div class="day-card__stat-label">未打卡人数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当日运动卡片 -->
|
||||
<!-- [交互] 点击查看跳转至运动记录详情 -->
|
||||
<div class="day-card">
|
||||
<div class="day-card__header">
|
||||
<div class="day-card__icon day-card__icon--green">🏃</div>
|
||||
<div class="day-card__title">当日运动</div>
|
||||
<span class="day-card__link" onclick="location.href='./exercise-detail.html'">查看详情 ›</span>
|
||||
</div>
|
||||
<div class="day-card__body">
|
||||
<div class="day-card__stat">
|
||||
<div class="day-card__stat-num day-card__stat-num--green">65</div>
|
||||
<div class="day-card__stat-label">已打卡人数</div>
|
||||
</div>
|
||||
<div class="day-card__stat-divider"></div>
|
||||
<div class="day-card__stat">
|
||||
<div class="day-card__stat-num day-card__stat-num--red">21</div>
|
||||
<div class="day-card__stat-label">未打卡人数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当日体重卡片 -->
|
||||
<!-- [交互] 点击查看跳转至体重记录详情 -->
|
||||
<div class="day-card">
|
||||
<div class="day-card__header">
|
||||
<div class="day-card__icon day-card__icon--orange">⚖️</div>
|
||||
<div class="day-card__title">当日体重</div>
|
||||
<span class="day-card__link" onclick="location.href='./weight-record.html'">查看详情 ›</span>
|
||||
</div>
|
||||
<div class="day-card__body">
|
||||
<div class="day-card__stat">
|
||||
<div class="day-card__stat-num day-card__stat-num--green">72</div>
|
||||
<div class="day-card__stat-label">已打卡人数</div>
|
||||
</div>
|
||||
<div class="day-card__stat-divider"></div>
|
||||
<div class="day-card__stat">
|
||||
<div class="day-card__stat-num day-card__stat-num--red">14</div>
|
||||
<div class="day-card__stat-label">未打卡人数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 减重排名面板 ==================== -->
|
||||
<div id="panel-ranking" style="display:none;">
|
||||
<!-- 排名子Tab -->
|
||||
<div class="rank-tabs" id="rankTabs">
|
||||
<div class="rank-tab active" data-tab="weight" onclick="switchRankTab('weight')">减重量</div>
|
||||
<div class="rank-tab" data-tab="rate" onclick="switchRankTab('rate')">减重率</div>
|
||||
<div class="rank-tab" data-tab="score" onclick="switchRankTab('score')">综合分</div>
|
||||
<div class="rank-tab" data-tab="action" onclick="switchRankTab('action')">行动分</div>
|
||||
</div>
|
||||
|
||||
<!-- 排行榜列表头 -->
|
||||
<div class="rank-list-header">
|
||||
<span class="rank-list-header__rank">排名</span>
|
||||
<span class="rank-list-header__user">员工</span>
|
||||
<span class="rank-list-header__score" id="rankListHeader">减重量</span>
|
||||
</div>
|
||||
|
||||
<!-- 排行榜列表 -->
|
||||
<div class="rank-list" id="rankList">
|
||||
<!-- JS动态渲染 -->
|
||||
</div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 目标审核面板 ==================== -->
|
||||
<div id="panel-review" style="display:none;">
|
||||
<!-- 审核子Tab -->
|
||||
<div class="sub-tabs" id="reviewTabs">
|
||||
<div class="sub-tab active" onclick="switchReviewTab(this,'rv-pending')">待审核<span class="badge">2</span></div>
|
||||
<div class="sub-tab" onclick="switchReviewTab(this,'rv-approved')">已通过</div>
|
||||
<div class="sub-tab" onclick="switchReviewTab(this,'rv-rejected')">已驳回</div>
|
||||
</div>
|
||||
|
||||
<!-- 待审核列表 -->
|
||||
<div id="rv-pending">
|
||||
<!-- 审核卡片1 -->
|
||||
<div class="review-card">
|
||||
<div class="review-card__head">
|
||||
<span class="review-card__name">张小红</span>
|
||||
<span class="review-card__time">申请时间:2026-06-20 14:30</span>
|
||||
</div>
|
||||
<div class="review-card__basic">
|
||||
<span>女</span><span>32岁</span><span>身高 162cm</span>
|
||||
</div>
|
||||
<div class="review-card__row">
|
||||
<div class="review-card__col">
|
||||
<div class="review-card__col-title">初始</div>
|
||||
<div class="review-card__col-vals">
|
||||
<span class="review-card__col-val">体重 72.5kg</span>
|
||||
<span class="review-card__col-val">BMI 27.6</span>
|
||||
<span class="review-card__col-val">体脂 32.1%</span>
|
||||
<span class="review-card__col-val">腰围 86cm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-card__row">
|
||||
<div class="review-card__col">
|
||||
<div class="review-card__col-title">目标</div>
|
||||
<div class="review-card__col-vals">
|
||||
<span class="review-card__col-val" style="color:#52c41a;">体重 62kg</span>
|
||||
<span class="review-card__col-val" style="color:#52c41a;">BMI 23.6</span>
|
||||
<span class="review-card__col-val" style="color:#52c41a;">体脂 26%</span>
|
||||
<span class="review-card__col-val" style="color:#52c41a;">腰围 76cm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [交互] 点击审核跳转至审核页面 -->
|
||||
<div class="review-card__actions">
|
||||
<button class="review-card__btn review-card__btn--primary" onclick="location.href='./goal-review.html'">审核</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审核卡片2 -->
|
||||
<div class="review-card">
|
||||
<div class="review-card__head">
|
||||
<span class="review-card__name">李建国</span>
|
||||
<span class="review-card__time">申请时间:2026-06-19 10:15</span>
|
||||
</div>
|
||||
<div class="review-card__basic">
|
||||
<span>男</span><span>45岁</span><span>身高 175cm</span>
|
||||
</div>
|
||||
<div class="review-card__row">
|
||||
<div class="review-card__col">
|
||||
<div class="review-card__col-title">初始</div>
|
||||
<div class="review-card__col-vals">
|
||||
<span class="review-card__col-val">体重 88.2kg</span>
|
||||
<span class="review-card__col-val">BMI 28.8</span>
|
||||
<span class="review-card__col-val">体脂 28.5%</span>
|
||||
<span class="review-card__col-val">腰围 98cm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-card__row">
|
||||
<div class="review-card__col">
|
||||
<div class="review-card__col-title">目标</div>
|
||||
<div class="review-card__col-vals">
|
||||
<span class="review-card__col-val" style="color:#52c41a;">体重 78kg</span>
|
||||
<span class="review-card__col-val" style="color:#52c41a;">BMI 25.5</span>
|
||||
<span class="review-card__col-val" style="color:#52c41a;">体脂 22%</span>
|
||||
<span class="review-card__col-val" style="color:#52c41a;">腰围 88cm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [交互] 点击审核跳转至审核页面 -->
|
||||
<div class="review-card__actions">
|
||||
<button class="review-card__btn review-card__btn--primary" onclick="location.href='./goal-review.html'">审核</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 已通过列表 -->
|
||||
<div id="rv-approved" style="display:none;">
|
||||
<!-- [交互] 点击卡片跳转至审核详情页面 -->
|
||||
<div class="review-card" onclick="location.href='./goal-review.html?status=approved'" style="cursor:pointer;">
|
||||
<div class="review-card__head">
|
||||
<span class="review-card__name">王芳</span>
|
||||
<span class="review-card__time">申请时间:2026-06-15 09:00</span>
|
||||
</div>
|
||||
<div class="review-card__basic"><span>女</span><span>28岁</span><span>身高 160cm</span></div>
|
||||
<div class="review-card__row"><div class="review-card__col"><div class="review-card__col-title">初始</div><div class="review-card__col-vals"><span class="review-card__col-val">体重 65kg</span><span class="review-card__col-val">BMI 25.4</span><span class="review-card__col-val">体脂 30%</span><span class="review-card__col-val">腰围 78cm</span></div></div></div>
|
||||
<div class="review-card__row"><div class="review-card__col"><div class="review-card__col-title">目标</div><div class="review-card__col-vals"><span class="review-card__col-val" style="color:#52c41a;">体重 56kg</span><span class="review-card__col-val" style="color:#52c41a;">BMI 21.9</span><span class="review-card__col-val" style="color:#52c41a;">体脂 24%</span><span class="review-card__col-val" style="color:#52c41a;">腰围 70cm</span></div></div></div>
|
||||
<div style="text-align:right;margin-top:8px;"><span class="tag tag-green">已通过</span><span style="font-size:11px;color:#999;margin-left:6px;">审核人:刘主管</span><span style="font-size:11px;color:#999;margin-left:6px;">审核时间:2026-06-16</span></div>
|
||||
</div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 已驳回列表 -->
|
||||
<div id="rv-rejected" style="display:none;">
|
||||
<!-- [交互] 点击卡片跳转至审核详情页面 -->
|
||||
<div class="review-card" onclick="location.href='./goal-review.html?status=rejected'" style="cursor:pointer;">
|
||||
<div class="review-card__head">
|
||||
<span class="review-card__name">刘海波</span>
|
||||
<span class="review-card__time">申请时间:2026-06-10 11:00</span>
|
||||
</div>
|
||||
<div class="review-card__basic"><span>男</span><span>52岁</span><span>身高 170cm</span></div>
|
||||
<div class="review-card__row"><div class="review-card__col"><div class="review-card__col-title">初始</div><div class="review-card__col-vals"><span class="review-card__col-val">体重 85kg</span><span class="review-card__col-val">BMI 29.4</span><span class="review-card__col-val">体脂 33%</span><span class="review-card__col-val">腰围 100cm</span></div></div></div>
|
||||
<div class="review-card__row"><div class="review-card__col"><div class="review-card__col-title">目标</div><div class="review-card__col-vals"><span class="review-card__col-val" style="color:#52c41a;">体重 70kg</span><span class="review-card__col-val" style="color:#52c41a;">BMI 24.2</span><span class="review-card__col-val" style="color:#52c41a;">体脂 24%</span><span class="review-card__col-val" style="color:#52c41a;">腰围 88cm</span></div></div></div>
|
||||
<div style="text-align:right;margin-top:8px;"><span class="tag tag-red">已驳回</span><span style="font-size:11px;color:#999;margin-left:6px;">审核人:刘主管</span><span style="font-size:11px;color:#999;margin-left:6px;">审核时间:2026-06-17</span></div>
|
||||
<div style="text-align:right;margin-top:4px;"><span style="font-size:11px;color:#999;">驳回原因:目标设定偏低,建议调整</span></div>
|
||||
</div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ===== 计划选择 ===== */
|
||||
function togglePlanDropdown() {
|
||||
var dd = document.getElementById('planDropdown');
|
||||
var arrow = document.getElementById('planSelectArrow');
|
||||
var isOpen = dd.classList.contains('active');
|
||||
dd.classList.toggle('active');
|
||||
if (isOpen) { arrow.classList.remove('open'); } else { arrow.classList.add('open'); }
|
||||
}
|
||||
|
||||
function calcEndDate(startStr, totalDays) {
|
||||
var d = new Date(startStr);
|
||||
d.setDate(d.getDate() + totalDays - 1);
|
||||
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||||
}
|
||||
|
||||
function selectPlan(e, name, startDate, days, expected, actual) {
|
||||
e.stopPropagation();
|
||||
document.getElementById('planSelectText').textContent = name;
|
||||
// 计算结束日期并更新标题行
|
||||
var endDate = calcEndDate(startDate, parseInt(days));
|
||||
document.getElementById('planDateRange').textContent = startDate + ' 至 ' + endDate;
|
||||
document.getElementById('planDays').textContent = days + '/' + days;
|
||||
document.getElementById('planExpected').textContent = expected;
|
||||
document.getElementById('planActual').textContent = actual;
|
||||
// 更新下拉选中态
|
||||
document.querySelectorAll('.plan-dropdown__item').forEach(function(item) {
|
||||
item.classList.toggle('selected', item.textContent === name);
|
||||
});
|
||||
document.getElementById('planDropdown').classList.remove('active');
|
||||
document.getElementById('planSelectArrow').classList.remove('open');
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.plan-select')) {
|
||||
document.getElementById('planDropdown').classList.remove('active');
|
||||
document.getElementById('planSelectArrow').classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== 主Tab切换 ===== */
|
||||
function switchMainTab(el, panelId) {
|
||||
el.closest('.main-tabs').querySelectorAll('.main-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
['panel-record','panel-ranking','panel-review'].forEach(function(id) {
|
||||
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== 日期导航 ===== */
|
||||
var currentDate = new Date(2026, 5, 22); // 6月=5
|
||||
var calViewYear = currentDate.getFullYear();
|
||||
var calViewMonth = currentDate.getMonth();
|
||||
|
||||
function changeDate(delta) {
|
||||
currentDate.setDate(currentDate.getDate() + delta);
|
||||
calViewYear = currentDate.getFullYear();
|
||||
calViewMonth = currentDate.getMonth();
|
||||
renderDate();
|
||||
}
|
||||
|
||||
function renderDate() {
|
||||
var now = new Date();
|
||||
var y = currentDate.getFullYear();
|
||||
var m = String(currentDate.getMonth() + 1).padStart(2, '0');
|
||||
var d = String(currentDate.getDate()).padStart(2, '0');
|
||||
var dateStr = y + '-' + m + '-' + d;
|
||||
var isToday = currentDate.toDateString() === now.toDateString();
|
||||
document.getElementById('dateText').textContent = dateStr;
|
||||
}
|
||||
|
||||
/* ===== 日历面板 ===== */
|
||||
function toggleCalPanel() {
|
||||
var panel = document.getElementById('calPanel');
|
||||
var isOpen = panel.classList.contains('active');
|
||||
if (isOpen) {
|
||||
panel.classList.remove('active');
|
||||
} else {
|
||||
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();
|
||||
var grid = document.getElementById('calGrid');
|
||||
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';
|
||||
var isToday = (calViewYear === today.getFullYear() && calViewMonth === today.getMonth() && d === today.getDate());
|
||||
var isSel = (calViewYear === currentDate.getFullYear() && calViewMonth === currentDate.getMonth() && d === currentDate.getDate());
|
||||
if (isToday) cls += ' cal-panel__day--today';
|
||||
if (isSel) cls += ' cal-panel__day--selected';
|
||||
html += '<div class="' + cls + '" onclick="selectCalDay(event,' + d + ')">' + d + '</div>';
|
||||
}
|
||||
grid.innerHTML = html;
|
||||
}
|
||||
|
||||
function selectCalDay(e, day) {
|
||||
e.stopPropagation();
|
||||
currentDate = new Date(calViewYear, calViewMonth, day);
|
||||
calViewYear = currentDate.getFullYear();
|
||||
calViewMonth = currentDate.getMonth();
|
||||
renderDate();
|
||||
renderCal();
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== 排名Tab切换 ===== */
|
||||
var rankHeaderLabels = { weight: '减重量', rate: '减重率', score: '综合分', action: '行动积分' };
|
||||
|
||||
var rankDataMap = {
|
||||
weight: [
|
||||
{ rank:1, name:'张**', dept:'公司机关 · 办公室', val:'15.2', unit:'kg' },
|
||||
{ rank:2, name:'刘**', dept:'信息技术中心 · 技术部', val:'14.8', unit:'kg' },
|
||||
{ rank:3, name:'王**', dept:'公司机关 · 人力部', val:'13.5', unit:'kg' },
|
||||
{ rank:4, name:'赵**', dept:'安全环保部 · 安环部', val:'12.9', unit:'kg' },
|
||||
{ rank:5, name:'陈**', dept:'公司机关 · 财务部', val:'12.1', unit:'kg' },
|
||||
{ rank:6, name:'孙**', dept:'信息技术中心 · 技术部', val:'11.6', unit:'kg' },
|
||||
{ rank:7, name:'周**', dept:'公司机关 · 办公室', val:'11.2', unit:'kg' },
|
||||
{ rank:8, name:'李**', dept:'公司机关 · 办公室', val:'10.9', unit:'kg' },
|
||||
{ rank:9, name:'吴**', dept:'信息技术中心 · 技术部', val:'10.3', unit:'kg' },
|
||||
{ rank:10, name:'郑**', dept:'公司机关 · 人力部', val:'9.8', unit:'kg' }
|
||||
],
|
||||
rate: [
|
||||
{ rank:1, name:'刘**', dept:'信息技术中心 · 技术部', val:'18.6', unit:'%' },
|
||||
{ rank:2, name:'张**', dept:'公司机关 · 办公室', val:'17.9', unit:'%' },
|
||||
{ rank:3, name:'赵**', dept:'安全环保部 · 安环部', val:'17.2', unit:'%' },
|
||||
{ rank:4, name:'王**', dept:'公司机关 · 人力部', val:'16.5', unit:'%' },
|
||||
{ rank:5, name:'陈**', dept:'公司机关 · 财务部', val:'16.0', unit:'%' },
|
||||
{ rank:6, name:'孙**', dept:'信息技术中心 · 技术部', val:'15.3', unit:'%' },
|
||||
{ rank:7, name:'周**', dept:'公司机关 · 办公室', val:'15.1', unit:'%' },
|
||||
{ rank:8, name:'吴**', dept:'信息技术中心 · 技术部', val:'14.8', unit:'%' },
|
||||
{ rank:9, name:'黄**', dept:'安全环保部 · 安环部', val:'14.2', unit:'%' },
|
||||
{ rank:10, name:'郑**', dept:'公司机关 · 人力部', val:'13.6', unit:'%' }
|
||||
],
|
||||
score: [
|
||||
{ rank:1, name:'张**', dept:'公司机关 · 办公室', val:'96.6', unit:'分' },
|
||||
{ rank:2, name:'刘**', dept:'信息技术中心 · 技术部', val:'95.3', unit:'分' },
|
||||
{ rank:3, name:'王**', dept:'公司机关 · 人力部', val:'92.9', unit:'分' },
|
||||
{ rank:4, name:'赵**', dept:'安全环保部 · 安环部', val:'90.2', unit:'分' },
|
||||
{ rank:5, name:'陈**', dept:'公司机关 · 财务部', val:'88.5', unit:'分' },
|
||||
{ rank:6, name:'孙**', dept:'信息技术中心 · 技术部', val:'87.3', unit:'分' },
|
||||
{ rank:7, name:'周**', dept:'公司机关 · 办公室', val:'87.0', unit:'分' },
|
||||
{ rank:8, name:'李**', dept:'公司机关 · 办公室', val:'86.6', unit:'分' },
|
||||
{ rank:9, name:'吴**', dept:'信息技术中心 · 技术部', val:'84.1', unit:'分' },
|
||||
{ rank:10, name:'郑**', dept:'公司机关 · 人力部', val:'82.8', unit:'分' }
|
||||
],
|
||||
action: [
|
||||
{ rank:1, name:'陈**', dept:'公司机关 · 财务部', val:'1580', unit:'分' },
|
||||
{ rank:2, name:'周**', dept:'公司机关 · 办公室', val:'1520', unit:'分' },
|
||||
{ rank:3, name:'吴**', dept:'信息技术中心 · 技术部', val:'1460', unit:'分' },
|
||||
{ rank:4, name:'孙**', dept:'信息技术中心 · 技术部', val:'1380', unit:'分' },
|
||||
{ rank:5, name:'李**', dept:'公司机关 · 办公室', val:'1250', unit:'分' },
|
||||
{ rank:6, name:'张**', dept:'公司机关 · 办公室', val:'1180', unit:'分' },
|
||||
{ rank:7, name:'黄**', dept:'安全环保部 · 安环部', val:'1100', unit:'分' },
|
||||
{ rank:8, name:'刘**', dept:'信息技术中心 · 技术部', val:'1060', unit:'分' },
|
||||
{ rank:9, name:'杨**', dept:'公司机关 · 办公室', val:'1020', unit:'分' },
|
||||
{ rank:10, name:'王**', dept:'公司机关 · 人力部', val:'980', unit:'分' }
|
||||
]
|
||||
};
|
||||
|
||||
function switchRankTab(tab) {
|
||||
document.querySelectorAll('#rankTabs .rank-tab').forEach(function(t) { t.classList.toggle('active', t.dataset.tab === tab); });
|
||||
document.getElementById('rankListHeader').textContent = rankHeaderLabels[tab];
|
||||
renderRankList(rankDataMap[tab]);
|
||||
}
|
||||
|
||||
function renderRankList(data) {
|
||||
var html = '';
|
||||
data.forEach(function(item) {
|
||||
var numHtml = '';
|
||||
if (item.rank === 1) {
|
||||
numHtml = '<div class="rank-item__num rank-item__num--gold"></div>';
|
||||
} else if (item.rank === 2) {
|
||||
numHtml = '<div class="rank-item__num rank-item__num--silver"></div>';
|
||||
} else if (item.rank === 3) {
|
||||
numHtml = '<div class="rank-item__num rank-item__num--bronze"></div>';
|
||||
} else {
|
||||
numHtml = '<span class="rank-item__num-text">' + item.rank + '</span>';
|
||||
}
|
||||
html += '<div class="rank-item">';
|
||||
html += ' <div class="rank-item__num">' + numHtml + '</div>';
|
||||
html += ' <div class="rank-item__user"><div class="rank-item__name">' + item.name + '</div><div class="rank-item__dept">' + item.dept + '</div></div>';
|
||||
html += ' <div class="rank-item__score"><div class="rank-item__score-val">' + item.val + '<span class="rank-item__score-unit">' + item.unit + '</span></div></div>';
|
||||
html += '</div>';
|
||||
});
|
||||
document.getElementById('rankList').innerHTML = html;
|
||||
}
|
||||
|
||||
/* ===== 审核Tab切换 ===== */
|
||||
function switchReviewTab(el, panelId) {
|
||||
el.closest('.sub-tabs').querySelectorAll('.sub-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
['rv-pending','rv-approved','rv-rejected'].forEach(function(id) {
|
||||
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/* 初始化 */
|
||||
switchRankTab('weight');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,207 @@
|
||||
<!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: #fff; padding: 14px 16px; margin: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.emp-info__name { font-size: 17px; font-weight: 700; color: #1a1a1a; }
|
||||
.emp-info__meta { font-size: 12px; color: #999; margin-top: 4px; display: flex; gap: 8px; }
|
||||
|
||||
.data-compare { background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 14px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.data-compare__title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin-bottom: 10px; }
|
||||
.data-compare__table { display: flex; gap: 0; }
|
||||
.data-compare__col { flex: 1; text-align: center; }
|
||||
.data-compare__col-label { font-size: 11px; color: #999; margin-bottom: 6px; font-weight: 600; }
|
||||
.data-compare__row { font-size: 12px; color: #666; padding: 4px 0; border-bottom: 1px solid #f8f8f8; }
|
||||
.data-compare__row:last-child { border-bottom: none; }
|
||||
.data-compare__row .val { font-weight: 600; }
|
||||
.data-compare__col--initial .val { color: #666; }
|
||||
.data-compare__col--target .val { color: #52c41a; }
|
||||
.data-compare__col--current .val { color: #1677ff; }
|
||||
|
||||
.change-row { display: flex; gap: 0; background: #fff; border-radius: 12px; margin: 10px 12px 0; padding: 10px 0; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.change-item { flex: 1; text-align: center; }
|
||||
.change-item + .change-item { border-left: 1px solid #f0f0f0; }
|
||||
.change-item__val { font-size: 16px; font-weight: 700; }
|
||||
.change-item__val--down { color: #52c41a; }
|
||||
.change-item__val--up { color: #ff4d4f; }
|
||||
.change-item__label { font-size: 10px; color: #999; margin-top: 2px; }
|
||||
|
||||
.section-title { font-size: 14px; font-weight: 700; color: #1a1a1a; margin: 14px 12px 6px; }
|
||||
|
||||
/* 记录列表 */
|
||||
.record-list { padding: 8px 12px 16px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.record-item { background: #fff; border-radius: 14px; padding: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.03); }
|
||||
.record-item__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.record-item__time { font-size: 11px; color: #bbb; }
|
||||
.record-item__body { display: flex; align-items: center; gap: 16px; }
|
||||
.record-item__metric { text-align: center; min-width: 48px; }
|
||||
.record-item__metric-label { font-size: 10px; color: #999; margin-bottom: 2px; }
|
||||
.record-item__metric-val { font-size: 16px; font-weight: 700; color: #1a1a1a; }
|
||||
.record-item__metric-unit { font-size: 10px; color: #999; font-weight: 400; margin-left: 1px; }
|
||||
.record-item__source { margin-left: auto; }
|
||||
.source-badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }
|
||||
.source-badge--scale { background: #e8effa; color: #5470c6; }
|
||||
.source-badge--bt { background: #f3efff; color: #8b5cf6; }
|
||||
.source-badge--self { background: #f5f0ff; color: #a78bfa; }
|
||||
.source-badge--fat { background: #e6edf5; color: #1e3a5f; }
|
||||
.source-badge--wear { background: #e6fffb; color: #13c2c2; }
|
||||
</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='./weight-record.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 style="padding:10px 16px;background:#fff;border-bottom:1px solid #f0f0f0;font-size:14px;color:#333;font-weight:600;">2026-06-22</div>
|
||||
|
||||
<div class="emp-info">
|
||||
<div class="emp-info__name">张小红</div>
|
||||
<div class="emp-info__meta">
|
||||
<span>女</span><span>32岁</span><span>公司机关 · 办公室</span><span>身高 162cm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-compare">
|
||||
<div class="data-compare__title">数据对比</div>
|
||||
<div class="data-compare__table">
|
||||
<div class="data-compare__col data-compare__col--initial">
|
||||
<div class="data-compare__col-label">初始</div>
|
||||
<div class="data-compare__row">体重 <span class="val">72.5kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">27.6</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">32.1%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">86cm</span></div>
|
||||
</div>
|
||||
<div class="data-compare__col data-compare__col--target">
|
||||
<div class="data-compare__col-label">目标</div>
|
||||
<div class="data-compare__row">体重 <span class="val">62kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">23.6</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">26%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">76cm</span></div>
|
||||
</div>
|
||||
<div class="data-compare__col data-compare__col--current">
|
||||
<div class="data-compare__col-label">当前</div>
|
||||
<div class="data-compare__row">体重 <span class="val">67.2kg</span></div>
|
||||
<div class="data-compare__row">BMI <span class="val">25.6</span></div>
|
||||
<div class="data-compare__row">体脂 <span class="val">29.5%</span></div>
|
||||
<div class="data-compare__row">腰围 <span class="val">81cm</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="change-row">
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-5.3kg</div>
|
||||
<div class="change-item__label">体重变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-2.0</div>
|
||||
<div class="change-item__label">BMI变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-2.6%</div>
|
||||
<div class="change-item__label">体脂变化</div>
|
||||
</div>
|
||||
<div class="change-item">
|
||||
<div class="change-item__val change-item__val--down">-5cm</div>
|
||||
<div class="change-item__label">腰围变化</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">体重记录列表</div>
|
||||
|
||||
<div class="record-list">
|
||||
<div class="record-item">
|
||||
<div class="record-item__header">
|
||||
<span class="record-item__time">2026-06-22 07:30</span>
|
||||
</div>
|
||||
<div class="record-item__body">
|
||||
<div class="record-item__metric"><div class="record-item__metric-label">体重</div><div class="record-item__metric-val">67.2<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.5<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">81.0<span class="record-item__metric-unit">cm</span></div></div>
|
||||
<div class="record-item__source"><span class="source-badge source-badge--bt">蓝牙秤</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-item">
|
||||
<div class="record-item__header">
|
||||
<span class="record-item__time">2026-06-21 18:30</span>
|
||||
</div>
|
||||
<div class="record-item__body">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="record-item">
|
||||
<div class="record-item__header">
|
||||
<span class="record-item__time">2026-06-20 07:15</span>
|
||||
</div>
|
||||
<div class="record-item__body">
|
||||
<div class="record-item__metric"><div class="record-item__metric-label">体重</div><div class="record-item__metric-val">67.0<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.3<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">81.0<span class="record-item__metric-unit">cm</span></div></div>
|
||||
<div class="record-item__source"><span class="source-badge source-badge--fat">体脂仪</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-item">
|
||||
<div class="record-item__header">
|
||||
<span class="record-item__time">2026-06-18 09:00</span>
|
||||
</div>
|
||||
<div class="record-item__body">
|
||||
<div class="record-item__metric"><div class="record-item__metric-label">体重</div><div class="record-item__metric-val">67.5<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">--</div></div>
|
||||
<div class="record-item__metric"><div class="record-item__metric-label">腰围</div><div class="record-item__metric-val">--</div></div>
|
||||
<div class="record-item__source"><span class="source-badge source-badge--self">自记</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-item">
|
||||
<div class="record-item__header">
|
||||
<span class="record-item__time">2026-06-15 07:20</span>
|
||||
</div>
|
||||
<div class="record-item__body">
|
||||
<div class="record-item__metric"><div class="record-item__metric-label">体重</div><div class="record-item__metric-val">68.2<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.8<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">81.5<span class="record-item__metric-unit">cm</span></div></div>
|
||||
<div class="record-item__source"><span class="source-badge source-badge--wear">穿戴</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height:20px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,216 @@
|
||||
<!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; 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; }
|
||||
|
||||
.date-nav { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; background: #fff; border-bottom: 1px solid #f0f0f0; position: relative; }
|
||||
.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; }
|
||||
.date-nav__cal:active { background: #e6f4ff; }
|
||||
|
||||
.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__nav:active { background: #e6f4ff; }
|
||||
.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; }
|
||||
.cal-panel__day--selected:hover { background: #1677ff; }
|
||||
|
||||
.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; }
|
||||
|
||||
.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__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__arrow { font-size: 12px; color: #1677ff; flex-shrink: 0; white-space: nowrap; }
|
||||
</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='./weight-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="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="seg-tabs">
|
||||
<div class="seg-tab active" onclick="switchTab(this,'panel-recorded')">已记录<span class="badge" id="badgeRecorded">72</span></div>
|
||||
<div class="seg-tab" onclick="switchTab(this,'panel-unrecorded')">未记录<span class="badge" id="badgeUnrecorded">14</span></div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
<div id="panel-recorded"><!-- JS动态渲染 --></div>
|
||||
<div id="panel-unrecorded" style="display:none;"><!-- JS动态渲染 --></div>
|
||||
<div style="height:16px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var recordedList = [
|
||||
{ name:'张小红', gender:'女', age:32, dept:'公司机关 · 办公室', height:162 },
|
||||
{ name:'李建国', gender:'男', age:45, dept:'采油一厂 · 技术部', height:175 },
|
||||
{ name:'陈伟', gender:'男', age:36, dept:'安全环保部 · 安环部', height:178 },
|
||||
{ name:'周明', gender:'男', age:33, dept:'信息技术中心 · 技术部', height:182 },
|
||||
{ name:'王芳', gender:'女', age:28, dept:'信息技术中心 · 数据部', height:160 },
|
||||
{ name:'赵志强', gender:'男', age:61, dept:'公司机关 · 后勤部', height:170 },
|
||||
{ name:'孙丽', gender:'女', age:41, dept:'采油一厂 · 作业区', height:158 },
|
||||
{ name:'刘海波', gender:'男', age:55, dept:'公司机关 · 行政部', height:168 }
|
||||
];
|
||||
|
||||
var unrecordedList = [
|
||||
{ name:'吴静', gender:'女', age:26, dept:'公司机关 · 人力部', height:165 },
|
||||
{ name:'黄晓明', gender:'男', age:42, dept:'采油一厂 · 工程部', height:176 },
|
||||
{ name:'马燕', gender:'女', age:35, dept:'信息技术中心 · 运维部', height:159 },
|
||||
{ name:'林伟东', gender:'男', age:44, dept:'采油一厂 · 设备部', height:180 },
|
||||
{ name:'郑国强', gender:'男', age:50, dept:'公司机关 · 财务部', height:172 },
|
||||
{ name:'杨丽华', gender:'女', age:39, dept:'安全环保部 · 安环部', height:163 }
|
||||
];
|
||||
|
||||
function switchTab(el, panelId) {
|
||||
el.closest('.seg-tabs').querySelectorAll('.seg-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
['panel-recorded','panel-unrecorded'].forEach(function(id) {
|
||||
document.getElementById(id).style.display = id === panelId ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function renderRecordedList() {
|
||||
var html = '';
|
||||
recordedList.forEach(function(item) {
|
||||
html += '<div class="employee-card" onclick="location.href=\'./weight-employee-detail.html\'">';
|
||||
html += ' <div class="employee-card__info">';
|
||||
html += ' <div class="employee-card__name">' + item.name + '</div>';
|
||||
html += ' <div class="employee-card__meta"><span>' + item.gender + '</span><span>' + item.age + '岁</span><span>' + item.dept + '</span><span>身高 ' + item.height + 'cm</span></div>';
|
||||
html += ' </div>';
|
||||
html += ' <span class="employee-card__arrow">记录明细 ›</span>';
|
||||
html += '</div>';
|
||||
});
|
||||
document.getElementById('panel-recorded').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderUnrecordedList() {
|
||||
var html = '';
|
||||
unrecordedList.forEach(function(item) {
|
||||
html += '<div class="employee-card">';
|
||||
html += ' <div class="employee-card__info">';
|
||||
html += ' <div class="employee-card__name">' + item.name + '</div>';
|
||||
html += ' <div class="employee-card__meta"><span>' + item.gender + '</span><span>' + item.age + '岁</span><span>' + item.dept + '</span><span>身高 ' + item.height + 'cm</span></div>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
});
|
||||
document.getElementById('panel-unrecorded').innerHTML = html;
|
||||
}
|
||||
|
||||
var currentDate = new Date(2026, 5, 22);
|
||||
var calViewYear = currentDate.getFullYear();
|
||||
var calViewMonth = currentDate.getMonth();
|
||||
|
||||
function changeDate(delta) {
|
||||
currentDate.setDate(currentDate.getDate() + delta);
|
||||
calViewYear = currentDate.getFullYear(); calViewMonth = currentDate.getMonth();
|
||||
renderDate();
|
||||
}
|
||||
function renderDate() {
|
||||
var y = currentDate.getFullYear();
|
||||
var m = String(currentDate.getMonth() + 1).padStart(2, '0');
|
||||
var d = String(currentDate.getDate()).padStart(2, '0');
|
||||
document.getElementById('dateText').textContent = y + '-' + m + '-' + d;
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
renderDate(); renderCal();
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
renderRecordedList();
|
||||
renderUnrecordedList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -128,7 +128,9 @@ function initTabLinks() {
|
||||
assistant: '../assistant',
|
||||
professional: '../professional',
|
||||
operator: '../operator',
|
||||
onsite: '../onsite'
|
||||
onsite: '../onsite',
|
||||
admin: '../admin',
|
||||
nutritionist: '../nutritionist'
|
||||
};
|
||||
var base = roleBaseMap[role] || '../expert';
|
||||
document.getElementById('link-workbench').href = base + '/workbench/index.html';
|
||||
|
||||
|
After Width: | Height: | Size: 45 KiB |
@@ -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: 新增"我的"个人中心模块
|
||||
|
||||
@@ -15,7 +15,6 @@ const pages = [
|
||||
{ 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' },
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB |
@@ -35,7 +35,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
</div>
|
||||
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
@@ -66,7 +66,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
<div class="modal-title">修改成功</div>
|
||||
<div class="modal-body">密码已更新,下次登录请使用新密码。</div>
|
||||
<div class="modal-actions">
|
||||
<!-- [交互] 确定后跳转至: ./index.html -->
|
||||
<!-- [交互] 确定后跳转至: ./index-v2.html -->
|
||||
<button onclick="closeModal('modal-pwd-saved');location.href='./index.html'" style="color:#1677ff;font-weight:600;">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
}
|
||||
.nav-back { font-size: 22px; color: #333; text-decoration: none; margin-right: 8px; line-height: 1; cursor: pointer; }
|
||||
.nav-title { font-size: 17px; font-weight: 600; color: #333; }
|
||||
.nav-right-btn { margin-left: auto; font-size: 13px; color: #1677ff; background: #e8f4ff; border: none; border-radius: 6px; padding: 5px 10px; cursor: pointer; font-weight: 500; }
|
||||
|
||||
/* ===== 页面内容 ===== */
|
||||
.page-content { flex: 1; overflow-y: auto; padding: 0 0 34px; background: #f5f5f5; }
|
||||
@@ -116,6 +117,7 @@
|
||||
<!-- [交互] 点击返回: 根据来源页面动态返回 -->
|
||||
<a class="nav-back" id="back-btn" onclick="goBack()">‹</a>
|
||||
<span class="nav-title">咨询人档案</span>
|
||||
<button class="nav-right-btn" onclick="location.href='../../../employee-app/profile/health-profile.html'">健康画像</button>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
@@ -27,7 +27,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
<div class="modal-actions">
|
||||
<!-- [交互] 点击取消: 返回 ./text-chat.html -->
|
||||
<button onclick="location.href='./text-chat.html'">取消</button>
|
||||
<!-- [交互] 点击确认结束: 跳转至 ./index.html -->
|
||||
<!-- [交互] 点击确认结束: 跳转至 ./index-v2.html -->
|
||||
<button onclick="location.href='./index.html'">确认结束</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||
@@ -48,7 +48,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||
@@ -37,7 +37,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||
@@ -41,7 +41,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||
@@ -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助手页面的工作台跳转逻辑
|
||||
|
||||
@@ -19,6 +19,7 @@ const pages = [
|
||||
{ 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' },
|
||||
|
||||
|
After Width: | Height: | Size: 39 KiB |
@@ -40,7 +40,7 @@
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
@@ -78,7 +78,7 @@
|
||||
<div class="modal-title">修改成功</div>
|
||||
<div class="modal-body">密码已更新,下次登录请使用新密码。</div>
|
||||
<div class="modal-actions">
|
||||
<!-- [交互] 确定后跳转至: ./index.html -->
|
||||
<!-- [交互] 确定后跳转至: ./index-v2.html -->
|
||||
<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>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||
@@ -132,23 +132,23 @@
|
||||
|
||||
<!-- 底部Tab栏,我的为active状态 -->
|
||||
<div class="tab-bar">
|
||||
<!-- [交互] 点击跳转至: ../workbench/index.html -->
|
||||
<!-- [交互] 点击跳转至: ../workbench/index-v2.html -->
|
||||
<div class="tab-item" onclick="location.href='../workbench/index.html'">
|
||||
<span class="tab-icon">🏥</span>
|
||||
<span class="tab-label">工作台</span>
|
||||
<span class="tab-badge">3</span>
|
||||
</div>
|
||||
<!-- [交互] 点击跳转至: ../knowledge/index.html -->
|
||||
<!-- [交互] 点击跳转至: ../knowledge/index-v2.html -->
|
||||
<div class="tab-item" onclick="location.href='../knowledge/index.html'">
|
||||
<span class="tab-icon">📚</span>
|
||||
<span class="tab-label">知识库</span>
|
||||
</div>
|
||||
<!-- [交互] 点击跳转至: ../../ai-assistant/index.html -->
|
||||
<!-- [交互] 点击跳转至: ../../ai-assistant/index-v2.html -->
|
||||
<div class="tab-item" onclick="location.href='../../ai-assistant/index.html'">
|
||||
<span class="tab-icon">🤖</span>
|
||||
<span class="tab-label">AI助手</span>
|
||||
</div>
|
||||
<!-- [交互] 点击跳转至: ../../message/index.html -->
|
||||
<!-- [交互] 点击跳转至: ../../message/index-v2.html -->
|
||||
<div class="tab-item" onclick="location.href='../../message/index.html'">
|
||||
<span class="tab-icon">💬</span>
|
||||
<span class="tab-label">消息</span>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<div class="icons"><span>●●●</span><span>WiFi</span><span>🔋</span></div>
|
||||
</div>
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<!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; padding: 12px 16px; }
|
||||
.scroll-content::-webkit-scrollbar { display: none; }
|
||||
/* 摘要卡片 */
|
||||
.summary-card { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); position: relative; cursor: pointer; transition: box-shadow .2s; }
|
||||
.summary-card:active { box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
|
||||
.summary-card__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.summary-card__date { font-size: 12px; color: #999; }
|
||||
.summary-card__toggle { font-size: 12px; color: #1677ff; cursor: pointer; user-select: none; background: none; border: none; padding: 2px 6px; display: flex; align-items: center; gap: 2px; }
|
||||
.summary-card__toggle .arrow { display: inline-block; transition: transform .25s; font-size: 10px; }
|
||||
.summary-card__toggle.collapsed .arrow { transform: rotate(-90deg); }
|
||||
.summary-card__body { overflow: hidden; transition: max-height .3s ease, opacity .3s ease; max-height: 500px; opacity: 1; }
|
||||
.summary-card__body.collapsed { max-height: 0; opacity: 0; margin: 0; padding: 0; }
|
||||
.summary-card__disclaimer { font-size: 11px; color: #bbb; line-height: 1.5; margin-top: 10px; padding-top: 10px; border-top: 1px dashed #e8e8e8; }
|
||||
.summary-card__status { font-size: 11px; padding: 3px 8px; border-radius: 10px; font-weight: 500; }
|
||||
.summary-card__status--done { background: #f6ffed; color: #52c41a; }
|
||||
.summary-card__status--timeout { background: #fff7e6; color: #fa8c16; }
|
||||
.summary-card__status--cancelled { background: #fff1f0; color: #ff4d4f; }
|
||||
.summary-card__consultant { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
||||
.summary-card__avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
|
||||
.summary-card__name { font-size: 15px; font-weight: 600; color: #1a1a1a; }
|
||||
.summary-card__relation { font-size: 11px; color: #888; font-weight: 400; margin-left: 4px; }
|
||||
.summary-card__reason { font-size: 12px; color: #666; }
|
||||
.summary-card__ai-label { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
|
||||
.summary-card__ai-label span { font-size: 11px; color: #1677ff; font-weight: 600; }
|
||||
.summary-card__ai-badge { font-size: 10px; background: linear-gradient(135deg, #e6f4ff, #bae0ff); color: #1677ff; padding: 2px 6px; border-radius: 4px; }
|
||||
.summary-card__summary { font-size: 13px; color: #333; line-height: 1.7; background: #fafbfc; border-radius: 8px; padding: 10px 12px; border-left: 3px solid #1677ff; }
|
||||
.summary-card__tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
||||
.summary-card__tag { font-size: 10px; padding: 3px 8px; border-radius: 4px; }
|
||||
.summary-card__tag--blue { color: #1677ff; background: #e8f4ff; }
|
||||
.summary-card__tag--orange { color: #d46b08; background: #fff7e6; }
|
||||
.summary-card__tag--green { color: #389e0d; background: #f6ffed; }
|
||||
.summary-card__arrow { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); 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="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" id="summary-list">
|
||||
|
||||
<!-- 摘要卡片列表 -->
|
||||
<!-- 卡片1 -->
|
||||
<div class="summary-card" onclick="location.href='./text-chat-done.html'">
|
||||
<div class="summary-card__header">
|
||||
<span class="summary-card__date">2026-08-10 14:30</span>
|
||||
<button class="summary-card__toggle" onclick="toggleCard(this, event)">收起 <span class="arrow">▲</span></button>
|
||||
</div>
|
||||
<div class="summary-card__consultant">
|
||||
<div class="summary-card__avatar" style="background:linear-gradient(135deg,#ffe0e0,#ffb3b3);">👩</div>
|
||||
<div>
|
||||
<div class="summary-card__name">张小红<span class="summary-card__relation">本人</span></div>
|
||||
<div class="summary-card__reason">头痛咨询</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card__body">
|
||||
<div class="summary-card__ai-label">
|
||||
<span>🤖 AI 摘要</span>
|
||||
<span class="summary-card__ai-badge">自动生成</span>
|
||||
</div>
|
||||
<div class="summary-card__summary">
|
||||
咨询者主诉反复头痛一周,以双侧太阳穴胀痛为主,下午加重,偶伴恶心。专家建议排查紧张性头痛可能,嘱其规律作息、减少屏幕用眼时间,必要时行头颅CT检查。已开具布洛芬缓释胶囊对症处理,并建议记录头痛日记以便后续复诊参考。
|
||||
</div>
|
||||
<div class="summary-card__tags">
|
||||
<span class="summary-card__tag summary-card__tag--orange">头痛</span>
|
||||
<span class="summary-card__tag summary-card__tag--blue">紧张性头痛</span>
|
||||
<span class="summary-card__tag summary-card__tag--green">用药指导</span>
|
||||
</div>
|
||||
<div class="summary-card__disclaimer">⚠️ 本摘要由 AI 自动生成,仅供参考,不具备专业医学诊断效力。具体诊疗请以医生意见为准。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片2 -->
|
||||
<div class="summary-card">
|
||||
<div class="summary-card__header">
|
||||
<span class="summary-card__date">2026-08-05 09:15</span>
|
||||
<button class="summary-card__toggle" onclick="toggleCard(this, event)">收起 <span class="arrow">▲</span></button>
|
||||
</div>
|
||||
<div class="summary-card__consultant">
|
||||
<div class="summary-card__avatar" style="background:linear-gradient(135deg,#e0f0ff,#b3d9ff);">👨</div>
|
||||
<div>
|
||||
<div class="summary-card__name">李明强<span class="summary-card__relation">配偶</span></div>
|
||||
<div class="summary-card__reason">腰部酸痛咨询</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card__body">
|
||||
<div class="summary-card__ai-label">
|
||||
<span>🤖 AI 摘要</span>
|
||||
<span class="summary-card__ai-badge">自动生成</span>
|
||||
</div>
|
||||
<div class="summary-card__summary">
|
||||
咨询者因长期久坐办公,近两个月出现腰部酸痛不适,晨起僵硬,活动后缓解。专家初步判断为腰肌劳损,建议进行核心肌群锻炼,调整办公坐姿,避免长时间维持同一姿势。已推荐物理治疗及居家拉伸方案,嘱两周后复诊评估。
|
||||
</div>
|
||||
<div class="summary-card__tags">
|
||||
<span class="summary-card__tag summary-card__tag--orange">腰痛</span>
|
||||
<span class="summary-card__tag summary-card__tag--blue">腰肌劳损</span>
|
||||
<span class="summary-card__tag summary-card__tag--green">康复指导</span>
|
||||
</div>
|
||||
<div class="summary-card__disclaimer">⚠️ 本摘要由 AI 自动生成,仅供参考,不具备专业医学诊断效力。具体诊疗请以医生意见为准。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片3 -->
|
||||
<div class="summary-card">
|
||||
<div class="summary-card__header">
|
||||
<span class="summary-card__date">2026-07-22 15:20</span>
|
||||
<button class="summary-card__toggle" onclick="toggleCard(this, event)">收起 <span class="arrow">▲</span></button>
|
||||
</div>
|
||||
<div class="summary-card__consultant">
|
||||
<div class="summary-card__avatar" style="background:linear-gradient(135deg,#ffe0e0,#ffb3b3);">👩</div>
|
||||
<div>
|
||||
<div class="summary-card__name">张小红<span class="summary-card__relation">本人</span></div>
|
||||
<div class="summary-card__reason">失眠调理咨询</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card__body">
|
||||
<div class="summary-card__ai-label">
|
||||
<span>🤖 AI 摘要</span>
|
||||
<span class="summary-card__ai-badge">自动生成</span>
|
||||
</div>
|
||||
<div class="summary-card__summary">
|
||||
咨询者反映近一月入睡困难,每晚平均睡眠不足5小时,伴焦虑情绪。专家评估为轻度睡眠障碍,建议建立规律作息、睡前1小时远离电子设备、尝试正念冥想放松。已开具褪黑素短期辅助,建议连续记录睡眠日记两周后复诊。
|
||||
</div>
|
||||
<div class="summary-card__tags">
|
||||
<span class="summary-card__tag summary-card__tag--orange">失眠</span>
|
||||
<span class="summary-card__tag summary-card__tag--blue">睡眠障碍</span>
|
||||
<span class="summary-card__tag summary-card__tag--green">生活方式指导</span>
|
||||
<span class="summary-card__tag summary-card__tag--green">用药指导</span>
|
||||
</div>
|
||||
<div class="summary-card__disclaimer">⚠️ 本摘要由 AI 自动生成,仅供参考,不具备专业医学诊断效力。具体诊疗请以医生意见为准。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 切换卡片 AI 摘要的展开/收起
|
||||
* @param {HTMLElement} btn - 被点击的切换按钮
|
||||
* @param {Event} e - 点击事件
|
||||
*/
|
||||
function toggleCard(btn, e) {
|
||||
e.stopPropagation();
|
||||
var body = btn.parentElement.parentElement.querySelector('.summary-card__body');
|
||||
var collapsed = btn.classList.contains('collapsed');
|
||||
if (collapsed) {
|
||||
btn.classList.remove('collapsed');
|
||||
body.classList.remove('collapsed');
|
||||
btn.innerHTML = '收起 <span class="arrow">▲</span>';
|
||||
} else {
|
||||
btn.classList.add('collapsed');
|
||||
body.classList.add('collapsed');
|
||||
btn.innerHTML = '展开 <span class="arrow">▼</span>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
|
||||
<!-- 导航栏(无结束咨询按钮) -->
|
||||
<div class="nav-bar">
|
||||
<!-- [交互] 点击返回: ./index.html -->
|
||||
<!-- [交互] 点击返回: ./index-v2.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>
|
||||
|
||||