diff --git a/other-module/canteen-device/canteen-nutrition-scale/doc/异常场景素材-秤体视角.md b/other-module/canteen-device/canteen-nutrition-scale/doc/异常场景素材-秤体视角.md
new file mode 100644
index 0000000..64a89b8
--- /dev/null
+++ b/other-module/canteen-device/canteen-nutrition-scale/doc/异常场景素材-秤体视角.md
@@ -0,0 +1,179 @@
+# 营养秤异常场景素材(秤体视角)
+
+> 为模型师提供素材,以秤体为主体视角,每条场景按「机器描述 → 场景分析」组织。
+
+---
+
+## 1. 夹子是否异常
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 1.1 | 初始在夹盘,取餐后仍在夹盘 | ✅ 正常 |
+| 1.2 | 初始在食物盆,取餐后仍在食物盆 | ✅ 正常 |
+| 1.3 | 秤检测到:夹子初始在夹盘,取餐后检测到夹子在食物盆 | 用户取餐后把夹子扔进盆里,未归位 |
+| 1.4 | 秤检测到:夹子初始在食物盆,取餐后检测到夹子在夹盘 | 用户把夹子从盆里拿出来放回夹盘 |
+| 1.5 | 秤检测到:夹子初始在夹盘,取餐后夹子信号消失 | 用户拿着夹子走了 |
+| 1.6 | 秤检测到:夹子初始在食物盆,取餐后夹子信号消失 | 用户从盆里抓起夹子带走了 |
+| 1.7 | 秤检测到:本轮取餐开始前,夹子就不在夹盘上 | 上一轮遗留的异常状态,初始态已错 |
+
+---
+
+## 2. 是否本人取餐
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 2.1 | 人脸 ID=A,取餐动作归属 A | ✅ 正常 |
+| 2.2 | 人脸 ID=A,取餐动作归属 B | B 的手伸进画面取了菜。身份冒用 |
+| 2.3 | 人脸未识别到,但发生了重量减少 | 识别故障,取餐无主 |
+| 2.4 | 人脸 ID=A,A 正在取餐,B 的手也伸入 | 多人混取 |
+| 2.5 | 两人同时站在识别区,两笔重量同时发生 | 两人同时取餐 |
+| 2.6 | 两人同时站在识别区,重量分先后两次 | 两人轮流取餐 |
+| 2.7 | 人脸 ID=A 持续识别中突断(扭头),数秒后恢复,期间发生了重量变化 | 人脸丢失又恢复,中间段归属存疑 |
+| 2.8 | 人脸 ID=A 持续识别中突断(摄像头被遮挡),无恢复 | 非主动丢失,可能是物品遮挡镜头 |
+
+---
+
+## 3. 是否正常取餐
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 3.1 | 重量陡降 N 克,随后稳定,期间人脸持续识别 | ✅ 正常取餐 |
+| 3.2 | 重量陡降 N 克,数秒后重量恢复 N 克,人脸持续识别 | 夹起来看了看又放回去了(反悔) |
+| 3.3 | 重量连续多次小幅波动(±几克),最终稳定在一个降幅 | 夹子试探/抖动,多次夹取后最终取走 |
+| 3.4 | 重量无变化,但夹子从夹盘拿起又放回 | 空夹——夹了但什么都没夹到 |
+| 3.5 | 重量陡降 N 克,过程中有短暂微幅回升,最终降幅 < N | 夹起时汤汁滴落或食物部分滑落回盆,实际取走量小于夹取量 |
+| 3.6 | 重量陡降 N 克,且邻盆也检测到微小重量变化 | 食物掉落台面或汤汁溅到邻盆 |
+| 3.7 | 重量出现持续不规则波动,幅度较小,期间检测到夹子未动作 | 用户身体倚靠或手碰到台面 |
+| 3.8 | 邻盆无夹子动作但发生微小重量变化 | 用户取餐时碰到了旁边的盆 |
+
+---
+
+## 4. 重量变化是否合理
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 4.1 | 重量减少,幅度在合理范围内 | ✅ 正常 |
+| 4.2 | 重量增加(排除补菜后) | 方向异常——可能是回退、夹子掉入盆中、或人为按压 |
+| 4.3 | 重量单次减少远超单人单次合理取餐量 | 超量——可能是夹子掉入盆中被当作取餐、或传感器异常 |
+| 4.4 | 重量在无操作期间持续缓慢变化(非阶跃) | 传感器漂移,或热食蒸汽导致读数偏移 |
+| 4.5 | 重量瞬时跳变(毫秒级阶跃),无夹子动作伴随 | 秤体被碰撞或台面振动 |
+| 4.6 | 当前盆内净重接近于零(仅剩碎屑),发生了取餐动作 | 空盆取餐——边界值易误判,可能取到的是盆重波动 |
+
+---
+
+## 5. 是否发生回退
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 5.1 | 取餐完成后,重量出现增加,人脸 ID 与取餐时一致 | 用户从自己盘里夹回一部分到盆中 |
+| 5.2 | 取餐完成后,重量增加至接近初始值,人脸 ID 与取餐时一致 | 用户全部倒回,完全反悔。全程净变化≈0 |
+| 5.3 | 取餐完成后,重量增加过程中人脸信号中断(扭头)又恢复 | 回退过程中转头,中间段归属存疑 |
+| 5.4 | 取餐时人脸 ID=A,回退时人脸 ID=B | A 取走的菜,B 往回放。跨用户反向操作 |
+| 5.5 | 无人识别状态,重量先增后恢复(手按/抬秤面),随后人脸识别进入,正常取餐 | 恶意操作——取餐前人为改变初始重量基准,骗取重量差 |
+| 5.6 | 取餐完成后,重量出现增加,但回退餐品与当前盆内菜品不一致(用户从 A 盆取了菜,却往回放到 B 盆) | 跨菜品回退——回退食物不属于当前盆,导致 B 盆重量增加但菜品成分不匹配 |
+
+---
+
+## 6. 秤体能否检测到变化
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 6.1 | 重量持续以 1~2g/次的幅度递减,单次变化 < 传感器分辨率,累计已超阈值但全程无触发 | 逐克静默取餐——用户少量多次夹取,秤"感觉不到"但食物已大量减少 |
+| 6.2 | 重量极缓慢持续下降,斜率低于触发阈值,累计变化已达显著值 | 漂移式取餐——用户缓慢倾倒食物,规避检测 |
+| 6.3 | 重量先降 A 克又增 B 克,|A-B| < 阈值,净变化近似为零 | 取多补少——取走一部分又补回一部分,净值抵消 |
+| 6.4 | 多次微量取餐,每次单独 < 阈值,累计远超阈值 | 分散取餐——需要累积判定才能发现 |
+| 6.5 | 开餐/补餐后重量仍在波动中(食物刚倒入、盆未稳定),此时发生取餐 | 补餐后不稳定期取餐——基准未锁定,变化量计算不准 |
+
+---
+
+## 7. 是否为服务员操作
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 7.1 | 重量陡增(幅度远超单人回退),无人脸识别 | 服务员补菜 |
+| 7.2 | 重量发生整体替换式变化(先清零后陡增),无人脸识别 | 服务员换整盆菜 |
+| 7.3 | 重量大幅减少(负向变化 ≈ 盆自重),读数变为负值(低于去皮基准),无人脸识别 | 服务员收走空盆 |
+| 7.4 | 重量陡增期间,恰有用户在识别区取餐 | 服务员补菜与用户取餐并发——重量增和减同时发生 |
+| 7.5 | 重量出现频繁小幅不规则波动,无人脸识别,无夹子动作 | 保洁擦拭台面 |
+
+---
+
+## 8. 秤体自身是否正常
+
+| # | 机器描述 | 场景分析 |
+|---|---------|---------|
+| 8.1 | 重量短时间内出现巨大跳变后恢复,无夹子动作 | 秤体被碰撞或搬动 |
+| 8.2 | 热食蒸汽持续熏蒸秤面,重量读数持续单向漂移 | 温度干扰导致传感器漂移 |
+| 8.3 | 运行中突然断电→重启,期间发生的取餐数据全部丢失 | 断电丢数——重启后状态不连续 |
+| 8.4 | 网络中断,秤端本地缓存数据未上报 | 断网积压——恢复后需补传,时序可能错乱 |
+| 8.5 | 本秤检测到微小振动,但夹子和重量均无变化,邻秤恰好有取餐事件 | 邻秤串扰——隔壁取餐的振动传到了本秤 |
+| 8.6 | 台面上有上一轮遗留的食物残渣/汤汁,导致初始重量偏高 | 台面未清洁——初始重量含异物 |
+| 8.7 | 台面上有残留物品,当前用户取餐后残留物被一并带走 | 遗留物被清走——减重包含异物重量 |
+
+---
+
+## 9. 多人交叉场景
+
+### 9.1 交叉矩阵总览
+
+```
+A 的状态 × B 的动作 = 场景
+
+ B仅入镜 B取餐 B回退 B按压秤面
+A取餐中 M1 M2 M3 M4
+A完成未离开 M5 M6 M7 M8
+A识别未取餐 — M9 M10 —
+```
+
+### 9.2 A 取餐中 + B 入镜
+
+| # | A 状态 | B 动作 | 机器描述 | 场景分析 |
+|---|--------|--------|---------|---------|
+| M1 | A 取餐中 | B 入镜不动 | A 人脸持续,画面中出现 B 人脸;重量仅 A 侧在变化 | 两人同时被识别,但只有 A 在取。订单归属以 A 为准 |
+| M2 | A 取餐中 | B 取餐 | A/B 人脸交替或同框;两笔重量下降交织发生 | 两人同时取餐,需帧级对齐人脸与重量变化,拆分成两笔 |
+| M3 | A 取餐中 | B 回退 | A 人脸持续,B 人脸同时出现;重量增加与 B 入镜时序吻合 | A 在减重、B 在增重,净值需解耦 |
+| M4 | A 取餐中 | B 按压秤面 | A/B 人脸同框;重量突然阶跃(增/减),与任何夹子动作无关 | A 正在取餐时 B 恶意改变基准,A 的取餐量被污染 |
+
+### 9.3 A 完成未离开 + B 入镜
+
+| # | A 状态 | B 动作 | 机器描述 | 场景分析 |
+|---|--------|--------|---------|---------|
+| M5 | A 完成未离开 | B 入镜不动 | A/B 双人脸同框,重量已稳定 | A 占位,B 等待。系统需判定当前订单归属仍为 A |
+| M6 | A 完成未离开 | B 取餐 | A 人脸存在,B 人脸出现;新重量下降与 B 人脸时序匹配 | B 接力取餐,需将新减重从 A 名下切换到 B |
+| M7 | A 完成未离开 | B 回退 | A 人脸存在,B 人脸出现;重量增加与 B 入镜时序吻合 | A 已取完,B 往回放。回退归属应为 B |
+| M8 | A 完成未离开 | B 按压秤面 | A/B 双人脸同框;重量异常阶跃无夹子动作 | A 的结算基准被篡改 |
+
+### 9.4 A 识别未取餐 + B 入镜
+
+| # | A 状态 | B 动作 | 机器描述 | 场景分析 |
+|---|--------|--------|---------|---------|
+| M9 | A 识别未取餐 | B 取餐 | A 人脸存在,B 人脸出现;重量下降与 B 入镜同步,A 无动作 | A 站着没动,B 先取了。A 的订单怎么处理?空单关闭还是保留? |
+| M10 | A 识别未取餐 | B 回退 | A 人脸存在,B 人脸出现;重量增加与 B 入镜同步 | A 还没取餐,重量先涨了。回退归属 B,A 订单状态存疑 |
+
+### 9.5 A 离开后 B 继承异常状态
+
+| # | A 遗留 | B 动作 | 机器描述 | 场景分析 |
+|---|--------|--------|---------|---------|
+| M11 | A 取餐后夹子留在盆里 | B 入镜取餐 | 夹子初始在食物盆(A 遗留);B 人脸识别正常,取餐动作正常 | B 继承异常初始态,夹子位置判定基准被 A 污染 |
+| M12 | A 把夹子带走 | B 入镜取餐 | 夹盘无夹子;B 已识别,用手或其他方式取餐 | 工具缺失,B 取餐方式改变 |
+| M13 | A 遗留汤汁/残渣在台面 | B 入镜取餐 | 初始重量含遗留物;B 取餐过程中可能碰掉残渣 | B 的取餐计量含遗留物干扰 |
+| M14 | A 按压秤面后离开 | B 入镜取餐 | 秤未归零,初始基准被 A 篡改;B 正常取餐 | 重量绝对值不准,差值也可能偏移 |
+| M15 | A 完成→离开→B 入镜取餐 | B 未识别,重量下降 | 摄像头未能识别 B,但发生了取餐 | 设备识别故障,取餐无主 |
+| M16 | A 完成→离开→B 入镜回退 | B 未识别,重量增加 | 摄像头未能识别 B,但发生了回退 | 设备识别故障,回退无主 |
+
+---
+
+## 维度统计
+
+| 维度 | 条目数 |
+|------|:-----:|
+| 1. 夹子是否异常 | 7 |
+| 2. 是否本人取餐 | 8 |
+| 3. 是否正常取餐 | 8 |
+| 4. 重量变化是否合理 | 6 |
+| 5. 是否发生回退 | 6 |
+| 6. 秤体能否检测到变化 | 5 |
+| 7. 是否为服务员操作 | 5 |
+| 8. 秤体自身是否正常 | 7 |
+| 9. 多人交叉场景 | 16 |
+| **合计** | **68** |
diff --git a/other-module/canteen-device/doc/加工厂硬件流程说明.md b/other-module/canteen-device/doc/加工厂硬件流程说明.md
new file mode 100644
index 0000000..c100d00
--- /dev/null
+++ b/other-module/canteen-device/doc/加工厂硬件流程说明.md
@@ -0,0 +1,172 @@
+# 加工厂硬件流程说明
+
+> 适用范围:`other-module/canteen-device/` 下「卫生供应」分组所覆盖的加工厂硬件终端(「健康生产」「营养用餐」分组属餐厅/食堂现场,不在加工厂范畴)
+> 创建时间:2026-08-13
+> 说明:本文档描述的硬件设备,以已设计(有实际原型页面)的设备为准;仅占位、未设计的设备视为"暂不存在"。
+
+---
+
+## 目录
+
+- **一、业务描述**
+ - 1.1 加工厂介绍
+ - 1.2 硬件设备清单
+ - 1.3 设备之间的关系
+ - 1.4 完整业务流程
+- **二、硬件使用文档**
+ - 2.1 毛菜验收秤
+
+---
+
+## 一、业务描述
+
+### 1.1 加工厂介绍
+
+加工厂承担「卫生供应」业务,将毛菜加工成可直接净配、包装、发货的净菜包。本期共 8 类硬件设备,其中 7 台已设计、1 台(毛菜卫检设备)待硬件选型。
+
+### 1.2 硬件设备清单
+
+#### 已设计设备(共 7 台)
+
+| 序号 | 设备 | 目录 | 核心职能 | 设计位置 |
+|---|---|---|---|---|
+| 1 | 毛菜验收秤 | `raw-veg-inspection-scale` | 毛菜到货验收、称重、AI 食材识别、质检判定、食材采样 | 设备端 8 页 + PRD / FLOW |
+| 2 | 毛菜库存柜 | `raw-veg-inventory-cabinet` | 毛菜储位分配、出入库管理 | 后台 `sup-warehouse` 仓储管理 / `sup-in-raw` 毛菜入库 |
+| 3 | 加工毛菜秤 | `raw-veg-processing-scale` | 毛菜加工任务、批次称重、清洗操作 | 设备端 7 页 + FLOW |
+| 4 | 厨配秤 | `clean-veg-ratio-scale` | 净菜上秤识别、加工确认(含尺寸/形状/刀工规格) | 设备端 6 页 |
+| 5 | 净菜库存柜 | `clean-veg-inventory-cabinet` | 净菜库存管理与盘点 | 后台 `sup-warehouse` 仓储管理 / `sup-in-cleanin` 净菜入库 |
+| 6 | 净配秤 | `ratio-scale` | 净菜包 / 餐品净菜包净配(主辅材 + 调料双区称重) | 设备端 5 页 + PRD |
+| 7 | 净菜包装贴标秤 | `clean-veg-packaging-labeler` | 核对实物、包装塑封、打印标签、发货 | 设备端 4 页 |
+
+#### 待硬件选型设备(共 1 台)
+
+| 设备 | 目录 | 规划职能 | 现状 |
+|---|---|---|---|
+| 毛菜卫检设备 | `raw-veg-hygiene-inspection` | 毛菜卫生检测与质量判定 | 占位页,待硬件选型(后台 `sup-hygiene-check` 卫生检测已设计) |
+
+> 另有历史遗留目录 `raw-veg-washing-scale`(毛菜清洗秤),已并入「加工毛菜秤」,未出现在导航中,不计入设备清单。
+
+### 1.3 设备之间的关系
+
+7 台已设计设备构成一条**毛菜 → 净菜包**的加工流水线,上游设备的产出是下游设备的输入:
+
+```mermaid
+flowchart LR
+ A[毛菜验收秤
到货验收·称重·AI识别] --> B[毛菜库存柜
储位分配·出入库]
+ B --> C[加工毛菜秤
毛菜清洗]
+ C --> D[厨配秤
净菜切配·加工确认]
+ D --> E[净菜库存柜
净菜库存·盘点]
+ E --> F[净配秤
净菜包/餐品组合打包]
+ F --> G[净菜包装贴标秤
包装·打印·发货]
+```
+
+| 设备 | 上游(输入) | 下游(输出) |
+|---|---|---|
+| 毛菜验收秤 | 后台「绿色种采-采供管理」填好配送信息的采购订单 | 毛菜入库记录(后台建档)→ 毛菜库存柜 |
+| 毛菜库存柜 | 毛菜验收秤入库的毛菜 | 按储位取出的毛菜 → 加工毛菜秤 |
+| 加工毛菜秤 | 毛菜库存柜取出的毛菜 + 后台推送的毛菜加工任务 | 清洗完成的毛菜 → 厨配秤 |
+| 厨配秤 | 加工毛菜秤清洗完成的毛菜 + 切配任务 | 切配好的净菜 → 净菜库存柜 |
+| 净菜库存柜 | 厨配秤切配好的净菜 | 库存净菜 → 净配秤 |
+| 净配秤 | 净菜库存柜取出的净菜 + 净配任务 | 净配完成的净菜包 → 净菜包装贴标秤 |
+| 净菜包装贴标秤 | 净配秤净配完成的订单(净菜包 / 餐品净菜包) | 已包装、已打印标签、可发货的净菜包 |
+
+> 本期所有硬件设备以**场所(食堂/加工厂)为维度**组织数据,设备是场所的附属资源,不作为独立维度。任务单、验收记录、设备配置等均归属到场所。
+
+### 1.4 完整业务流程
+
+加工厂业务是一条「**采购 → 验收 → 入库 → 清洗 → 切配 → 入库 → 净配 → 包装 → 发货**」的完整闭环,全程由后台系统(Web 管理端「卫生供应 / 绿色种采」)驱动任务下发,硬件终端负责现场执行并回传数据。
+
+```mermaid
+flowchart TD
+ subgraph 后台系统
+ P[采供管理
采购下单·填配送信息]
+ T[任务整合
定时推送加工任务]
+ end
+
+ subgraph 加工厂现场
+ S1[毛菜验收秤
到货验收·采样]
+ S2[毛菜库存柜
储位分配·出入库]
+ S3[加工毛菜秤
毛菜清洗]
+ S4[厨配秤
净菜切配]
+ S5[净菜库存柜
净菜库存·盘点]
+ S6[净配秤
净菜包净配]
+ S7[净菜包装贴标秤
包装·发货]
+ end
+
+ P --> S1
+ S1 --> S2
+ S2 --> S3
+ T -.毛菜加工任务.-> S3
+ T -.切配任务.-> S4
+ S3 --> S4
+ S4 --> S5
+ S5 --> S6
+ T -.净配任务.-> S6
+ S6 --> S7
+ S7 --> OUT[发货/外售·内供]
+
+ S1 --> DB[(毛菜入库
后台建档)]
+ S3 --> DB2[(清洗记录)]
+ S4 --> DB3[(加工记录)]
+ S6 --> DB4[(净配记录)]
+ S7 --> DB5[(包装·发货记录)]
+```
+
+| 阶段 | 执行设备 | 关键动作 | 后台联动 |
+|---|---|---|---|
+| ① 采购 | 后台采供管理 | 采购下单、填配送信息 | 生成采购订单 PO |
+| ② 到货验收 | 毛菜验收秤 | 称重、AI 识别、质检判定、采样 | 毛菜入库建档、溯源码分配 |
+| ③ 毛菜入库 | 毛菜库存柜 | 储位分配、出入库管理 | `sup-warehouse` 仓储管理、`sup-in-raw` 毛菜入库 |
+| ④ 毛菜清洗 | 加工毛菜秤 | 批次称重、毛菜加工任务处理 | 毛菜加工任务推送、任务状态回传 |
+| ⑤ 净菜切配 | 厨配秤 | 上秤识别、加工确认(切配规格) | 切配任务推送、加工记录 |
+| ⑥ 净菜入库 | 净菜库存柜 | 净菜库存管理、盘点 | `sup-warehouse` 仓储管理、`sup-in-cleanin` 净菜入库 |
+| ⑦ 净配 | 净配秤 | 净菜包 / 餐品组合打包 | 净配任务推送、净配记录 |
+| ⑧ 包装发货 | 净菜包装贴标秤 | 核对、包装、打印标签、发货 | 发货状态回传、配送信息 |
+
+> 「毛菜卫检」环节(对应后台 `sup-hygiene-check` 卫生检测)因设备端尚待硬件选型,未纳入上述闭环;待选型完成后将插入「到货验收」与「毛菜入库」之间。
+
+---
+
+## 二、硬件使用文档
+
+### 2.1 毛菜验收秤
+
+#### 用途
+
+毛菜验收秤负责毛菜到货的**验收、采样、记录查询**。
+
+#### 硬件配置
+
+| 组件 | 用途 |
+|---|---|
+| 触摸屏 | 交互操作、信息确认、不合格处理 |
+| 称重传感器 + 秤盘 | 自动读取重量(kg) |
+| 食材识别摄像头 | AI 自动识别食材品种 |
+| 场景监控摄像头 | 后台全程静默录像,不参与前端交互 |
+
+#### 登录方式
+
+账号密码登录,登录后保持会话;身份以登录账号为准,未登录无法操作。
+
+#### 场景
+
+| 场景 | 触发条件 | 说明 |
+|---|---|---|
+| **有订单验收** | 后台采购订单已填配送信息 | 主场景,按订单逐食材验收 |
+| **食材采样** | 验收前采集样本 | 验收前置环节 |
+
+#### 按场景使用
+
+##### 场景一:有订单验收(主场景)
+
+1. 在欢迎页输入账号密码登录。
+2. 点击「验收食材」,选择要验收的订单。
+3. 把食材放上秤盘,系统自动识别并称重;拿起食材即完成该食材的验收。
+4. 订单内食材全部验收完后,在弹出的汇总弹窗点「提交并继续」或「提交并关闭」。
+
+##### 场景二:食材采样(验收前置)
+
+1. 在欢迎页点击「食材采样」,登录后进入采样页。
+2. 输入食材名称,点击拍照。
+3. 点击「保存」或「保存并继续」。
+
diff --git a/web-admin/nutrition/din-terminal.html b/web-admin/nutrition/din-terminal.html
index 28263f3..192f110 100644
--- a/web-admin/nutrition/din-terminal.html
+++ b/web-admin/nutrition/din-terminal.html
@@ -76,6 +76,7 @@ tbody tr:hover{background:#F8FBFF;}
.tag-orange{background:#FFF7E6;color:var(--warning);}
.tag-red{background:#FFF1F0;color:var(--danger);}
.tag-gray{background:#F5F5F5;color:#999;}
+.tag-purple{background:#F9F0FF;color:#722ED1;}
.link{color:var(--primary);cursor:pointer;font-size:var(--font-sm);}.link:hover{color:#40a9ff;}
.pagination{display:flex;align-items:center;justify-content:flex-end;padding:16px 24px;gap:8px;border-top:1px solid var(--border);}
.pagination span{font-size:var(--font-sm);color:var(--text-secondary);}
@@ -109,7 +110,7 @@ tbody tr:hover{background:#F8FBFF;}
.stat-card__label{font-size:var(--font-sm);color:var(--text-secondary);}
.action-bar{display:flex;align-items:center;gap:8px;margin-bottom:16px;}
/* 动态弹窗基础 */
-.dyn-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:1010;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .25s;}
+.dyn-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:1100;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .25s;}
.dyn-overlay.show{opacity:1;}
.dyn-modal{background:#fff;border-radius:var(--radius-lg);max-height:80vh;display:flex;flex-direction:column;transform:translateY(30px);transition:transform .25s;box-shadow:0 12px 40px rgba(0,0,0,0.18);}
.dyn-overlay.show .dyn-modal{transform:translateY(0);}
@@ -311,6 +312,46 @@ tbody tr:hover{background:#F8FBFF;}
/* Tab 内嵌区块标题 */
.ov-section-title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);margin:0 0 12px;display:flex;align-items:center;gap:6px;}
.ov-section-title::before{content:'';display:inline-block;width:3px;height:15px;background:var(--primary);border-radius:2px;}
+/* 表头列筛选 */
+.th-filter{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-left:4px;border-radius:3px;color:#c0c4cc;font-size:10px;cursor:pointer;vertical-align:-3px;transition:all .15s;}
+.th-filter:hover{color:var(--primary);background:#e8f1ff;}
+.th-filter--active{color:var(--primary);background:#e8f1ff;}
+.th-filter-pop{position:fixed;z-index:1700;min-width:150px;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:0 6px 20px rgba(0,0,0,.15);padding:6px;max-height:240px;overflow-y:auto;}
+.th-filter-pop__item{padding:6px 10px;font-size:var(--font-sm);color:var(--text-primary);border-radius:4px;cursor:pointer;white-space:nowrap;}
+.th-filter-pop__item:hover{background:#f5f8ff;}
+.th-filter-pop__item.active{color:var(--primary);background:#e8f1ff;font-weight:500;}
+/* ===== 餐线营养秤概览(子页切换) ===== */
+.scale-sub-tabs{display:flex;gap:8px;margin-bottom:16px;border-bottom:1px solid var(--border);}
+.scale-sub-tab{padding:8px 16px;font-size:var(--font-sm);color:var(--text-secondary);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;user-select:none;transition:all .15s;}
+.scale-sub-tab:hover{color:var(--primary);}
+.scale-sub-tab.active{color:var(--primary);font-weight:600;border-bottom-color:var(--primary);}
+/* ===== 人脸识别视频回放弹窗 ===== */
+@keyframes ovScan{0%{top:10%}50%{top:88%}100%{top:10%}}
+.face-video-mask{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1600;display:flex;align-items:center;justify-content:center;animation:ovFadeIn .2s ease;}
+.face-video-mask.closing{animation:ovFadeOut .2s ease forwards;}
+.face-video-modal{width:680px;max-width:92vw;max-height:86vh;background:#fff;border-radius:var(--radius-md);box-shadow:0 12px 40px rgba(0,0,0,.22);display:flex;flex-direction:column;overflow:hidden;animation:ovModalIn .22s ease;}
+.face-video-mask.closing .face-video-modal{animation:ovModalOut .18s ease forwards;}
+.face-video-modal__header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0;}
+.face-video-modal__title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);}
+.face-video-modal__close{width:32px;height:32px;border:none;background:none;font-size:20px;cursor:pointer;border-radius:var(--radius-sm);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;}
+.face-video-modal__close:hover{background:#f5f5f5;color:var(--text-primary);}
+.face-video-modal__body{padding:20px;overflow-y:auto;flex:1;}
+.face-video-player{position:relative;background:#141a24;border-radius:var(--radius-md);height:280px;display:flex;align-items:center;justify-content:center;overflow:hidden;}
+.face-video-player__scan{position:absolute;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,#40a9ff,transparent);animation:ovScan 3s linear infinite;}
+.face-video-player__badge{position:absolute;top:12px;left:14px;font-size:10px;color:#fff;background:rgba(255,77,79,.85);padding:2px 8px;border-radius:10px;z-index:1;}
+.face-video-player__meta{position:absolute;bottom:12px;left:14px;right:14px;display:flex;justify-content:space-between;color:rgba(255,255,255,.65);font-size:var(--font-xs);font-family:monospace;z-index:1;}
+.face-video-player__play{width:52px;height:52px;border-radius:50%;background:rgba(24,144,255,.18);border:2px solid rgba(24,144,255,.7);display:flex;align-items:center;justify-content:center;color:#40a9ff;cursor:pointer;z-index:1;font-size:18px;}
+.face-video-controls{display:flex;align-items:center;gap:10px;padding:14px 4px 4px;}
+.face-video-btn{height:30px;padding:0 12px;border:1px solid var(--border);background:#fff;border-radius:var(--radius-sm);cursor:pointer;font-size:var(--font-xs);color:var(--text-secondary);white-space:nowrap;}
+.face-video-btn:hover{border-color:var(--primary);color:var(--primary);}
+.face-video-btn--primary{min-width:36px;padding:0;display:flex;align-items:center;justify-content:center;font-size:14px;border-color:var(--primary);color:var(--primary);}
+.face-video-bar{flex:1;height:6px;background:#f0f0f0;border-radius:3px;overflow:hidden;cursor:pointer;}
+.face-video-bar__progress{height:100%;background:var(--primary);border-radius:3px;width:0;}
+.face-video-time{font-size:var(--font-xs);color:var(--text-secondary);font-family:monospace;white-space:nowrap;}
+.face-video-info{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:12px;padding-top:14px;border-top:1px solid #f0f0f0;}
+.face-video-info__item{font-size:var(--font-sm);}
+.face-video-info__item .label{display:block;font-size:var(--font-xs);color:var(--text-placeholder);margin-bottom:4px;}
+.face-video-info__item strong{color:var(--text-primary);font-weight:600;}
@@ -668,6 +709,7 @@ document.querySelectorAll('tbody tr').forEach(row=>{
/* ===== 设备概览大弹窗(原型演示:Tab 集中演示各类设备,实际交付按设备类型显示对应概览) ===== */
/* Tab 定义:仅「智能货柜」已实现内容,其余为占位 */
var OV_TABS=[
+ {key:'scale',name:'餐线营养秤',icon:'⚖️'},
{key:'plate',name:'餐盘柜',icon:'🍽️'},
{key:'smart',name:'智能货柜',icon:'🗄️'},
{key:'raw',name:'毛菜货柜',icon:'🥬'},
@@ -725,6 +767,7 @@ function switchOvTab(key){
/* 按 Tab 渲染内容:占位 / 智能货柜可视化 */
function renderOvTabBody(key){
+ if(key==='scale')return renderCanteenScale();
if(key==='smart')return renderSmartCabinet();
if(key==='plate')return renderPlateCabinet();
if(key==='clean')return renderStockCabinet('clean');
@@ -741,6 +784,340 @@ function renderOvTabBody(key){
`;
}
+/* ===== 表头列筛选(餐线营养秤四个 Tab 复用) ===== */
+var SCALE_FILTERS={status:{},face:{},weight:{},log:{}};
+/* 每个 Tab 的列定义:key 列标识 / label 表头文字 / field 数据字段 */
+var SCALE_COLS={
+ status:[
+ {key:'time',label:'日期/时间',field:'time'},
+ {key:'item',label:'监控项',field:'item'},
+ {key:'event',label:'事件',field:'event'}
+ ],
+ face:[
+ {key:'start',label:'开始时间',field:'start'},
+ {key:'end',label:'结束时间',field:'end'},
+ {key:'status',label:'状态',field:'status'},
+ {key:'weight',label:'重量变化',field:'weight'},
+ {key:'total',label:'总重量',field:'total'}
+ ],
+ weight:[
+ {key:'start',label:'开始时间',field:'start'},
+ {key:'end',label:'结束时间',field:'end'},
+ {key:'target',label:'识别对象',field:'target'},
+ {key:'submit',label:'提交重量',field:'submit'},
+ {key:'total',label:'总重量',field:'total'}
+ ],
+ log:[
+ {key:'time',label:'日期/时间',field:'time'},
+ {key:'type',label:'报错类型',field:'type'},
+ {key:'content',label:'报错内容',field:'code'}
+ ]
+};
+/* 各维度数据声明(实际数据在对应渲染函数上方赋值) */
+var SCALE_STATUS_EVENTS=[];
+var SCALE_WEIGHT_RECORDS=[];
+var SCALE_LOG_RECORDS=[];
+
+/* 生成带筛选图标的表头单元格 */
+function scaleTh(colKey,label,tabKey){
+ var active=SCALE_FILTERS[tabKey]&&SCALE_FILTERS[tabKey][colKey];
+ return label+'▾';
+}
+/* 生成表头(可选追加操作列,操作列不可筛选) */
+function scaleThead(tabKey,actionLabel){
+ var ths=SCALE_COLS[tabKey].map(function(c){return ''+scaleTh(c.key,c.label,tabKey)+' | ';}).join('');
+ if(actionLabel)ths+=''+actionLabel+' | ';
+ return ''+ths+'
';
+}
+/* 取某 Tab 原始数据 */
+function getScaleRows(tabKey){
+ if(tabKey==='status')return SCALE_STATUS_EVENTS||[];
+ if(tabKey==='face')return SCALE_FACE_RECORDS||[];
+ if(tabKey==='weight')return SCALE_WEIGHT_RECORDS||[];
+ if(tabKey==='log')return SCALE_LOG_RECORDS||[];
+ return [];
+}
+/* 应用当前筛选,返回过滤后的行 */
+function getFilteredScaleRows(tabKey){
+ var rows=getScaleRows(tabKey);
+ var f=SCALE_FILTERS[tabKey]||{};
+ var cols=SCALE_COLS[tabKey]||[];
+ return rows.filter(function(r){
+ for(var i=0;i全部';
+ values.forEach(function(v){
+ html+=''+escapeHtml(v)+'
';
+ });
+ pop.innerHTML=html;
+ document.body.appendChild(pop);
+ var rect=icon.getBoundingClientRect();
+ pop.style.left=Math.min(rect.left,window.innerWidth-170)+'px';
+ pop.style.top=(rect.bottom+4)+'px';
+ document.addEventListener('click',onDocClickForFilter,{once:true});
+}
+function onDocClickForFilter(e){
+ if(!e.target.closest('.th-filter-pop'))closeThFilter();
+}
+function closeThFilter(){
+ var p=document.getElementById('thFilterPop');
+ if(p)p.remove();
+}
+function pickThFilter(tabKey,colKey,val){
+ if(!SCALE_FILTERS[tabKey])SCALE_FILTERS[tabKey]={};
+ SCALE_FILTERS[tabKey][colKey]=val;
+ closeThFilter();
+ var body=document.getElementById('scaleSubBody');
+ if(body)body.innerHTML=renderScaleSubBody(SCALE_SUB_TAB);
+}
+function escapeHtml(s){
+ return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
+}
+function escapeAttr(s){
+ return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');
+}
+
+/* ===== 餐线营养秤:四个维度(子页切换) ===== */
+var SCALE_SUB_TABS=[
+ {key:'status',name:'设备状态监控'},
+ {key:'weight',name:'重量变化数据'},
+ {key:'face',name:'人脸识别数据'},
+ {key:'log',name:'运行日志'}
+];
+var SCALE_SUB_TAB='status';
+
+function renderCanteenScale(){
+ const tabsHtml=SCALE_SUB_TABS.map(t=>`${t.name}
`).join('');
+ return `${tabsHtml}
${renderScaleSubBody(SCALE_SUB_TAB)}
`;
+}
+function switchScaleSubTab(key){
+ SCALE_SUB_TAB=key;
+ document.querySelectorAll('#ovTabBody .scale-sub-tab').forEach(t=>t.classList.toggle('active',t.dataset.key===key));
+ const body=document.getElementById('scaleSubBody');
+ if(body)body.innerHTML=renderScaleSubBody(key);
+}
+function renderScaleSubBody(key){
+ if(key==='status')return renderScaleStatus();
+ if(key==='face')return renderScaleFace();
+ if(key==='weight')return renderScaleWeight();
+ if(key==='log')return renderScaleLog();
+ return '';
+}
+
+/* 维度1:设备状态监控 — 状态事件流水 */
+SCALE_STATUS_EVENTS=[
+ {time:'2026-08-25 08:15:32.456',item:'网络连接',event:'重新连接成功'},
+ {time:'2026-08-25 08:00:00.000',item:'运行状态',event:'开餐'},
+ {time:'2026-08-25 07:40:12.511',item:'运行状态',event:'加餐'},
+ {time:'2026-08-25 06:30:00.000',item:'设备开关机',event:'开机'},
+ {time:'2026-08-25 04:12:33.098',item:'运行状态',event:'换餐'},
+ {time:'2026-08-24 23:00:00.000',item:'设备开关机',event:'关机'},
+ {time:'2026-08-24 22:30:18.764',item:'运行状态',event:'闭餐'},
+ {time:'2026-08-24 21:00:02.130',item:'运行状态',event:'进入待机'}
+];
+function renderScaleStatus(){
+ const rows=getFilteredScaleRows('status').map(s=>`| ${s.time} | ${s.item} | ${s.event} |
`).join('');
+ return `设备状态监控
+ ${scaleThead('status')}${rows}
`;
+}
+
+/* 维度2:人脸识别数据 — 开始/结束时间 / 状态 / 用户类型 / 重量变化 + 视频回放 */
+var SCALE_FACE_RECORDS=[
+ {start:'2026-08-25 08:15:23.456',end:'2026-08-25 08:15:58.210',status:'刷脸+离开',userType:'系统用户',total:'486.5g',weight:'+486.5g'},
+ {start:'2026-08-25 08:14:51.203',end:'2026-08-25 08:15:02.118',status:'刷脸',userType:'系统用户',total:'462.0g',weight:'+462.0g'},
+ {start:'2026-08-25 08:13:47.889',end:'2026-08-25 08:14:10.540',status:'离开',userType:'系统用户',total:'0.0g',weight:'-445.8g'},
+ {start:'2026-08-25 08:12:09.310',end:'2026-08-25 08:12:30.775',status:'刷脸',userType:'陌生人',total:'0.0g',weight:'+0.0g'}
+];
+
+function renderScaleFace(){
+ const statusTagMap={
+ '刷脸':'刷脸',
+ '离开':'离开',
+ '刷脸+离开':'刷脸+离开'
+ };
+ const rows=getFilteredScaleRows('face').map((f)=>{
+ const statusTag=statusTagMap[f.status]||''+escapeHtml(f.status)+'';
+ const isPos=f.weight.startsWith('+');
+ const weightHtml=`${f.weight}`;
+ return `| ${f.start} | ${f.end} | ${statusTag} | ${weightHtml} | ${f.total} | 回放视频 |
`;
+ }).join('');
+ return `人脸识别数据
+ ${scaleThead('face','操作')}${rows}
`;
+}
+
+/* 录像回放弹窗:自动播放 + 快进/后退 + 进度条跳转 */
+var FACE_VIDEO={playing:false,current:0,duration:30,timer:null};
+
+function showFaceVideo(start){
+ const f=SCALE_FACE_RECORDS.find(x=>x.start===start);
+ if(!f)return;
+ FACE_VIDEO.current=0;
+ FACE_VIDEO.duration=30;
+ FACE_VIDEO.playing=true;
+ const mask=document.createElement('div');
+ mask.className='face-video-mask';
+ mask.id='faceVideoMask';
+ mask.innerHTML=`
+
+
+
+
● REC
+
+
▶
+
CAM-01${f.start}
+
+
+
+
+
+
+
00:00 / 00:30
+
+
+
识别时间${f.start}
+
状态${f.status}
+
用户类型${f.userType}
+
重量变化${f.weight}
+
+
+
`;
+ mask.addEventListener('click',e=>{if(e.target===mask)closeFaceVideo();});
+ document.body.appendChild(mask);
+ updateFaceVideoUI();
+ startFaceVideoTimer();
+}
+function closeFaceVideo(){
+ clearInterval(FACE_VIDEO.timer);
+ FACE_VIDEO.playing=false;
+ const m=document.getElementById('faceVideoMask');
+ if(!m)return;
+ m.classList.add('closing');
+ setTimeout(()=>m.remove(),200);
+}
+function startFaceVideoTimer(){
+ clearInterval(FACE_VIDEO.timer);
+ FACE_VIDEO.timer=setInterval(()=>{
+ if(!FACE_VIDEO.playing)return;
+ FACE_VIDEO.current=Math.min(FACE_VIDEO.duration,FACE_VIDEO.current+0.1);
+ if(FACE_VIDEO.current>=FACE_VIDEO.duration){
+ FACE_VIDEO.playing=false;
+ clearInterval(FACE_VIDEO.timer);
+ }
+ updateFaceVideoUI();
+ },100);
+}
+function faceVideoPlayPause(){
+ FACE_VIDEO.playing=!FACE_VIDEO.playing;
+ if(FACE_VIDEO.playing){
+ if(FACE_VIDEO.current>=FACE_VIDEO.duration)FACE_VIDEO.current=0;
+ startFaceVideoTimer();
+ }
+ updateFaceVideoUI();
+}
+function faceVideoSeek(delta){
+ FACE_VIDEO.current=Math.max(0,Math.min(FACE_VIDEO.duration,FACE_VIDEO.current+delta));
+ updateFaceVideoUI();
+}
+function faceVideoSeekTo(e){
+ const bar=document.getElementById('faceVideoBar');
+ if(!bar)return;
+ const rect=bar.getBoundingClientRect();
+ const pct=Math.max(0,Math.min(1,(e.clientX-rect.left)/rect.width));
+ FACE_VIDEO.current=pct*FACE_VIDEO.duration;
+ updateFaceVideoUI();
+}
+function updateFaceVideoUI(){
+ const progress=document.getElementById('faceVideoProgress');
+ const time=document.getElementById('faceVideoTime');
+ const btn=document.getElementById('faceVideoPlayBtn');
+ const big=document.querySelector('#faceVideoMask .face-video-player__play');
+ const pct=FACE_VIDEO.current/FACE_VIDEO.duration*100;
+ if(progress)progress.style.width=pct+'%';
+ if(time)time.textContent=formatFaceVideoTime(FACE_VIDEO.current)+' / '+formatFaceVideoTime(FACE_VIDEO.duration);
+ const icon=FACE_VIDEO.playing?'⏸':'▶';
+ if(btn)btn.textContent=icon;
+ if(big)big.textContent=icon;
+}
+function formatFaceVideoTime(t){
+ const m=Math.floor(t/60);
+ const s=Math.floor(t%60);
+ return (m<10?'0'+m:m)+':'+(s<10?'0'+s:s);
+}
+
+/* 维度3:重量变化数据 — 开始/结束时间 / 识别对象 / 提交重量 / 总重量 */
+SCALE_WEIGHT_RECORDS=[
+ {start:'2026-08-25 08:15:22.701',end:'2026-08-25 08:15:30.210',target:'EMP0234 · 张伟',submit:'436.5',total:'486.5'},
+ {start:'2026-08-25 08:14:50.512',end:'2026-08-25 08:14:58.430',target:'无用户',submit:'412.0',total:'462.0'},
+ {start:'2026-08-25 08:13:46.098',end:'2026-08-25 08:13:55.118',target:'EMP0357 · 王强',submit:'—',total:'501.2'},
+ {start:'2026-08-25 08:12:08.876',end:'2026-08-25 08:12:18.775',target:'无用户',submit:'0.0',total:'0.0'}
+];
+function renderScaleWeight(){
+ const rows=getFilteredScaleRows('weight').map(w=>`| ${w.start} | ${w.end} | ${w.target} | ${w.submit} | ${w.total} |
`).join('');
+ return `重量变化数据
+ ${scaleThead('weight')}${rows}
`;
+}
+
+/* 维度4:运行日志 — 日期/时间 / 报错类型 / 报错内容(报错代码,20字符) / 操作 */
+SCALE_LOG_RECORDS=[
+ {time:'2026-08-25 08:10:12.330',type:'异常报错',code:'E1001_WEIGHT_SENSOR_READ_TIMEOUT_RETRY',detail:'E1001_WEIGHT_SENSOR_READ_TIMEOUT_RETRY\n\n[2026-08-25 08:10:12.330] ERROR WeightSensorModule::read()\n 称重传感器读取超时,连续 3 次无响应\n 设备编号: DEV-00025 传感器通道: CH-02\n 已触发自动重试,若重试失败将上报设备故障\n TraceId: 9f3c1a7b-2e4d-4c8a-b1f0-5d6e7f8a9b0c'},
+ {time:'2026-08-25 08:02:45.117',type:'性能预警',code:'W2003_RECOG_LATENCY_OVER_THRESHOLD_320MS',detail:'W2003_RECOG_LATENCY_OVER_THRESHOLD_320MS\n\n[2026-08-25 08:02:45.117] WARN RecognitionModule::detect()\n 人脸识别响应延迟 320ms,接近阈值 300ms\n 摄像头: CAM-01 帧率: 25fps\n 建议检查现场光照条件与网络带宽\n TraceId: 4b8e2c31-7a9d-4f5b-9c2e-1a3b4c5d6e7f'},
+ {time:'2026-08-25 07:58:03.662',type:'性能预警',code:'W2004_MEMORY_USAGE_HIGH_82PCT',detail:'W2004_MEMORY_USAGE_HIGH_82PCT\n\n[2026-08-25 07:58:03.662] WARN SystemMonitor::check()\n 内存占用 82%,接近预警阈值 85%\n 当前进程: nutrition-scale-app\n 建议关注内存泄漏或及时清理缓存\n TraceId: c5d6e7f8-2a3b-4c4d-8e9f-0a1b2c3d4e5f'},
+ {time:'2026-08-25 07:30:00.000',type:'运行日志',code:'I0000_SELF_CHECK_PASS',detail:'I0000_SELF_CHECK_PASS\n\n[2026-08-25 07:30:00.000] INFO SystemMonitor::selfCheck()\n 设备完成夜间自检,各项指标正常\n 传感器: 正常 摄像头: 正常 网络: 正常\n TraceId: 6f1a2b3c-4d5e-4f6a-8b7c-9d0e1f2a3b4c'}
+];
+function truncateCode(code){
+ if(!code)return '';
+ return code.length>20?code.slice(0,20)+'…':code;
+}
+function renderScaleLog(){
+ const rows=getFilteredScaleRows('log').map(l=>{
+ const codeHtml=`${escapeHtml(truncateCode(l.code))}`;
+ return `| ${l.time} | ${l.type} | ${codeHtml} | 查看详情 |
`;
+ }).join('');
+ return `运行日志
+ ${scaleThead('log','操作')}${rows}
`;
+}
+function showLogDetail(code){
+ var rec=SCALE_LOG_RECORDS.filter(function(l){return l.code===code;})[0];
+ if(!rec)return;
+ var html=''
+ +'
日期/时间
'+escapeHtml(rec.time)+'
'
+ +'
报错类型
'+escapeHtml(rec.type)+'
'
+ +'
报错代码
'+escapeHtml(rec.detail)+'
'
+ +'
';
+ DynModal.open('报错详情',html,'');
+}
+
/* ===== 智能货柜:柜子轮廓布局 + 储物状态(参考 prod-proc-cabinet.html 左侧柜子样式) ===== */
/* 格口 Mock:状态 occupied 储物中 / warning 临期预警 / alert 过期预警 / empty 空闲 */
var OV_SLOTS=[
diff --git a/web-admin/nutrition/doc/CHANGELOG.md b/web-admin/nutrition/doc/CHANGELOG.md
index 38e3df1..4a1b957 100644
--- a/web-admin/nutrition/doc/CHANGELOG.md
+++ b/web-admin/nutrition/doc/CHANGELOG.md
@@ -1,5 +1,20 @@
# 营养用餐管理端 CHANGELOG
+## [2026-08-27] feat: 订餐终端设备概览新增「餐线营养秤」Tab
+
+- `din-terminal.html` 订餐终端设备概览:
+ - 新增「餐线营养秤」Tab,四个维度子页切换(设备状态监控/重量变化数据/人脸识别数据/运行日志)
+ - 表头新增列筛选(▾ 图标下拉,按列去重取值,四个维度复用)
+ - 人脸识别数据支持「回放视频」:弹窗含自动播放/快进后退 10s/进度条跳转/识别信息
+ - 运行日志支持「查看详情」:报错内容超 20 字符截断,详情弹窗展示完整报错日志(含 TraceId)
+
+## [2026-08-26] feat: 净菜订单新增生成历史 + 生成方式列 + 详情就餐人数
+
+- `prod-clean-order.html` 净菜订单:
+ - 新增「净菜订单生成历史」按钮与弹窗(食谱日期/预定配送时间/餐次/内配调料四筛选 + 9 列表格),由食谱创建订单时自动写入生成历史
+ - 主列表「订单类型」前新增「生成方式」列(手动/自动),预置自动订单 CO-009 示例
+ - 详情抽屉订单明细:仅自动生成的订单展示「就餐人数」列
+
## [2026-08-25] feat: 餐线设置新增用户范围必填复选 + 食谱订单餐品明细表份数调整
- `din-canteen-line.html` 餐线管理:
diff --git a/web-admin/nutrition/doc/角色场景流程图.md b/web-admin/nutrition/doc/角色场景流程图.md
new file mode 100644
index 0000000..c96b7ca
--- /dev/null
+++ b/web-admin/nutrition/doc/角色场景流程图.md
@@ -0,0 +1,159 @@
+# 营养用餐模块 - 角色泳道图 & 问题汇总
+
+> 横轴:角色(平台 → 加工商 → 场所 → 用户)
+> 纵轴:场景流程
+> 标注:每个场景下方附现有问题
+
+---
+
+## 一、角色场景泳道图
+
+```mermaid
+flowchart TD
+ subgraph 平台["🏛️ 平台(Platform)"]
+ direction TB
+
+ subgraph 准备["阶段一:平台准备"]
+ S1["场景1:食物成分录入
──────────────
营养用餐 → 食物成分
→ 分类管理:新增多级分类
→ 点击新增食物
→ 录入食物成分信息
→ 保存"]
+ S2["场景2:餐品总库
──────────────
营养用餐 → 餐品总库
→ 新增餐品分类
→ 新增餐品
→ 保存"]
+ end
+
+ subgraph 场所管理["阶段二:场所管理"]
+ S3["场景3:新增场所
──────────────
营养用餐 → 场所管理
→ 新增场所
→ 录入场所信息
→ 保存 → 场所创建完成
→ 主账号发放"]
+ S4["场景4:餐线管理
──────────────
场所管理 → 选择场所
→ 餐线管理
→ 绑定设备"]
+ end
+
+ 准备 --> 场所管理
+ end
+
+ subgraph 加工商["🏭 加工商(Processor)"]
+ P_EMPTY["(待补充)"]
+ end
+
+ subgraph 场所["📍 场所(Venue)"]
+ V_EMPTY["(待补充)"]
+ end
+
+ subgraph 用户["👤 用户(User)"]
+ U_EMPTY["(待补充)"]
+ end
+
+ 平台 --> 加工商 --> 场所 --> 用户
+
+ %% 平台准备 - 食物成分问题
+ S1 -.->|"⚠️"| Q1["⑥ 缺少「食物功效」模块"]
+ S1 -.->|"⚠️"| Q2["⑦ 味道档案应改为
酸甜苦辣咸下拉菜单"]
+ S1 -.->|"⚠️"| Q3["⑧ 分类应内置
中国食物成分表标准分类"]
+ S1 -.->|"⚠️"| Q4["⑨ 口感和色泽字段不需要"]
+ S1 -.->|"⚠️"| Q5["⑩ 宏量营养素应为必填项"]
+ S1 -.->|"⚠️"| Q6["⑪ 食物类目启禁用
应展示在列表中"]
+ S1 -.->|"⚠️"| Q7["⑫ 食物缺少启禁用状态"]
+
+ %% 平台准备 - 餐品总库问题
+ S2 -.->|"⚠️"| Q8["⑬ 餐品分类无法维护
(增删改查)"]
+ S2 -.->|"⚠️"| Q9["⑭ 餐品分类无法启禁用"]
+ S2 -.->|"⚠️"| Q10["⑮ 基本档案中适用餐次、
过敏原建议删除"]
+ S2 -.->|"⚠️"| Q11["⑯ 烹饪工艺/味道档案/
口感档案/色泽档案
建议删除"]
+ S2 -.->|"⚠️"| Q12["⑰ 营养指标模块
应按2.0版本做"]
+
+ %% 场所管理 - 新增场所问题
+ S3 -.->|"⚠️"| Q13["① 负责人和负责人电话
应为必填项"]
+ S3 -.->|"⚠️"| Q14["② 「是否使用三减终端」
含义不明确"]
+ S3 -.->|"⚠️"| Q15["③ 详细地址应为必填项"]
+ S3 -.->|"⚠️"| Q16["④ 场所主账号应
发送到负责人手机"]
+ S3 -.->|"⚠️"| Q17["⑤ 场所管理缺少
启禁用按钮"]
+
+ %% 场所管理 - 餐线管理问题
+ S4 -.->|"⚠️"| Q18["⑥ 绑定设备数量不反显"]
+ S4 -.->|"⚠️"| Q19["⑦ 期望有默认餐线"]
+```
+
+---
+
+## 二、泳道图(简化文字版)
+
+```
+┌──────────────┬──────────────┬──────────────┬──────────────┐
+│ 平台 │ 加工商 │ 场所 │ 用户 │
+├──────────────┼──────────────┼──────────────┼──────────────┤
+│ │ │ │ │
+│ 【平台准备】 │ │ │ │
+│ │ │ │ │
+│ 1.食物成分 │ │ │ │
+│ 分类管理 │ │ │ │
+│ 新增食物 │ │ │ │
+│ │ │ │ │
+│ 2.餐品总库 │ │ │ │
+│ 餐品分类 │ │ │ │
+│ 新增餐品 │ │ │ │
+│ │ │ │ │
+│ 【场所管理】 │ │ │ │
+│ │ │ │ │
+│ 3.新增场所 │ │ │ │
+│ 录入信息 │ │ │ │
+│ 主账号发放 │ │ │ │
+│ │ │ │ │
+│ 4.餐线管理 │ │ │ │
+│ 绑定设备 │ │ │ │
+│ │ │ │ │
+├──────────────┼──────────────┼──────────────┼──────────────┤
+│ │ (待补充) │ (待补充) │ (待补充) │
+└──────────────┴──────────────┴──────────────┴──────────────┘
+```
+
+---
+
+## 三、问题汇总表
+
+### 平台准备 → 食物成分(#⑥ ~ #⑫)
+
+| # | 问题 | 期望 |
+|---|------|------|
+| ⑥ | 缺少「食物功效」模块 | 新增模块,后期用于匹配模型自动关联用户调理方案 |
+| ⑦ | 味道档案含主观填写字段(口味描述、辅味型、味道强度、饮食禁忌等) | 改为**酸甜苦辣咸**下拉菜单,去掉其余主观字段 |
+| ⑧ | 分类需手动新建 | 内置**中国食物成分表**标准分类 |
+| ⑨ | 有「口感」和「色泽」字段 | 去掉,不需要 |
+| ⑩ | 宏量营养素非必填 | 改为**必填项** |
+| ⑪ | 食物类目启禁用在编辑页内,操作不便 | 启禁用展示在**列表中**;禁用类目时,关联餐品**全部下架** |
+| ⑫ | 食物缺少启禁用状态 | 新增启禁用,逻辑同类目:禁用食物时关联餐品全部下架 |
+
+### 平台准备 → 餐品总库(#⑬ ~ #⑰)
+
+| # | 问题 | 期望 |
+|---|------|------|
+| ⑬ | 餐品分类无法维护(增删改查) | 支持餐品分类的增删改查 |
+| ⑭ | 餐品分类无法启禁用 | 新增启禁用功能 |
+| ⑮ | 基本档案中「适用餐次」「过敏原」多余 | 删除适用餐次、过敏原字段 |
+| ⑯ | 烹饪工艺、味道档案、口感档案、色泽档案模块多余 | 全部删除 |
+| ⑰ | 营养指标模块不符合 2.0 版本规范 | 按 2.0 版本重新设计 |
+
+### 场所管理 → 新增场所(#① ~ #⑤)
+
+| # | 问题 | 期望 |
+|---|------|------|
+| ① | 负责人和负责人电话非必填 | 改为**必填项** |
+| ② | 「是否使用三减终端」含义不明确 | 加字段说明或提示文案 |
+| ③ | 详细地址非必填 | 改为**必填项** |
+| ④ | 场所主账号发放逻辑不明确 | 创建场所后主账号**发送到负责人手机** |
+| ⑤ | 场所管理缺少启禁用按钮 | 新增启禁用按钮 |
+
+### 场所管理 → 餐线管理(#⑥ ~ #⑦)
+
+| # | 问题 | 期望 |
+|---|------|------|
+| ⑥ | 绑定设备后数量不反显 | 绑定后实时显示已绑设备数量 |
+| ⑦ | 没有默认餐线 | 创建场所后自动生成一条默认餐线 |
+
+---
+
+## 四、汇总统计
+
+| 角色 | 阶段 | 场景数 | 问题数 | 状态 |
+|------|------|--------|--------|------|
+| 平台 | 平台准备 | 2(食物成分、餐品总库) | 12 | ✅ 已梳理 |
+| 平台 | 场所管理 | 2(新增场所、餐线管理) | 7 | ✅ 已梳理 |
+| **平台小计** | — | **4** | **19** | — |
+| 加工商 | — | 0 | 0 | ⬜ 待补充 |
+| 场所 | — | 0 | 0 | ⬜ 待补充 |
+| 用户 | — | 0 | 0 | ⬜ 待补充 |
+| **总计** | — | **4** | **19** | — |
diff --git a/web-admin/nutrition/prod-clean-order.html b/web-admin/nutrition/prod-clean-order.html
index 7d8054e..0351c34 100644
--- a/web-admin/nutrition/prod-clean-order.html
+++ b/web-admin/nutrition/prod-clean-order.html
@@ -268,17 +268,18 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
+
- | 订单号 | 订单类型 | 净菜类型 | 菜品种类 | 总份数 | 总计量(kg) | 订单金额 | 订单状态 | 申领时间 | 预定配送时间 | 创建人 | 物流信息 | 操作 |
+ | 订单号 | 生成方式 | 订单类型 | 净菜类型 | 菜品种类 | 总份数 | 总计量(kg) | 订单金额 | 订单状态 | 申领时间 | 预定配送时间 | 创建人 | 物流信息 | 操作 |
@@ -397,6 +398,34 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
+
+
+
+
+
+
+
+
+
+
+ | 食谱日期 | 餐次 | 餐品名称 | 就餐人数 | 内配料(是否) | 打包份数 | 预定配送时间 | 开始生成时间 | 生成成功时间 |
+
+
+
+
暂无符合条件的生成记录
+
+
+
+
+