feat: 餐次管理模块新增 — 开餐/补餐/闭餐/余量处置/历史四页联动

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-10 18:36:42 +08:00
co-authored by Claude Opus 4.7
parent ae9acce200
commit ff48f38dbf
12 changed files with 3652 additions and 13 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
_Last synced: 2026-08-10 | Base commit: `deb4752`_
_Last synced: 2026-08-10 | Base commit: `32bd018`_
# 记忆索引
| 文件 | 描述 | 类型 | 最后更新 |
+25
View File
@@ -150,3 +150,28 @@ body {
- **日期**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 -7
View File
@@ -7,7 +7,7 @@ last_updated: 2026-08-10
# 记忆健康检查报告
> _执行时间: 2026-08-10 | Base commit: `deb4752` | Last synced: 2026-08-10_
> _执行时间: 2026-08-10 | Base commit: `32bd018` | Last synced: 2026-08-10_
## 健康概览
@@ -16,19 +16,21 @@ last_updated: 2026-08-10
| 孤儿检测 | ✅ | 0 | — |
| 幽灵检测 | ✅ | 0 | — |
| 引用存在性检测(3A) | ✅ | 0 | 0 |
| 双链对称性检测(3B) | ✅ | 1 | 0 |
| 双链对称性检测(3B) | ✅ | 0 | 0 |
| 内容矛盾检测 | ✅ | — | 0 |
| 过期检测 | ⚠ | — | 3 待处理 |
| 可推断内容污染 | ⚠ | — | 1 待处理 |
| frontmatter 完整性 | ⚠ | — | 4 待处理 |
**AUTO-FIX 已执行 1 项 | NEED-HUMAN 待处理 8 项**
**AUTO-FIX 已执行 0 项 | NEED-HUMAN 待处理 8 项**
> 本次为 2026-08-10 第二批次 sync 后的复查。新增 1 条决策(待机页副屏人脸识别监控区)+ 1 条里程碑条目,交叉引用闭环验证通过,无 AUTO-FIX 需执行;前期遗留的 8 项 NEED-HUMAN 维持原判定(结构性稳定内容/历史污染/统一性可选)。
---
## AUTO-FIX 已执行清单
- [x] 双链对称性补全:`project_progress.md` 2026-08-10 里程碑条目末尾追加反向引用 `[[decisions.md#收银设备用户类型徽标三态体系]]``[[decisions.md#补齐明细页业务路径修正:少记来源 = 未识别取餐记录]]`,闭环 decisions → progress 的两条新增决策引用
_本次无 AUTO-FIX 执行项(所有结构性检查通过)。_
---
@@ -54,7 +56,7 @@ last_updated: 2026-08-10
- **文件**`project_progress.md` 第 109~168 行「近期里程碑」区段(历史条目)
- **问题**:历史里程碑条目仍含大量具体文件路径(如 `din-ingredient.html``prod-clean-order``canteen-nutrition-scale/02-dish-display.html`)与 commit hash`445d824``d00208a``4fd44f1` 等),按 memory-update 规则「Git 历史(可用 git log 查)」不应写入记忆文件
- **本次执行**:新增的 2026-08-10、2026-08-07 两条里程碑仍保留文件名(属变更范围说明,便于跨模块追溯,未全量罗列),历史 110~168 行污染条目未清理
- **本次执行**:新增的 2026-08-10 两条里程碑仍保留文件名(属变更范围说明,便于跨模块追溯,未全量罗列),历史 110~168 行污染条目未清理
- **建议**:人工审查 110~168 行历史里程碑,保留结论性描述、删除具体文件路径与 commit hash,让用户能通过 `git log --since=2026-07` 自查细节
### [WARN] frontmatter 缺失 — 4 个文件
@@ -74,5 +76,6 @@ last_updated: 2026-08-10
- `decisions.md#餐厅终端设备 PAD 居中外壳设计模式``[[feedback.md#PAD 终端页面内禁止可见滚动条]]` ✅ 目标存在,双向闭环
- `feedback.md#PAD 终端页面内禁止可见滚动条``[[decisions.md#餐厅终端设备 PAD 居中外壳设计模式]]` ✅ 目标存在,双向闭环
- `decisions.md#规格体系统一化改造(跨设备端 + Web 端 + 辅助页)``[[feedback.md]]` ✅ 目标存在(占位引用「暂无直接关联条目」,非对称但属预期)
- `decisions.md#收银设备用户类型徽标三态体系``[[project_progress.md]]` ✅ 目标存在,次 AUTO-FIX 后双向闭环
- `decisions.md#补齐明细页业务路径修正:少记来源 = 未识别取餐记录``[[project_progress.md]]` ✅ 目标存在,次 AUTO-FIX 后双向闭环
- `decisions.md#收银设备用户类型徽标三态体系``[[project_progress.md]]` ✅ 目标存在,双向闭环(前次 AUTO-FIX 完成)
- `decisions.md#补齐明细页业务路径修正:少记来源 = 未识别取餐记录``[[project_progress.md]]` ✅ 目标存在,双向闭环(前次 AUTO-FIX 完成)
- `decisions.md#待机页副屏人脸识别监控区(收银员侧实时反馈)``[[project_progress.md]]` ✅ 目标存在,双向闭环(本次 memory-update 同步建立)
+2 -1
View File
@@ -3,7 +3,7 @@ name: 项目进度
description: 各模块原型开发进度与里程碑
type: project
last_updated: 2026-08-10
commit: deb4752
commit: 32bd018
---
# 项目进度
@@ -106,6 +106,7 @@ Web 管理端和员工端 APP 的其余模块(系统管理、运动管理、
- 覆盖模块:18 个(含三端+其他终端)
## 近期里程碑
- 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() 兜底逻辑、菜品展示与单屏取餐页样式微调。
+1 -1
View File
@@ -1,4 +1,4 @@
<!-- Last updated: 2026-08-10 | Commit: deb4752 -->
<!-- Last updated: 2026-08-10 | Commit: 32bd018 -->
# 项目约束
本项目所有工作必须严格遵守 `项目背景.md` 中定义的全部规范,包括:
@@ -585,11 +585,11 @@
<div class="entry-card__desc">今日数据可视化</div>
</div>
</a>
<a class="entry-card" href="javascript:void(0)" onclick="alert('用餐管理功能待开发')">
<a class="entry-card" href="10-meal-center.html">
<div class="entry-card__icon entry-card__icon--gold">&#x1F37D;&#xFE0F;</div>
<div class="entry-card__text">
<div class="entry-card__title">餐管理</div>
<div class="entry-card__desc">用餐记录/餐次管理</div>
<div class="entry-card__title">管理</div>
<div class="entry-card__desc">开餐/结束用餐/历史记录</div>
</div>
</a>
<a class="entry-card" href="javascript:void(0)" onclick="alert('设置功能待开发')">
@@ -0,0 +1,343 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>结算设备 - 餐次管理 | 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: #05080f;
color: #fff;
min-height: 100vh;
min-width: 1280px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.device-frame {
width: 1280px;
height: 720px;
background: #0e1326;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.04);
position: relative;
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.top-bar {
display: flex;
align-items: center;
height: 44px;
background: #1a2b4a;
padding: 0 20px;
flex-shrink: 0;
}
.back-btn {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
.top-bar__title {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
}
.top-bar__meta {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
font-family: Menlo, Consolas, monospace;
}
/* 主内容区 */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
padding: 28px 56px;
overflow: hidden;
}
/* 顶部餐次状态摘要 */
.meal-summary {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 20px;
background: rgba(26, 43, 74, 0.5);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
margin-bottom: 24px;
}
.meal-summary__icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(74, 144, 217, 0.18);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.meal-summary__body { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.meal-summary__title {
font-size: 14px;
font-weight: 600;
color: #fff;
}
.meal-summary__sub {
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
}
.meal-summary__time {
font-size: 22px;
font-weight: 600;
font-family: Menlo, Consolas, monospace;
color: #4ade80;
letter-spacing: 1px;
}
/* 导航卡片容器 */
.nav-cards {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
min-height: 0;
}
/* 导航卡片 */
.nav-card {
display: flex;
flex-direction: column;
align-items: center;
width: 260px;
padding: 32px 24px 24px;
border-radius: 16px;
text-decoration: none;
cursor: pointer;
transition: all 0.25s;
border: 1px solid;
text-align: center;
}
.nav-card:hover {
transform: translateY(-3px);
}
/* 开餐开关卡(绿色主调) */
.nav-card--active {
background: linear-gradient(180deg, rgba(74, 222, 128, 0.12) 0%, rgba(26, 43, 74, 0.4) 100%);
border-color: rgba(74, 222, 128, 0.35);
}
.nav-card--active:hover {
background: linear-gradient(180deg, rgba(74, 222, 128, 0.2) 0%, rgba(26, 43, 74, 0.55) 100%);
border-color: rgba(74, 222, 128, 0.55);
box-shadow: 0 8px 24px rgba(74, 222, 128, 0.15);
}
/* 历史记录卡(蓝色主调) */
.nav-card--history {
background: linear-gradient(180deg, rgba(74, 144, 217, 0.12) 0%, rgba(26, 43, 74, 0.4) 100%);
border-color: rgba(74, 144, 217, 0.35);
}
.nav-card--history:hover {
background: linear-gradient(180deg, rgba(74, 144, 217, 0.2) 0%, rgba(26, 43, 74, 0.55) 100%);
border-color: rgba(74, 144, 217, 0.55);
box-shadow: 0 8px 24px rgba(74, 144, 217, 0.15);
}
.nav-card__icon {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
margin-bottom: 16px;
}
.nav-card--active .nav-card__icon {
background: rgba(74, 222, 128, 0.2);
}
.nav-card--history .nav-card__icon {
background: rgba(74, 144, 217, 0.2);
}
.nav-card__title {
font-size: 18px;
font-weight: 700;
color: #fff;
margin-bottom: 6px;
}
.nav-card__desc {
font-size: 12px;
color: rgba(255, 255, 255, 0.55);
line-height: 1.6;
margin-bottom: 16px;
}
/* 状态标签 */
.nav-card__pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.nav-card__pill--live {
background: rgba(74, 222, 128, 0.18);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.35);
}
.nav-card__pill--count {
background: rgba(74, 144, 217, 0.18);
color: #60a5fa;
border: 1px solid rgba(74, 144, 217, 0.35);
}
.nav-card__pill .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
animation: dotBlink 1.4s infinite;
}
@keyframes dotBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* Toast 提示条 */
.toast {
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
padding: 10px 22px;
background: rgba(74, 222, 128, 0.95);
color: #0e1326;
border-radius: 22px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 8px 24px rgba(74, 222, 128, 0.3);
opacity: 0;
pointer-events: none;
transition: all 0.3s;
z-index: 100;
}
.toast--show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<div class="device-frame">
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="02-standby.html">&#x25C0; 返回待机</a>
<span class="top-bar__title">餐次管理</span>
<span class="top-bar__meta" id="topMeta">2026-08-10 周一</span>
</div>
<!-- Toast 提示 -->
<div class="toast" id="toast">余量处置已提交 ✓</div>
<!-- 主内容 -->
<div class="main-area">
<!-- 当前餐次状态摘要 -->
<div class="meal-summary">
<div class="meal-summary__icon">&#x1F96A;</div>
<div class="meal-summary__body">
<div class="meal-summary__title">当前餐次 · 午餐</div>
<div class="meal-summary__sub">数味餐厅 · 一楼 | 共 3 条餐线 · 28 道菜</div>
</div>
<div class="meal-summary__time" id="liveClock">12:30:45</div>
</div>
<!-- 导航卡片区 -->
<div class="nav-cards">
<!-- 餐次开关 -->
<a class="nav-card nav-card--active" href="11-meal-active.html?meal=lunch">
<div class="nav-card__icon">&#x1F50A;</div>
<div class="nav-card__title">餐次开关</div>
<div class="nav-card__desc">查看开餐信息 · 餐品余量<br>通知补餐 · 结束用餐</div>
<span class="nav-card__pill nav-card__pill--live">
<span class="dot"></span> 开餐中
</span>
</a>
<!-- 历史记录 -->
<a class="nav-card nav-card--history" href="13-meal-history.html">
<div class="nav-card__icon">&#x1F4DC;</div>
<div class="nav-card__title">历史记录</div>
<div class="nav-card__desc">查看餐次开餐/补餐<br>闭餐/余量处置流程 · 点击查看明细</div>
<span class="nav-card__pill nav-card__pill--count">今日 2 餐次</span>
</a>
</div>
</div>
</div>
<script>
/* ============ 实时时钟(只显示时分秒) ============ */
function updateClock() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, '0');
var mm = String(now.getMinutes()).padStart(2, '0');
var ss = String(now.getSeconds()).padStart(2, '0');
document.getElementById('liveClock').textContent = hh + ':' + mm + ':' + ss;
/* 右上角元信息:年月日 + 周几 */
var year = now.getFullYear();
var month = String(now.getMonth() + 1).padStart(2, '0');
var date = String(now.getDate()).padStart(2, '0');
var weekDay = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.getDay()];
document.getElementById('topMeta').textContent = year + '-' + month + '-' + date + ' ' + weekDay;
}
updateClock();
setInterval(updateClock, 1000);
/* ============ URL 参数驱动的 Toast 提示 ============
场景:从 12-meal-end-surplus.html 提交余量处置后跳回本页时
携带 ?toast=submitted 参数,本页解析后显示"余量处置已提交"提示
*/
(function () {
var params = new URLSearchParams(window.location.search);
var toastKey = params.get('toast');
if (toastKey === 'submitted') {
var toast = document.getElementById('toast');
setTimeout(function () {
toast.classList.add('toast--show');
setTimeout(function () {
toast.classList.remove('toast--show');
}, 2400);
}, 400);
}
/* 清除 URL 中的 toast 参数,避免刷新重复触发 */
if (toastKey) {
history.replaceState(null, '', window.location.pathname);
}
})();
</script>
</body>
</html>
@@ -0,0 +1,714 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>结算设备 - 开餐中详情 | 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: #05080f;
color: #fff;
min-height: 100vh;
min-width: 1280px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.device-frame {
width: 1280px;
height: 720px;
background: #0e1326;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.04);
position: relative;
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.top-bar {
display: flex;
align-items: center;
height: 44px;
background: #1a2b4a;
padding: 0 20px;
flex-shrink: 0;
}
.back-btn {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
.top-bar__title {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
}
.top-bar__meta {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
font-family: Menlo, Consolas, monospace;
}
/* 主内容区 */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px 28px 18px;
overflow: hidden;
}
/* 开餐信息条 */
.meal-info-bar {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 18px;
background: linear-gradient(135deg, rgba(74, 222, 128, 0.12) 0%, rgba(26, 43, 74, 0.6) 100%);
border: 1px solid rgba(74, 222, 128, 0.3);
border-radius: 14px;
margin-bottom: 14px;
}
.meal-info-bar__icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(74, 222, 128, 0.22);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.meal-info-bar__body {
flex: 1;
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
}
.meal-info-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.meal-info-item__label {
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
}
.meal-info-item__value {
font-size: 13px;
color: #fff;
font-weight: 600;
}
.meal-info-item__value--green { color: #4ade80; }
.meal-info-item__value--gold { color: #fbbf24; }
.meal-info-divider {
width: 1px;
height: 28px;
background: rgba(255, 255, 255, 0.1);
}
.meal-info-bar__live {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: rgba(74, 222, 128, 0.18);
border: 1px solid rgba(74, 222, 128, 0.4);
border-radius: 14px;
font-size: 12px;
font-weight: 600;
color: #4ade80;
}
.meal-info-bar__live::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: #4ade80;
animation: dotBlink 1.4s infinite;
}
@keyframes dotBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* 餐线 Tab 切换 */
.line-tabs {
display: flex;
gap: 8px;
margin-bottom: 14px;
}
.line-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: rgba(26, 43, 74, 0.5);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 18px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.2s;
}
.line-tab:hover {
background: rgba(74, 144, 217, 0.12);
color: #fff;
}
.line-tab.is-active {
background: linear-gradient(135deg, rgba(74, 144, 217, 0.35), rgba(74, 144, 217, 0.18));
border-color: rgba(74, 144, 217, 0.6);
color: #fff;
font-weight: 600;
}
.line-tab__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 16px;
padding: 0 5px;
background: rgba(255, 255, 255, 0.12);
border-radius: 8px;
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
}
.line-tab.is-active .line-tab__count {
background: rgba(74, 144, 217, 0.45);
color: #fff;
}
/* 餐品列表区(可滚动但隐藏滚动条) */
.dish-list-wrap {
flex: 1;
overflow-y: auto;
scrollbar-width: none;
padding-right: 4px;
margin-right: -4px;
}
.dish-list-wrap::-webkit-scrollbar { display: none; }
.dish-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
/* 餐品卡 */
.dish-card {
display: flex;
gap: 12px;
padding: 12px;
background: rgba(26, 43, 74, 0.5);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
transition: all 0.2s;
}
.dish-card:hover {
background: rgba(74, 144, 217, 0.08);
border-color: rgba(74, 144, 217, 0.3);
}
.dish-card__img {
width: 64px;
height: 64px;
border-radius: 10px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
background: linear-gradient(135deg, rgba(74, 144, 217, 0.15), rgba(26, 43, 74, 0.6));
border: 1px solid rgba(255, 255, 255, 0.06);
}
.dish-card__body {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.dish-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.dish-card__name {
font-size: 13px;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dish-card__tag {
font-size: 9px;
padding: 2px 6px;
border-radius: 6px;
background: rgba(74, 144, 217, 0.18);
color: #60a5fa;
flex-shrink: 0;
}
.dish-card__metrics {
display: flex;
gap: 10px;
margin-top: 2px;
}
.metric {
display: flex;
align-items: baseline;
gap: 2px;
font-size: 10px;
}
.metric__label {
color: rgba(255, 255, 255, 0.45);
}
.metric__value {
font-size: 12px;
font-weight: 700;
color: #fff;
}
.metric__value--red { color: #ef4444; }
.metric__value--green { color: #4ade80; }
.metric__value--gold { color: #fbbf24; }
/* 余量进度条(可视化余量比例) */
.stock-bar {
height: 4px;
background: rgba(255, 255, 255, 0.08);
border-radius: 2px;
margin-top: 4px;
overflow: hidden;
}
.stock-bar__fill {
height: 100%;
border-radius: 2px;
transition: width 0.4s;
}
.stock-bar__fill--ok { background: linear-gradient(90deg, #4ade80, #22c55e); }
.stock-bar__fill--mid { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
.stock-bar__fill--low { background: linear-gradient(90deg, #ef4444, #dc2626); }
/* 操作按钮(通知补餐) */
.dish-card__action {
display: flex;
gap: 6px;
margin-top: 6px;
}
.dish-btn {
flex: 1;
padding: 5px 8px;
background: rgba(251, 191, 36, 0.15);
border: 1px solid rgba(251, 191, 36, 0.35);
border-radius: 6px;
color: #fbbf24;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 3px;
}
.dish-btn:hover {
background: rgba(251, 191, 36, 0.28);
border-color: rgba(251, 191, 36, 0.55);
}
.dish-btn.is-done {
background: rgba(74, 222, 128, 0.18);
border-color: rgba(74, 222, 128, 0.4);
color: #4ade80;
cursor: default;
}
/* 底部固定操作区 */
.footer-actions {
display: flex;
gap: 14px;
padding-top: 14px;
margin-top: 14px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.footer-btn {
flex: 1;
height: 50px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: 1px solid;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
text-decoration: none;
}
.footer-btn--secondary {
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.75);
}
.footer-btn--secondary:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.footer-btn--danger {
background: linear-gradient(135deg, #ef4444 0%, #f97316 100%);
border-color: #ef4444;
color: #fff;
box-shadow: 0 6px 18px rgba(239, 68, 68, 0.35);
}
.footer-btn--danger:hover {
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
box-shadow: 0 8px 22px rgba(239, 68, 68, 0.5);
}
/* 二次确认对话框 */
.modal-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(5, 8, 15, 0.75);
backdrop-filter: blur(6px);
z-index: 50;
display: none;
align-items: center;
justify-content: center;
}
.modal-overlay.is-show { display: flex; }
.modal {
width: 460px;
background: #1a2b4a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 24px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
}
.modal__title {
font-size: 17px;
font-weight: 700;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.modal__desc {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.6;
margin-bottom: 18px;
}
.modal__desc strong { color: #ef4444; }
.modal__actions {
display: flex;
gap: 10px;
}
.modal__btn {
flex: 1;
height: 38px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1px solid;
transition: all 0.2s;
}
.modal__btn--cancel {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.75);
}
.modal__btn--cancel:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
.modal__btn--confirm {
background: linear-gradient(135deg, #ef4444, #f97316);
border-color: #ef4444;
color: #fff;
}
/* Toast */
.toast {
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
padding: 10px 22px;
background: rgba(251, 191, 36, 0.95);
color: #0e1326;
border-radius: 22px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 8px 24px rgba(251, 191, 36, 0.3);
opacity: 0;
pointer-events: none;
transition: all 0.3s;
z-index: 60;
}
.toast--show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<div class="device-frame">
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="10-meal-center.html">&#x25C0; 返回餐次管理</a>
<span class="top-bar__title">开餐中 · <span id="mealTitle">午餐</span></span>
<span class="top-bar__meta" id="topMeta">2026-08-10 周一</span>
</div>
<!-- Toast -->
<div class="toast" id="toast">已通知后厨补做宫保鸡丁 📢</div>
<!-- 主内容 -->
<div class="main-area">
<!-- 开餐信息条 -->
<div class="meal-info-bar">
<div class="meal-info-bar__icon">&#x1F50A;</div>
<div class="meal-info-bar__body">
<div class="meal-info-item">
<span class="meal-info-item__label">操作人</span>
<span class="meal-info-item__value">张三 · CS001</span>
</div>
<div class="meal-info-divider"></div>
<div class="meal-info-item">
<span class="meal-info-item__label">餐厅</span>
<span class="meal-info-item__value">数味餐厅 · 一楼</span>
</div>
<div class="meal-info-divider"></div>
<div class="meal-info-item">
<span class="meal-info-item__label">开餐时间</span>
<span class="meal-info-item__value">11:00:23</span>
</div>
<div class="meal-info-divider"></div>
<div class="meal-info-item">
<span class="meal-info-item__label">已开餐</span>
<span class="meal-info-item__value meal-info-item__value--green" id="elapsed">32 分</span>
</div>
<div class="meal-info-divider"></div>
<div class="meal-info-item">
<span class="meal-info-item__label">本餐次订单</span>
<span class="meal-info-item__value meal-info-item__value--gold">96 单</span>
</div>
</div>
<span class="meal-info-bar__live">开餐中</span>
</div>
<!-- 餐线 Tab -->
<div class="line-tabs" id="lineTabs">
<span class="line-tab is-active" data-line="A" onclick="switchLine(this)">A 区中餐线 <span class="line-tab__count">12</span></span>
<span class="line-tab" data-line="B" onclick="switchLine(this)">B 区中餐线 <span class="line-tab__count">9</span></span>
<span class="line-tab" data-line="C" onclick="switchLine(this)">C 区快餐线 <span class="line-tab__count">7</span></span>
</div>
<!-- 餐品列表 -->
<div class="dish-list-wrap">
<div class="dish-grid" id="dishGrid">
<!-- JS 动态渲染 -->
</div>
</div>
<!-- 底部固定操作 -->
<div class="footer-actions">
<a class="footer-btn footer-btn--secondary" href="10-meal-center.html">&#x25C0; 返回</a>
<button class="footer-btn footer-btn--danger" onclick="showEndModal()">
&#x26D4; 结束用餐
</button>
</div>
</div>
<!-- 二次确认对话框 -->
<div class="modal-overlay" id="endModal">
<div class="modal">
<div class="modal__title">&#x26D4; 确认结束用餐?</div>
<div class="modal__desc">
结束后顾客将<strong>无法继续取餐结算</strong>,本餐次进入收尾流程。<br>
系统将引导你完成各餐线餐品的<strong>余量处置</strong>(员工福利/捐赠/废弃/加工再利用)。
</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--cancel" onclick="hideEndModal()">取消</button>
<a class="modal__btn modal__btn--confirm" href="12-meal-end-surplus.html?meal=lunch" style="text-decoration:none;display:inline-flex;align-items:center;justify-content:center;">确认结束 · 去处置余量</a>
</div>
</div>
</div>
</div>
<script>
/* ============ 餐品 Mock 数据(按餐线分组) ============
每个餐线维护 7-12 道菜,字段:
name 菜名
emoji 菜图片占位 emoji
pricePer 100g 单价(元)
planned 计划制作(kg)
sold 已售(kg)
line 所属餐线
type 类别(stall 档口 / line 餐线)
*/
var DISHES = {
A: [
{ name: '宫保鸡丁', emoji: '🍗', pricePer: 3.5, planned: 8.5, sold: 7.8, type: 'line' },
{ name: '番茄炒蛋', emoji: '🍳', pricePer: 2.8, planned: 6.2, sold: 5.9, type: 'line' },
{ name: '白米饭', emoji: '🍚', pricePer: 1.2, planned: 15.0, sold: 12.0, type: 'line' },
{ name: '红烧肉', emoji: '🥩', pricePer: 5.8, planned: 7.0, sold: 6.5, type: 'line' },
{ name: '清炒西兰花', emoji: '🥦', pricePer: 2.5, planned: 4.5, sold: 3.8, type: 'line' },
{ name: '麻婆豆腐', emoji: '🥘', pricePer: 3.2, planned: 5.5, sold: 4.5, type: 'line' },
{ name: '糖醋里脊', emoji: '🍤', pricePer: 4.8, planned: 6.0, sold: 5.5, type: 'line' },
{ name: '紫菜蛋花汤', emoji: '🍲', pricePer: 1.5, planned: 8.0, sold: 6.0, type: 'line' },
{ name: '凉拌黄瓜', emoji: '🥒', pricePer: 1.8, planned: 3.5, sold: 2.5, type: 'line' },
{ name: '土豆丝', emoji: '🥔', pricePer: 2.0, planned: 5.0, sold: 4.5, type: 'line' },
{ name: '鱼香肉丝', emoji: '🍖', pricePer: 4.2, planned: 5.8, sold: 4.0, type: 'line' },
{ name: '蒸南瓜', emoji: '🎃', pricePer: 2.2, planned: 3.0, sold: 1.0, type: 'line' }
],
B: [
{ name: '蒜蓉菠菜', emoji: '🥬', pricePer: 2.6, planned: 4.8, sold: 3.5, type: 'line' },
{ name: '黄焖鸡', emoji: '🍗', pricePer: 5.5, planned: 7.2, sold: 6.8, type: 'line' },
{ name: '酸菜鱼', emoji: '🐟', pricePer: 6.8, planned: 8.0, sold: 7.0, type: 'line' },
{ name: '回锅肉', emoji: '🥩', pricePer: 5.2, planned: 5.5, sold: 4.0, type: 'line' },
{ name: '蛋炒饭', emoji: '🍚', pricePer: 2.5, planned: 6.0, sold: 5.5, type: 'line' },
{ name: '干煸豆角', emoji: '🫘', pricePer: 2.8, planned: 4.0, sold: 3.0, type: 'line' },
{ name: '银耳莲子汤', emoji: '🥣', pricePer: 1.8, planned: 5.0, sold: 3.0, type: 'line' },
{ name: '水煮肉片', emoji: '🌶️', pricePer: 5.8, planned: 6.5, sold: 5.8, type: 'line' },
{ name: '蒜蓉生菜', emoji: '🥗', pricePer: 2.2, planned: 3.8, sold: 3.0, type: 'line' }
],
C: [
{ name: '牛肉面', emoji: '🍜', pricePer: 8.5, planned: 5.0, sold: 4.5, type: 'stall' },
{ name: '炸酱面', emoji: '🍝', pricePer: 6.5, planned: 4.5, sold: 4.0, type: 'stall' },
{ name: '馄饨', emoji: '🥟', pricePer: 5.5, planned: 4.0, sold: 3.5, type: 'stall' },
{ name: '扬州炒饭', emoji: '🍚', pricePer: 6.8, planned: 5.5, sold: 5.0, type: 'stall' },
{ name: '盖浇饭', emoji: '🍱', pricePer: 8.0, planned: 6.0, sold: 5.5, type: 'stall' },
{ name: '酸辣粉', emoji: '🌶️', pricePer: 5.0, planned: 4.0, sold: 3.5, type: 'stall' },
{ name: '煎饺', emoji: '🥟', pricePer: 4.5, planned: 3.5, sold: 2.0, type: 'stall' }
]
};
/* ============ 渲染餐品卡 ============ */
function renderDishes(lineKey) {
var list = DISHES[lineKey] || [];
var html = '';
list.forEach(function (dish, idx) {
/* 余量计算 */
var surplus = +(dish.planned - dish.sold).toFixed(1);
var surplusRate = +(surplus / dish.planned * 100).toFixed(0);
var soldRate = +(dish.sold / dish.planned * 100).toFixed(0);
/* 余量状态判定 */
var stockCls = 'stock-bar__fill--ok';
var surplusValCls = 'metric__value--green';
if (surplusRate <= 10) {
stockCls = 'stock-bar__fill--low';
surplusValCls = 'metric__value--red';
} else if (surplusRate <= 25) {
stockCls = 'stock-bar__fill--mid';
surplusValCls = 'metric__value--gold';
}
/* 餐品类别徽标 */
var typeTag = dish.type === 'stall' ? '档口' : '餐线';
html += ''
+ '<div class="dish-card">'
+ ' <div class="dish-card__img">' + dish.emoji + '</div>'
+ ' <div class="dish-card__body">'
+ ' <div class="dish-card__head">'
+ ' <span class="dish-card__name">' + dish.name + '</span>'
+ ' <span class="dish-card__tag">' + typeTag + '</span>'
+ ' </div>'
+ ' <div class="dish-card__metrics">'
+ ' <div class="metric"><span class="metric__label">余量</span>'
+ ' <span class="metric__value ' + surplusValCls + '">' + surplus + 'kg</span></div>'
+ ' <div class="metric"><span class="metric__label">已售</span>'
+ ' <span class="metric__value metric__value--gold">' + dish.sold.toFixed(1) + 'kg</span></div>'
+ ' </div>'
+ ' <div class="stock-bar"><div class="stock-bar__fill ' + stockCls + '" style="width:' + (100 - soldRate) + '%;"></div></div>'
+ ' <div class="dish-card__action">'
+ ' <button class="dish-btn" data-name="' + dish.name + '" onclick="notifySupply(this)">'
+ ' 📢 通知补餐'
+ ' </button>'
+ ' </div>'
+ ' </div>'
+ '</div>';
});
document.getElementById('dishGrid').innerHTML = html;
}
/* ============ 餐线 Tab 切换 ============ */
function switchLine(el) {
var siblings = el.parentNode.querySelectorAll('.line-tab');
siblings.forEach(function (s) { s.classList.remove('is-active'); });
el.classList.add('is-active');
renderDishes(el.getAttribute('data-line'));
}
/* ============ 通知补餐 ============ */
function notifySupply(btn) {
if (btn.classList.contains('is-done')) return;
var name = btn.getAttribute('data-name');
btn.classList.add('is-done');
btn.innerHTML = '✓ 已通知';
showToast('已通知后厨补做 ' + name + ' 📢');
}
/* ============ Toast 提示 ============ */
var toastTimer = null;
function showToast(text) {
var toast = document.getElementById('toast');
toast.textContent = text;
toast.classList.add('toast--show');
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toast.classList.remove('toast--show');
}, 2200);
}
/* ============ 二次确认对话框 ============ */
function showEndModal() {
document.getElementById('endModal').classList.add('is-show');
}
function hideEndModal() {
document.getElementById('endModal').classList.remove('is-show');
}
/* ============ 实时时钟 + 开餐时长 ============ */
function updateClock() {
var now = new Date();
var year = now.getFullYear();
var month = String(now.getMonth() + 1).padStart(2, '0');
var date = String(now.getDate()).padStart(2, '0');
var weekDay = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.getDay()];
document.getElementById('topMeta').textContent = year + '-' + month + '-' + date + ' ' + weekDay;
}
updateClock();
setInterval(updateClock, 30000);
/* ============ URL 参数解析(餐次) ============ */
(function () {
var params = new URLSearchParams(window.location.search);
var meal = params.get('meal') || 'lunch';
var mealMap = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐' };
document.getElementById('mealTitle').textContent = mealMap[meal] || '午餐';
})();
/* ============ 初始渲染:A 区餐线 ============ */
renderDishes('A');
</script>
</body>
</html>
@@ -0,0 +1,876 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>结算设备 - 余量处置 | 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: #05080f;
color: #fff;
min-height: 100vh;
min-width: 1280px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.device-frame {
width: 1280px;
height: 720px;
background: #0e1326;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.04);
position: relative;
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.top-bar {
display: flex;
align-items: center;
height: 44px;
background: #1a2b4a;
padding: 0 20px;
flex-shrink: 0;
}
.back-btn {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
.top-bar__title {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
}
.top-bar__meta {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
font-family: Menlo, Consolas, monospace;
}
/* 主内容区 */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
padding: 14px 24px 16px;
overflow: hidden;
}
/* 顶部信息条 */
.end-banner {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 18px;
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(26, 43, 74, 0.6) 100%);
border: 1px solid rgba(239, 68, 68, 0.35);
border-radius: 12px;
margin-bottom: 12px;
}
.end-banner__icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(239, 68, 68, 0.22);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.end-banner__body { flex: 1; }
.end-banner__title {
font-size: 14px;
font-weight: 600;
color: #fff;
margin-bottom: 2px;
}
.end-banner__desc {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
}
.end-banner__time {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.end-banner__time-label {
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
}
.end-banner__time-value {
font-size: 13px;
color: #fbbf24;
font-weight: 600;
font-family: Menlo, Consolas, monospace;
}
/* 工具栏(批量操作) */
.toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: rgba(26, 43, 74, 0.4);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
margin-bottom: 12px;
}
.toolbar__select-all {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
user-select: none;
}
.toolbar__select-all input { cursor: pointer; accent-color: #4a90d9; }
.toolbar__divider {
width: 1px;
height: 22px;
background: rgba(255, 255, 255, 0.1);
}
.toolbar__label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.toolbar__select {
height: 28px;
padding: 0 28px 0 10px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
color: #fff;
font-size: 12px;
outline: none;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
min-width: 130px;
}
.toolbar__select:focus { border-color: #4a90d9; }
.toolbar__btn {
height: 28px;
padding: 0 14px;
background: linear-gradient(135deg, rgba(74, 144, 217, 0.4), rgba(74, 144, 217, 0.25));
border: 1px solid rgba(74, 144, 217, 0.5);
border-radius: 6px;
color: #fff;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.toolbar__btn:hover {
background: linear-gradient(135deg, rgba(74, 144, 217, 0.6), rgba(74, 144, 217, 0.4));
}
.toolbar__btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.toolbar__spacer { flex: 1; }
.toolbar__hint {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
}
/* 表格容器 */
.table-wrap {
flex: 1;
overflow-y: auto;
scrollbar-width: none;
background: rgba(26, 43, 74, 0.4);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
}
.table-wrap::-webkit-scrollbar { display: none; }
.surplus-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.surplus-table thead {
position: sticky;
top: 0;
background: #1a2b4a;
z-index: 2;
}
.surplus-table thead th {
padding: 10px 8px;
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
white-space: nowrap;
}
.surplus-table tbody td {
padding: 10px 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
vertical-align: middle;
}
.surplus-table tbody tr:hover { background: rgba(74, 144, 217, 0.06); }
.surplus-table tbody tr:last-child td { border-bottom: none; }
/* 列宽 */
.col-ck { width: 36px; text-align: center; }
.col-name { width: 130px; }
.col-line { width: 80px; }
.col-num { width: 80px; text-align: right; }
.col-rate { width: 80px; text-align: right; }
.col-method { width: 130px; }
.col-amount { width: 90px; }
.col-note { width: auto; }
/* 复选框 */
.row-ck {
width: 14px;
height: 14px;
cursor: pointer;
accent-color: #4a90d9;
vertical-align: middle;
}
/* 餐线徽标 */
.line-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
}
.line-badge--A { background: rgba(74, 144, 217, 0.18); color: #60a5fa; }
.line-badge--B { background: rgba(251, 191, 36, 0.18); color: #fbbf24; }
.line-badge--C { background: rgba(168, 85, 247, 0.18); color: #c084fc; }
/* 余量数字(红色) */
.num-surplus { color: #ef4444; font-weight: 700; }
.num-made { color: #fff; font-weight: 600; }
.num-sold { color: #4ade80; font-weight: 600; }
.num-rate { color: #fbbf24; font-weight: 600; }
/* 行内下拉(处置方式) */
.method-select {
width: 100%;
height: 26px;
padding: 0 22px 0 8px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
color: #fff;
font-size: 11px;
outline: none;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 6px center;
}
.method-select:focus { border-color: #4a90d9; }
.method-select.is-welfare { border-color: rgba(74, 144, 217, 0.5); color: #60a5fa; }
.method-select.is-donate { border-color: rgba(74, 222, 128, 0.5); color: #4ade80; }
.method-select.is-waste { border-color: rgba(239, 68, 68, 0.5); color: #ef4444; }
.method-select.is-reprocess { border-color: rgba(168, 85, 247, 0.5); color: #c084fc; }
.method-select.is-locker { border-color: rgba(251, 146, 60, 0.5); color: #fb923c; }
/* 处置量输入 */
.amount-input {
width: 70px;
height: 26px;
padding: 0 6px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
color: #fbbf24;
font-size: 12px;
font-weight: 600;
outline: none;
text-align: right;
font-family: Menlo, Consolas, monospace;
}
.amount-input:focus { border-color: #4a90d9; color: #fff; }
/* 备注输入 */
.note-input {
width: 100%;
height: 26px;
padding: 0 8px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: rgba(255, 255, 255, 0.85);
font-size: 11px;
outline: none;
}
.note-input:focus { border-color: #4a90d9; }
.note-input::placeholder { color: rgba(255, 255, 255, 0.3); }
/* 汇总栏 */
.summary-bar {
display: flex;
align-items: center;
gap: 24px;
padding: 12px 18px;
background: linear-gradient(135deg, rgba(74, 144, 217, 0.15), rgba(26, 43, 74, 0.6));
border: 1px solid rgba(74, 144, 217, 0.3);
border-radius: 10px;
margin-top: 12px;
}
.summary-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.summary-item__label {
font-size: 10px;
color: rgba(255, 255, 255, 0.55);
}
.summary-item__value {
font-size: 18px;
font-weight: 700;
color: #fff;
}
.summary-item__value--red { color: #ef4444; }
.summary-item__value--gold { color: #fbbf24; }
.summary-item__value--green { color: #4ade80; }
.summary-spacer { flex: 1; }
/* 底部固定操作区 */
.footer-actions {
display: flex;
gap: 14px;
padding-top: 12px;
}
.footer-btn {
flex: 1;
height: 46px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1px solid;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
text-decoration: none;
}
.footer-btn--secondary {
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.75);
}
.footer-btn--secondary:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
.footer-btn--submit {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
border-color: #22c55e;
color: #fff;
box-shadow: 0 6px 18px rgba(34, 197, 94, 0.35);
}
.footer-btn--submit:hover {
background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
box-shadow: 0 8px 22px rgba(34, 197, 94, 0.5);
}
/* 二次确认对话框 */
.modal-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(5, 8, 15, 0.75);
backdrop-filter: blur(6px);
z-index: 50;
display: none;
align-items: center;
justify-content: center;
}
.modal-overlay.is-show { display: flex; }
.modal {
width: 480px;
background: #1a2b4a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 24px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
}
.modal__title {
font-size: 17px;
font-weight: 700;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.modal__summary {
padding: 12px 14px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
margin-bottom: 18px;
}
.modal__summary-line {
display: flex;
justify-content: space-between;
font-size: 12px;
padding: 3px 0;
}
.modal__summary-label { color: rgba(255, 255, 255, 0.65); }
.modal__summary-value { color: #fff; font-weight: 600; }
.modal__actions {
display: flex;
gap: 10px;
}
.modal__btn {
flex: 1;
height: 38px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1px solid;
transition: all 0.2s;
}
.modal__btn--cancel {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.75);
}
.modal__btn--cancel:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
.modal__btn--confirm {
background: linear-gradient(135deg, #22c55e, #16a34a);
border-color: #22c55e;
color: #fff;
}
/* Toast */
.toast {
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
padding: 10px 22px;
background: rgba(74, 222, 128, 0.95);
color: #0e1326;
border-radius: 22px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 8px 24px rgba(74, 222, 128, 0.3);
opacity: 0;
pointer-events: none;
transition: all 0.3s;
z-index: 60;
}
.toast--show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<div class="device-frame">
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="11-meal-active.html?meal=lunch">&#x25C0; 返回开餐详情</a>
<span class="top-bar__title">余量处置 · <span id="mealTitle">午餐</span></span>
<span class="top-bar__meta" id="topMeta">2026-08-10 周一</span>
</div>
<!-- Toast -->
<div class="toast" id="toast">余量处置已提交 ✓</div>
<!-- 主内容 -->
<div class="main-area">
<!-- 顶部信息条 -->
<div class="end-banner">
<div class="end-banner__icon">&#x26D4;</div>
<div class="end-banner__body">
<div class="end-banner__title">午餐已结束 · 请处置各餐线餐品余量</div>
<div class="end-banner__desc">参考后台余量处置规则:每道菜需选择处置方式(员工福利/捐赠/废弃/加工再利用/存入智能货柜)</div>
</div>
<div class="end-banner__time">
<span class="end-banner__time-label">开餐 - 结束</span>
<span class="end-banner__time-value">11:00:23 - 14:02:15</span>
</div>
</div>
<!-- 工具栏 -->
<div class="toolbar">
<label class="toolbar__select-all">
<input type="checkbox" id="selectAll" onchange="toggleAll(this)">
全选
</label>
<div class="toolbar__divider"></div>
<span class="toolbar__label">批量处置方式:</span>
<select class="toolbar__select" id="batchMethod">
<option value="">请选择...</option>
<option value="welfare">员工福利</option>
<option value="donate">捐赠</option>
<option value="waste">废弃</option>
<option value="reprocess">加工再利用</option>
<option value="locker">存入智能货柜</option>
</select>
<button class="toolbar__btn" onclick="applyBatch()">应用到选中项</button>
<div class="toolbar__spacer"></div>
<span class="toolbar__hint" id="selectCount">已选 0 项</span>
</div>
<!-- 余量处置表格 -->
<div class="table-wrap">
<table class="surplus-table">
<colgroup>
<col class="col-ck">
<col class="col-name">
<col class="col-line">
<col class="col-num">
<col class="col-num">
<col class="col-num">
<col class="col-rate">
<col class="col-method">
<col class="col-amount">
<col class="col-note">
</colgroup>
<thead>
<tr>
<th class="col-ck"></th>
<th>餐品名称</th>
<th>餐线</th>
<th style="text-align:right;">实际制作(kg)</th>
<th style="text-align:right;">已售(kg)</th>
<th style="text-align:right;">余量(kg)</th>
<th style="text-align:right;">余量率</th>
<th>处置方式</th>
<th>处置量(kg)</th>
<th>备注</th>
</tr>
</thead>
<tbody id="surplusTbody">
<!-- JS 动态渲染 -->
</tbody>
</table>
</div>
<!-- 汇总栏 -->
<div class="summary-bar">
<div class="summary-item">
<span class="summary-item__label">总余量</span>
<span class="summary-item__value summary-item__value--red" id="totalSurplus">0.0 kg</span>
</div>
<div class="summary-item">
<span class="summary-item__label">已处置</span>
<span class="summary-item__value summary-item__value--green" id="totalDisposed">0.0 kg</span>
</div>
<div class="summary-item">
<span class="summary-item__label">待处置</span>
<span class="summary-item__value summary-item__value--gold" id="totalPending">0.0 kg</span>
</div>
<div class="summary-spacer"></div>
<div class="summary-item">
<span class="summary-item__label">处置方式分布</span>
<span class="summary-item__value" style="font-size:13px;font-weight:500;" id="methodDist"></span>
</div>
</div>
<!-- 底部操作 -->
<div class="footer-actions">
<a class="footer-btn footer-btn--secondary" href="11-meal-active.html?meal=lunch">&#x25C0; 放弃处置</a>
<button class="footer-btn footer-btn--submit" onclick="showSubmitModal()">
✓ 提交余量处置
</button>
</div>
</div>
<!-- 提交确认对话框 -->
<div class="modal-overlay" id="submitModal">
<div class="modal">
<div class="modal__title">✓ 确认提交余量处置</div>
<div class="modal__summary" id="modalSummary">
<!-- JS 动态渲染 -->
</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--cancel" onclick="hideSubmitModal()">取消</button>
<button class="modal__btn modal__btn--confirm" onclick="submitForm()">确认提交</button>
</div>
</div>
</div>
</div>
<script>
/* ============ 余量处置 Mock 数据 ============
复用 prod-proc-surplus.html 后台的处置方式四选项
每条字段:
id 行 ID
name 餐品名
line 所属餐线
made 实际制作(kg)
sold 已售(kg)
emoji 图标
*/
var SURPLUS_DATA = [
{ id: 1, name: '宫保鸡丁', line: 'A', made: 8.5, sold: 7.8, emoji: '🍗' },
{ id: 2, name: '番茄炒蛋', line: 'A', made: 6.2, sold: 5.9, emoji: '🍳' },
{ id: 3, name: '白米饭', line: 'A', made: 15.0, sold: 12.0, emoji: '🍚' },
{ id: 4, name: '红烧肉', line: 'A', made: 7.0, sold: 6.5, emoji: '🥩' },
{ id: 5, name: '清炒西兰花', line: 'A', made: 4.5, sold: 3.8, emoji: '🥦' },
{ id: 6, name: '麻婆豆腐', line: 'A', made: 5.5, sold: 4.5, emoji: '🥘' },
{ id: 7, name: '糖醋里脊', line: 'A', made: 6.0, sold: 5.5, emoji: '🍤' },
{ id: 8, name: '紫菜蛋花汤', line: 'A', made: 8.0, sold: 6.0, emoji: '🍲' },
{ id: 9, name: '蒜蓉菠菜', line: 'B', made: 4.8, sold: 3.5, emoji: '🥬' },
{ id: 10, name: '黄焖鸡', line: 'B', made: 7.2, sold: 6.8, emoji: '🍗' },
{ id: 11, name: '酸菜鱼', line: 'B', made: 8.0, sold: 7.0, emoji: '🐟' },
{ id: 12, name: '回锅肉', line: 'B', made: 5.5, sold: 4.0, emoji: '🥩' },
{ id: 13, name: '蛋炒饭', line: 'B', made: 6.0, sold: 5.5, emoji: '🍚' },
{ id: 14, name: '牛肉面', line: 'C', made: 5.0, sold: 4.5, emoji: '🍜' },
{ id: 15, name: '炸酱面', line: 'C', made: 4.5, sold: 4.0, emoji: '🍝' },
{ id: 16, name: '馄饨', line: 'C', made: 4.0, sold: 3.5, emoji: '🥟' },
{ id: 17, name: '扬州炒饭', line: 'C', made: 5.5, sold: 5.0, emoji: '🍚' },
{ id: 18, name: '盖浇饭', line: 'C', made: 6.0, sold: 5.5, emoji: '🍱' },
{ id: 19, name: '酸辣粉', line: 'C', made: 4.0, sold: 3.5, emoji: '🌶️' },
{ id: 20, name: '煎饺', line: 'C', made: 3.5, sold: 2.0, emoji: '🥟' }
];
/* 处置方式字典 */
var METHOD_MAP = {
'welfare': { label: '员工福利', cls: 'is-welfare' },
'donate': { label: '捐赠', cls: 'is-donate' },
'waste': { label: '废弃', cls: 'is-waste' },
'reprocess': { label: '加工再利用', cls: 'is-reprocess' },
'locker': { label: '存入智能货柜', cls: 'is-locker' }
};
/* ============ 渲染表格 ============ */
function renderTable() {
var html = '';
SURPLUS_DATA.forEach(function (item) {
var surplus = +(item.made - item.sold).toFixed(1);
var rate = +(surplus / item.made * 100).toFixed(1);
html += ''
+ '<tr data-id="' + item.id + '">'
+ ' <td class="col-ck"><input type="checkbox" class="row-ck" onchange="updateSummary()" data-id="' + item.id + '"></td>'
+ ' <td><span style="margin-right:4px;">' + item.emoji + '</span>' + item.name + '</td>'
+ ' <td><span class="line-badge line-badge--' + item.line + '">' + item.line + ' 区</span></td>'
+ ' <td style="text-align:right;" class="num-made">' + item.made.toFixed(1) + '</td>'
+ ' <td style="text-align:right;" class="num-sold">' + item.sold.toFixed(1) + '</td>'
+ ' <td style="text-align:right;" class="num-surplus">' + surplus.toFixed(1) + '</td>'
+ ' <td style="text-align:right;" class="num-rate">' + rate + '%</td>'
+ ' <td>'
+ ' <select class="method-select" onchange="onMethodChange(this,' + item.id + ')" data-id="' + item.id + '">'
+ ' <option value="">未处置</option>'
+ ' <option value="welfare">员工福利</option>'
+ ' <option value="donate">捐赠</option>'
+ ' <option value="waste">废弃</option>'
+ ' <option value="reprocess">加工再利用</option>'
+ ' <option value="locker">存入智能货柜</option>'
+ ' </select>'
+ ' </td>'
+ ' <td><input type="number" class="amount-input" step="0.1" min="0" max="' + surplus.toFixed(1) + '" value="' + surplus.toFixed(1) + '" data-id="' + item.id + '"></td>'
+ ' <td><input type="text" class="note-input" placeholder="如:加班餐/捐赠予..." data-id="' + item.id + '"></td>'
+ '</tr>';
});
document.getElementById('surplusTbody').innerHTML = html;
updateSummary();
}
/* ============ 全选/取消全选 ============ */
function toggleAll(masterCk) {
var cks = document.querySelectorAll('.row-ck');
cks.forEach(function (ck) { ck.checked = masterCk.checked; });
updateSummary();
}
/* ============ 处置方式变更联动样式 ============ */
function onMethodChange(sel, id) {
var cls = sel.value ? METHOD_MAP[sel.value].cls : '';
sel.className = 'method-select ' + cls;
updateSummary();
}
/* ============ 批量应用处置方式 ============ */
function applyBatch() {
var batchVal = document.getElementById('batchMethod').value;
if (!batchVal) {
showToast('请先选择批量处置方式', true);
return;
}
var checkedCks = document.querySelectorAll('.row-ck:checked');
if (checkedCks.length === 0) {
showToast('请先勾选要批量处置的餐品', true);
return;
}
checkedCks.forEach(function (ck) {
var id = ck.getAttribute('data-id');
var sel = document.querySelector('.method-select[data-id="' + id + '"]');
sel.value = batchVal;
sel.className = 'method-select ' + METHOD_MAP[batchVal].cls;
});
updateSummary();
showToast('已应用到 ' + checkedCks.length + ' 项 ✓');
}
/* ============ 更新汇总栏 ============ */
function updateSummary() {
var totalSurplus = 0;
var totalDisposed = 0;
var methodCount = { welfare: 0, donate: 0, waste: 0, reprocess: 0, locker: 0 };
var checkedCount = 0;
SURPLUS_DATA.forEach(function (item) {
var surplus = +(item.made - item.sold).toFixed(1);
totalSurplus += surplus;
var sel = document.querySelector('.method-select[data-id="' + item.id + '"]');
var amountInput = document.querySelector('.amount-input[data-id="' + item.id + '"]');
var ck = document.querySelector('.row-ck[data-id="' + item.id + '"]');
if (ck.checked) checkedCount++;
if (sel.value) {
var amount = parseFloat(amountInput.value) || 0;
totalDisposed += amount;
if (methodCount.hasOwnProperty(sel.value)) {
methodCount[sel.value] += amount;
}
}
});
totalSurplus = +totalSurplus.toFixed(1);
totalDisposed = +totalDisposed.toFixed(1);
var pending = +(totalSurplus - totalDisposed).toFixed(1);
document.getElementById('totalSurplus').textContent = totalSurplus.toFixed(1) + ' kg';
document.getElementById('totalDisposed').textContent = totalDisposed.toFixed(1) + ' kg';
document.getElementById('totalPending').textContent = pending.toFixed(1) + ' kg';
document.getElementById('selectCount').textContent = '已选 ' + checkedCount + ' 项';
/* 处置方式分布 */
var parts = [];
Object.keys(methodCount).forEach(function (k) {
if (methodCount[k] > 0) {
parts.push(METHOD_MAP[k].label + ' ' + methodCount[k].toFixed(1) + 'kg');
}
});
document.getElementById('methodDist').textContent = parts.length ? parts.join(' / ') : '—';
}
/* ============ 提交二次确认 ============ */
function showSubmitModal() {
var disposedCount = 0;
var totalDisposed = 0;
var methodCount = { welfare: 0, donate: 0, waste: 0, reprocess: 0, locker: 0 };
SURPLUS_DATA.forEach(function (item) {
var sel = document.querySelector('.method-select[data-id="' + item.id + '"]');
var amountInput = document.querySelector('.amount-input[data-id="' + item.id + '"]');
if (sel.value) {
disposedCount++;
var amount = parseFloat(amountInput.value) || 0;
totalDisposed += amount;
if (methodCount.hasOwnProperty(sel.value)) {
methodCount[sel.value] += amount;
}
}
});
if (disposedCount === 0) {
showToast('请至少为 1 道菜选择处置方式', true);
return;
}
var totalSurplus = SURPLUS_DATA.reduce(function (sum, item) {
return sum + (item.made - item.sold);
}, 0);
var pending = +(totalSurplus - totalDisposed).toFixed(1);
var html = ''
+ '<div class="modal__summary-line"><span class="modal__summary-label">总余量</span>'
+ ' <span class="modal__summary-value" style="color:#ef4444;">' + totalSurplus.toFixed(1) + ' kg</span></div>'
+ '<div class="modal__summary-line"><span class="modal__summary-label">本次处置</span>'
+ ' <span class="modal__summary-value" style="color:#4ade80;">' + totalDisposed.toFixed(1) + ' kg (' + disposedCount + ' 道)</span></div>'
+ '<div class="modal__summary-line"><span class="modal__summary-label">待后续处置</span>'
+ ' <span class="modal__summary-value" style="color:#fbbf24;">' + pending.toFixed(1) + ' kg</span></div>';
Object.keys(methodCount).forEach(function (k) {
if (methodCount[k] > 0) {
html += '<div class="modal__summary-line"><span class="modal__summary-label">· ' + METHOD_MAP[k].label + '</span>'
+ '<span class="modal__summary-value">' + methodCount[k].toFixed(1) + ' kg</span></div>';
}
});
document.getElementById('modalSummary').innerHTML = html;
document.getElementById('submitModal').classList.add('is-show');
}
function hideSubmitModal() {
document.getElementById('submitModal').classList.remove('is-show');
}
/* ============ 提交跳转(模拟) ============ */
function submitForm() {
/* 实际场景:POST 数据到后端 → 成功后跳转 */
window.location.href = '10-meal-center.html?toast=submitted';
}
/* ============ Toast 提示 ============ */
var toastTimer = null;
function showToast(text, isWarn) {
var toast = document.getElementById('toast');
toast.textContent = text;
toast.style.background = isWarn ? 'rgba(239,68,68,0.95)' : 'rgba(74,222,128,0.95)';
toast.style.color = isWarn ? '#fff' : '#0e1326';
toast.classList.add('toast--show');
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toast.classList.remove('toast--show');
}, 2200);
}
/* ============ 日期显示 ============ */
function updateDate() {
var now = new Date();
var year = now.getFullYear();
var month = String(now.getMonth() + 1).padStart(2, '0');
var date = String(now.getDate()).padStart(2, '0');
var weekDay = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.getDay()];
document.getElementById('topMeta').textContent = year + '-' + month + '-' + date + ' ' + weekDay;
}
updateDate();
/* ============ URL 参数解析(餐次) ============ */
(function () {
var params = new URLSearchParams(window.location.search);
var meal = params.get('meal') || 'lunch';
var mealMap = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐' };
document.getElementById('mealTitle').textContent = mealMap[meal] || '午餐';
})();
/* ============ 初始渲染 ============ */
renderTable();
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,44 @@
# 结算设备 CHANGELOG
## 2026-08-10 | 餐次管理模块新增(10-13 四页联动)
### 业务背景
`02-standby.html` 待机页底部「餐次管理」按钮(commit 32bd018 中由"小票打印"改名而来)一直为 `javascript:void(0)` 占位状态,功能未实现。本次落地完整的餐次管理业务闭环:开餐 → 上餐/加餐/闭餐 → 结束用餐 → 余量处置 → 历史回溯。
### 变更清单
- **入口改名**(`02-standby.html`):底部第 3 张入口卡片文案由「用餐管理 - 用餐记录/餐次管理」改为「餐次管理 - 开餐/结束用餐/历史记录」,跳转目标由占位改为 `10-meal-center.html`
- **新增 4 个页面**:
- `10-meal-center.html` 餐次管理入口页:左右两个大卡(左餐次开关 + 右历史记录),含当前餐次状态摘要条与实时时钟;URL 参数 `?toast=submitted` 驱动提交成功的 Toast 提示
- `11-meal-active.html` 开餐中详情页:开餐信息条(操作人/餐厅/开餐时间/已开时长/订单数) + 餐线 Tab(A/B/C 三区切换) + 餐品图文卡(余量进度条/余量阈值配色/已售份数) + 「通知补餐」按钮(含 toast 反馈 + 按钮态切换) + 底部「结束用餐」按钮(二次确认弹窗 → 跳余量处置页)
- `12-meal-end-surplus.html` 余量处置引导页:顶部信息条 + 批量操作工具栏(全选/批量处置方式下拉/应用到选中项) + 表格化余量列表(餐品/餐线/实制/已售/余量/余量率/处置方式/处置量/备注) + 汇总栏(总余量/已处置/待处置/处置方式分布) + 提交二次确认弹窗
- `13-meal-history.html` 历史记录页:左 30% 餐次列表(按日期分组 + 状态徽标) + 右 70% 流程时间轴(开餐→上餐→加餐→闭餐→结束→余量处置六类节点,色编码 + 节点卡片 + 处置明细) + 底部统计(订单/营收/客单价/余量)
- **导航页同步**(`index.html`):收银员屏区追加 4 张卡片(10-13),其中 10 标记「新」核心徽标
### 业务路径
```
02-standby.html (待机页,入口改名 + 跳转)
└─ 10-meal-center.html (二级入口:左开餐开关 + 右历史记录)
├─ 11-meal-active.html?meal=lunch (开餐中:餐线 Tab + 餐品列表 + 通知补餐 + 结束用餐)
│ └─ 12-meal-end-surplus.html?meal=lunch (余量处置:批量处置 + 提交跳回 10)
└─ 13-meal-history.html (历史:左餐次列表 + 右时间轴)
```
### 设计要点
- **余量处置四选项**(员工福利/捐赠/废弃/加工再利用)严格对齐后台 `web-admin/nutrition/prod-proc-surplus.html` 的处置方式体系,确保前后台业务语义一致
- **余量阈值三态**:≤10% 红色(紧急补餐)/ ≤25% 金色(预警)/ >25% 绿色(正常),通过 `stock-bar__fill--{ok|mid|low}` 修饰符联动配色
- **餐次状态三态**:进行中(绿色脉冲)/ 已结束(灰色)/ 未开餐(占位)
- **时间轴节点六类**:开餐(绿▶)/ 上餐(蓝🍽)/ 加餐(金➕)/ 闭餐(灰✕)/ 结束(红⏹)/ 处置(紫♻),通过渐变竖线串联
- **URL 参数驱动**:`?meal=lunch|breakfast|dinner` 切换餐次上下文,`?toast=submitted` 触发提交成功 Toast,`?date=2026-08-09&meal=lunch` 跳转历史指定餐次
- **批量处置交互**:复选框 + 顶部下拉"批量应用处置方式",处置方式变更联动下拉配色(福利蓝/捐赠绿/废弃红/再利用紫)
- **二次确认**:结束用餐(弹窗 → 跳余量处置)、提交处置(弹窗 → 显示处置汇总 → 跳餐次管理并触发 Toast)
### 不变更
- 现有 9 个收银员屏页面(01-09)与 4 个用户屏页面(user-01~04)结构与样式保持不变
- 用户类型徽标体系、支付方式徽标体系保持不变
- 副屏人脸识别监控区三态轮播逻辑保持不变
---
## 2026-08-10 | 待机页(02)新增副屏人脸识别监控区 + 入口改名
### 业务背景
@@ -311,6 +311,38 @@
<div class="card__desc">餐后小票预览与打印</div>
</div>
</a>
<a class="card" href="10-meal-center.html">
<span class="card__status card__status--core">10 新</span>
<div class="card__icon">&#x1F37D;&#xFE0F;</div>
<div class="card__body">
<div class="card__title">餐次管理</div>
<div class="card__desc">开餐开关 / 历史记录入口</div>
</div>
</a>
<a class="card" href="11-meal-active.html?meal=lunch">
<span class="card__status card__status--done">11</span>
<div class="card__icon">&#x1F50A;</div>
<div class="card__body">
<div class="card__title">开餐中详情</div>
<div class="card__desc">开餐信息 + 餐线餐品 + 补餐 + 结束</div>
</div>
</a>
<a class="card" href="12-meal-end-surplus.html?meal=lunch">
<span class="card__status card__status--done">12</span>
<div class="card__icon">&#x26D4;</div>
<div class="card__body">
<div class="card__title">余量处置</div>
<div class="card__desc">结束用餐引导 · 批量处置</div>
</div>
</a>
<a class="card" href="13-meal-history.html">
<span class="card__status card__status--done">13</span>
<div class="card__icon">&#x1F4DC;</div>
<div class="card__body">
<div class="card__title">餐次历史记录</div>
<div class="card__desc">开餐/补餐/闭餐/余量处置 · 点击查看明细</div>
</div>
</a>
</div>
</div>