feat:营养管理后台取餐管理新增取餐明细列表,食谱生成净菜订单功能优化

This commit is contained in:
fengpu
2026-09-21 10:18:15 +08:00
parent 80b0c78bdd
commit 143803a73f
4 changed files with 820 additions and 162 deletions
+1
View File
@@ -386,6 +386,7 @@ const SITE_MAP = [
{ file:'din-place-goal.html', label:'场所目标考核' },
{ file:'prod-recipe-plan.html', label:'食谱排期' },
{ file:'prod-clean-order.html', label:'净菜下单' },
{ file:'prod-clean-order-history.html', label:'净菜下单生成历史' },
{ file:'prod-proc-combo.html', label:'配菜组合' },
{ file:'prod-proc-cook.html', label:'烹饪加工' },
{ file:'prod-proc-serve.html', label:'出餐管理' },
+251 -19
View File
@@ -95,6 +95,35 @@ tbody tr:hover{background:#F8FBFF;}
.modal-close:hover{background:#f5f5f5;color:var(--text-primary);}
.modal-body{padding:24px;overflow-y:auto;flex:1;}
/* 主 Tab 栏 */
.main-tabs{display:flex;gap:0;background:#fff;border-radius:var(--radius-md);padding:0 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);}
.main-tab{padding:14px 20px;font-size:var(--font-base);color:var(--text-secondary);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .2s;user-select:none;}
.main-tab:hover{color:var(--primary);}
.main-tab.active{color:var(--primary);border-bottom-color:var(--primary);font-weight:600;}
.main-tab-panel{display:none;}
.main-tab-panel.active{display:block;}
/* 取餐明细 - 视频回放与丢餐提示 */
.video-info{font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:16px;}
.scale-info{display:flex;gap:24px;padding:12px 16px;background:#F8FAFC;border-radius:var(--radius-md);margin-bottom:16px;font-size:var(--font-sm);}
.scale-info__item{color:var(--text-primary);}
.scale-info__item strong{color:var(--primary);font-weight:600;}
.raw-data-table{width:100%;border-collapse:collapse;font-size:var(--font-sm);}
.raw-data-table thead th{background:#FAFAFA;padding:10px 14px;text-align:left;color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap;}
.raw-data-table tbody td{padding:12px 14px;color:var(--text-primary);border-bottom:1px solid #f0f0f0;white-space:nowrap;}
.raw-data-table tbody tr:last-child td{border-bottom:none;}
.raw-data-table tbody tr:hover{background:#F8FBFF;}
.video-player{background:#000;border-radius:var(--radius-md);overflow:hidden;}
.video-player__screen{position:relative;height:320px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);}
.video-player__placeholder{color:rgba(255,255,255,0.6);font-size:var(--font-sm);letter-spacing:2px;}
.video-player__bigplay{position:absolute;width:64px;height:64px;border-radius:50%;background:rgba(255,255,255,0.2);color:#fff;display:flex;align-items:center;justify-content:center;font-size:26px;cursor:pointer;transition:all .2s;}
.video-player__bigplay:hover{background:rgba(255,255,255,0.35);}
.video-player__bar{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#111;}
.video-player__btn{width:32px;height:32px;border:none;border-radius:50%;background:rgba(255,255,255,0.15);color:#fff;cursor:pointer;font-size:14px;}
.video-player__time{color:rgba(255,255,255,0.7);font-size:var(--font-xs);white-space:nowrap;}
.video-player__progress{flex:1;height:4px;background:rgba(255,255,255,0.2);border-radius:2px;position:relative;}
.video-player__progress-fill{width:40%;height:100%;background:var(--primary);border-radius:2px;}
/* 十维档案 Tab */
.profile-tabs{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:20px;overflow-x:auto;}
.profile-tab{padding:10px 16px;font-size:var(--font-sm);color:var(--text-secondary);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .2s;}
@@ -300,6 +329,15 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
</div>
</div>
<!-- 主 Tab 栏 -->
<div class="main-tabs">
<div class="main-tab active" data-tab="record" onclick="switchMainTab(this,'record')">用户取餐记录</div>
<div class="main-tab" data-tab="detail" onclick="switchMainTab(this,'detail')">取餐明细</div>
</div>
<!-- Tab1: 用户取餐记录 -->
<div class="main-tab-panel active" data-panel="record">
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-item"><label>员工姓名</label><input class="filter-input" type="text" placeholder="请输入姓名" style="min-width:120px;"></div>
@@ -328,7 +366,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<div class="table-card__title">取餐管理</div>
</div>
<div class="table-card__content">
<table>
<table id="recordTable">
<thead>
<!-- 交互注释:结算重量=终端结算时电子秤计重;实际重量=结算后复秤电子秤上传的实际重量;重量偏差=实际重量-结算重量,偏差超过±10克(含)显示橙色预警、超过±15克显示红色异常,未复秤显示"—" -->
<tr><th>就餐记录号</th><th>姓名</th><th>员工编号</th><th>单位</th><th>部门</th><th>餐次</th><th>取餐方式</th><th>取餐类型</th><th>人员类别</th><th>取餐开始时间</th><th>取餐结束时间</th><th>取餐总数量</th><th>结算重量(克)</th><th>实际重量(克)</th><th>重量偏差(克)</th><th>总金额</th><th>营养评分</th><th>操作</th></tr>
@@ -356,6 +394,70 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
</div>
</div>
</div><!-- /Tab1: 用户取餐记录 -->
<!-- Tab2: 取餐明细 -->
<div class="main-tab-panel" data-panel="detail">
<!-- 取餐明细筛选栏 -->
<div class="filter-bar">
<div class="filter-item"><label>记录状态</label><select class="filter-select"><option>全部</option><option>正常</option><option>无主记录</option><option>手动修正</option></select></div>
<div class="filter-item"><label>用户</label><input class="filter-input" type="text" placeholder="工号 / 姓名" style="min-width:140px;"></div>
<div class="filter-item"><label>餐次</label><select class="filter-select"><option>全部</option><option>早餐</option><option>中餐</option><option>晚餐</option><option>加餐</option></select></div>
<div class="filter-item"><label>餐品名称</label><input class="filter-input" type="text" placeholder="请输入餐品名称" style="min-width:140px;"></div>
<div class="filter-item"><label>取餐方式</label><select class="filter-select"><option>全部</option><option>餐线取餐</option><option>档口取餐</option></select></div>
<div class="filter-item"><label>取餐设备</label><select class="filter-select"><option>全部</option><option>一号餐线·2号秤</option><option>一号餐线·3号秤</option><option>一号餐线·5号秤</option><option>二号档口·1号秤</option><option>三号档口·2号秤</option></select></div>
<div class="filter-item"><label>就餐记录号</label><input class="filter-input" type="text" placeholder="请输入就餐记录号" style="min-width:140px;"></div>
<div class="filter-item"><label>日期范围</label><input class="filter-input" type="date" style="min-width:130px;"> <span style="margin:0 4px;color:#999;"></span> <input class="filter-input" type="date" style="min-width:130px;"></div>
<button class="btn btn-primary">查询</button>
<button class="btn btn-default">重置</button>
</div>
<!-- 取餐明细操作栏 -->
<div class="action-bar">
<button class="btn btn-primary"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 10v2h10v-2M7 2v7M4.5 6.5L7 9l2.5-2.5" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>导出列表数据</button>
<button class="btn btn-default"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4"/><path d="M4 5h6M4 7.5h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>查看导出任务</button>
</div>
<!-- 取餐明细数据表格 -->
<div class="table-card">
<div class="table-card__header">
<div class="table-card__title">取餐明细</div>
</div>
<div class="table-card__content">
<table id="detailTable">
<thead>
<!-- 交互注释:记录状态标识取餐记录与取餐人的对应关系——正常=已关联到取餐人;无主记录=未关联到取餐人(丢餐排查重点);手动修正=经人工核对后补全关联 -->
<tr><th>序号</th><th>取用记录ID</th><th>餐次</th><th>餐品名称</th><th>记录状态</th><th>取餐重量(克)</th><th>金额</th><th>用户(工号-姓名)</th><th>人员类别</th><th>取餐时间</th><th>取餐设备</th><th>取餐方式</th><th>取餐类型</th><th>就餐记录号</th><th>操作</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>P-20260428-0001</td><td>午餐</td><td>清炒生菜</td><td><span class="tag tag-green">正常</span></td><td>133.0</td><td>¥6.00</td><td>EMP001-张伟</td><td><span class="tag tag-orange">会员</span></td><td>2026-04-28 11:02:15</td><td>一号餐线·3号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td>M-20260428-001</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>2</td><td>P-20260428-0002</td><td>午餐</td><td>西红柿鸡蛋</td><td><span class="tag tag-green">正常</span></td><td>114.0</td><td>¥8.00</td><td>EMP001-张伟</td><td><span class="tag tag-orange">会员</span></td><td>2026-04-28 11:03:02</td><td>一号餐线·3号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td>M-20260428-001</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>3</td><td>P-20260428-0003</td><td>午餐</td><td>辣子鸡丁</td><td><span class="tag tag-green">正常</span></td><td>79.0</td><td>¥8.00</td><td>EMP001-张伟</td><td><span class="tag tag-orange">会员</span></td><td>2026-04-28 11:04:30</td><td>一号餐线·3号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td>M-20260428-001</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>4</td><td>P-20260428-0011</td><td>午餐</td><td>红烧肉</td><td><span class="tag tag-green">正常</span></td><td>85.0</td><td>¥16.00</td><td>EMP003-王磊</td><td><span class="tag tag-gray">非会员</span></td><td>2026-04-28 11:16:40</td><td>一号餐线·5号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td>M-20260428-003</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>5</td><td>P-20260428-0021</td><td>午餐</td><td>宫保鸡丁</td><td><span class="tag tag-red">无主记录</span></td><td>110.0</td><td>¥9.50</td><td></td><td></td><td>2026-04-28 11:25:08</td><td>一号餐线·5号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td></td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>6</td><td>P-20260428-0022</td><td>午餐</td><td>米饭</td><td><span class="tag tag-red">无主记录</span></td><td>200.0</td><td>¥12.00</td><td></td><td></td><td>2026-04-28 11:25:33</td><td>一号餐线·5号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td></td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>7</td><td>P-20260428-0031</td><td>晚餐</td><td>清蒸鲈鱼</td><td><span class="tag tag-red">无主记录</span></td><td>120.0</td><td>¥8.00</td><td></td><td></td><td>2026-04-28 18:12:45</td><td>二号档口·1号秤</td><td><span class="tag tag-green">档口取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td></td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>8</td><td>P-20260427-0005</td><td>午餐</td><td>糖醋里脊</td><td><span class="tag tag-orange">手动修正</span></td><td>95.0</td><td>¥12.00</td><td>EMP007-林教授</td><td><span class="tag tag-purple">外部人员</span></td><td>2026-04-27 12:12:20</td><td>二号档口·1号秤</td><td><span class="tag tag-green">档口取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td>M-20260428-007</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>9</td><td>P-20260427-0018</td><td>午餐</td><td>余量套餐B</td><td><span class="tag tag-orange">手动修正</span></td><td>195.0</td><td>¥5.00</td><td>EMP102-王保安</td><td><span class="tag tag-blue">工作人员</span></td><td>2026-04-27 14:11:05</td><td>一号餐线·2号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-orange">余量取餐</span></td><td>SP-001-003</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>10</td><td>P-20260428-0004</td><td>早餐</td><td>小米粥</td><td><span class="tag tag-green">正常</span></td><td>250.0</td><td>¥4.00</td><td>EMP005-陈刚</td><td><span class="tag tag-gray">非会员</span></td><td>2026-04-28 07:49:10</td><td>一号餐线·2号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td>M-20260428-005</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>11</td><td>P-20260428-0041</td><td>午餐</td><td>鸡胸肉沙拉</td><td><span class="tag tag-green">正常</span></td><td>180.0</td><td>¥10.80</td><td>EMP006-刘芳</td><td><span class="tag tag-orange">会员</span></td><td>2026-04-28 11:46:22</td><td>三号档口·2号秤</td><td><span class="tag tag-green">档口取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td>M-20260428-006</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
<tr><td>12</td><td>P-20260428-0023</td><td>午餐</td><td>余量套餐A</td><td><span class="tag tag-green">正常</span></td><td>220.0</td><td>¥5.00</td><td>EMP101-张运维</td><td><span class="tag tag-blue">员工</span></td><td>2026-04-28 14:05:50</td><td>一号餐线·2号秤</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-orange">余量取餐</span></td><td>SP-001-001</td><td class="td-actions"><a class="link">原始数据</a>&nbsp;<a class="link">查看视频</a></td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<span>显示 12 条,共 3,562 条</span>
<button class="page-btn"></button>
<button class="page-btn active">1</button>
<button class="page-btn">2</button>
<button class="page-btn">3</button>
<button class="page-btn"></button>
</div>
</div>
</div><!-- /Tab2: 取餐明细 -->
</div>
<!-- 交互脚本 -->
@@ -367,20 +469,25 @@ function toggleSubgroup(header){header.parentElement.classList.toggle('open');}
const Modal = {
open(title, bodyHtml) {
this.close();
this.stack(title, bodyHtml);
},
stack(title, bodyHtml) {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `<div class="modal">
<div class="modal-header"><div class="modal-title">${title}</div><button class="modal-close" onclick="Modal.close()">&times;</button></div>
<div class="modal-header"><div class="modal-title">${title}</div><button class="modal-close" onclick="Modal.closeTop(this.closest('.modal-overlay'))">&times;</button></div>
<div class="modal-body">${bodyHtml}</div>
</div>`;
overlay.addEventListener('click', e => { if (e.target === overlay) Modal.close(); });
overlay.addEventListener('click', e => { if (e.target === overlay) Modal.closeTop(overlay); });
document.body.appendChild(overlay);
},
close() {
const o = document.querySelector('.modal-overlay');
if (!o) return;
o.classList.add('closing');
setTimeout(() => o.remove(), 200);
document.querySelectorAll('.modal-overlay').forEach(o => { o.classList.add('closing'); setTimeout(() => o.remove(), 200); });
},
closeTop(overlay) {
if (!overlay) return;
overlay.classList.add('closing');
setTimeout(() => overlay.remove(), 200);
}
};
@@ -575,7 +682,7 @@ function showUserProfile(name) {
}
/* 绑定表格中的"查看用户信息"链接 */
document.querySelectorAll('tbody tr').forEach(row => {
document.querySelectorAll('#recordTable tbody tr').forEach(row => {
const link = row.querySelector('td:last-child .link');
const nameCell = row.querySelector('td:nth-child(2)');
if (link && nameCell) {
@@ -662,15 +769,24 @@ function showMealDetail(row) {
</div>
</div>`;
const tableRows = items.map(i => `<tr>
<td>${i.name}</td>
<td>¥${i.price.toFixed(2)}</td>
<td>${i.weight.toFixed(1)}</td>
<td>${i.cal.toFixed(1)}</td>
<td>${i.carb.toFixed(1)}</td>
<td>${i.fat.toFixed(1)}</td>
<td>${i.protein.toFixed(1)}</td>
</tr>`).join('');
const dateStr = (recordId.match(/\d{8}/) || ['20260428'])[0].replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
const methodDeviceMap = { '餐线取餐':'一号餐线·3号秤', '档口+餐线':'二号档口·1号秤', '点餐取餐':'一号餐线·3号秤', '外卖/外带':'二号档口·1号秤', '余量取餐':'一号餐线·2号秤' };
const device = methodDeviceMap[pickupMethod] || '一号餐线·3号秤';
const tableRows = items.map((i, idx) => {
const itemTime = dateStr + ' ' + startTime + ':' + String(idx * 8).padStart(2, '0');
return `<tr>
<td>${i.name}</td>
<td>¥${i.price.toFixed(2)}</td>
<td>${i.weight.toFixed(1)}</td>
<td>${i.cal.toFixed(1)}</td>
<td>${i.carb.toFixed(1)}</td>
<td>${i.fat.toFixed(1)}</td>
<td>${i.protein.toFixed(1)}</td>
<td><a class="link" onclick="showMealItemRawData('${i.name}', ${i.weight}, '${name}', '${itemTime}', '${device}')">查看</a></td>
<td><a class="link" onclick="showMealItemVideo('${i.name}', '${itemTime}', '${recordId}')">查看</a></td>
</tr>`;
}).join('');
const summaryRow = `<tr>
<td>合计</td>
@@ -680,12 +796,14 @@ function showMealDetail(row) {
<td>${totalCarb.toFixed(1)}克</td>
<td>${totalFat.toFixed(1)}克</td>
<td>${totalProtein.toFixed(1)}克</td>
<td></td>
<td></td>
</tr>`;
const tableHtml = `<div class="meal-section">
<div class="meal-section__title">餐品列表</div>
<table class="meal-table">
<thead><tr><th>餐品名称</th><th>金额</th><th>重量(克)</th><th>热量(千卡)</th><th>碳水化合物(克)</th><th>脂肪(克)</th><th>蛋白质(克)</th></tr></thead>
<thead><tr><th>餐品名称</th><th>金额</th><th>重量(克)</th><th>热量(千卡)</th><th>碳水化合物(克)</th><th>脂肪(克)</th><th>蛋白质(克)</th><th>原始数据</th><th>查看视频</th></tr></thead>
<tbody>${tableRows}${summaryRow}</tbody>
</table>
</div>`;
@@ -693,13 +811,127 @@ function showMealDetail(row) {
Modal.open('取餐详情 — ' + name, infoHtml + tableHtml);
}
function showMealItemRawData(dish, weight, user, time, device) {
buildRawDataModal({ dish: dish, statusText: '正常', weight: parseFloat(weight), user: user, time: time, device: device });
}
function showMealItemVideo(dish, time, recordId) {
buildVideoModal({ id: recordId, dish: dish, statusText: '正常', time: time });
}
/* 绑定"取餐总数量"列点击事件 */
document.querySelectorAll('tbody tr').forEach(row => {
document.querySelectorAll('#recordTable tbody tr').forEach(row => {
const quantityLink = row.querySelector('td:nth-child(12) .link');
if (quantityLink) {
quantityLink.addEventListener('click', () => showMealDetail(row));
}
});
/* ========== 主 Tab 切换 ========== */
function switchMainTab(el, key) {
document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
document.querySelectorAll('.main-tab-panel').forEach(p => p.classList.remove('active'));
document.querySelector(`.main-tab-panel[data-panel="${key}"]`).classList.add('active');
}
/* ========== 取餐明细 - 原始数据弹窗 ========== */
function showRawData(row) {
const c = row.querySelectorAll('td');
Modal.close();
buildRawDataModal({
dish: c[3].textContent.trim(),
statusText: c[4].textContent.trim(),
weight: parseFloat(c[5].textContent.trim()),
user: c[7].textContent.trim(),
time: c[9].textContent.trim(),
device: c[10].textContent.trim()
});
}
function buildRawDataModal(data) {
const deviceNoMap = { '一号餐线·2号秤':'SC-002', '一号餐线·3号秤':'SC-003', '一号餐线·5号秤':'SC-005', '二号档口·1号秤':'DC-001', '三号档口·2号秤':'DC-002' };
const deviceNo = deviceNoMap[data.device] || '—';
const isOrphan = data.statusText === '无主记录';
const isManual = data.statusText === '手动修正';
const recStatus = (isOrphan || isManual) ? '未识别' : '已识别';
const recUser = (isOrphan || isManual) ? '—' : data.user;
const statusTag = recStatus === '已识别' ? '<span class="tag tag-green">已识别</span>' : '<span class="tag tag-red">未识别</span>';
const samples = [
{ t: minusSeconds(data.time, 5), w: (data.weight - 38).toFixed(1), d: '+' + (data.weight - 38).toFixed(1), stable: '否' },
{ t: minusSeconds(data.time, 2), w: (data.weight - 9).toFixed(1), d: '+29.0', stable: '否' },
{ t: data.time, w: data.weight.toFixed(1), d: '+9.0', stable: '是' }
];
const rowsHtml = samples.map(s => `<tr>
<td>${s.t}</td>
<td>${s.w}</td>
<td>${s.d}</td>
<td>${statusTag}</td>
<td>${recUser}</td>
<td>${s.stable === '是' ? '<span class="tag tag-green">是</span>' : '<span class="tag tag-orange">否</span>'}</td>
</tr>`).join('');
const html = `<div class="scale-info">
<div class="scale-info__item">设备编号:<strong>${deviceNo}</strong></div>
<div class="scale-info__item">设备名称:<strong>${data.device}</strong></div>
</div>
<table class="raw-data-table">
<thead><tr><th>采样时间</th><th>当前重量(g)</th><th>较上次变化重量(g)</th><th>识别状态</th><th>用户</th><th>是否稳定</th></tr></thead>
<tbody>${rowsHtml}</tbody>
</table>`;
Modal.stack('秤原始数据 — ' + data.dish, html);
}
function minusSeconds(timeStr, sec) {
const parts = timeStr.split(' ');
const date = parts[0];
const hms = parts[1].split(':').map(Number);
const d = new Date(0);
d.setHours(hms[0], hms[1], hms[2] - sec);
const pad = n => String(n).padStart(2, '0');
return date + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
/* ========== 取餐明细 - 视频回放弹窗 ========== */
function showPickupVideo(row) {
const c = row.querySelectorAll('td');
Modal.close();
buildVideoModal({
id: c[1].textContent.trim(),
dish: c[3].textContent.trim(),
statusText: c[4].textContent.trim(),
time: c[9].textContent.trim()
});
}
function buildVideoModal(data) {
const html = `<div class="video-info">取用记录ID${data.id} · 取餐时间:${data.time} · 记录状态:${data.statusText}</div>
<div class="video-player">
<div class="video-player__screen">
<div class="video-player__placeholder">取餐过程监控画面</div>
<div class="video-player__bigplay">▶</div>
</div>
<div class="video-player__bar">
<button class="video-player__btn">▶</button>
<span class="video-player__time">00:00 / 00:18</span>
<div class="video-player__progress"><div class="video-player__progress-fill"></div></div>
</div>
</div>`;
Modal.stack('取餐视频回放 — ' + data.dish, html);
}
/* 绑定取餐明细表格操作列 */
document.querySelectorAll('#detailTable tbody tr').forEach(row => {
const links = row.querySelectorAll('.td-actions .link');
if (links.length >= 2) {
links[0].addEventListener('click', () => showRawData(row));
links[1].addEventListener('click', () => showPickupVideo(row));
}
});
</script>
</body>
</html>
@@ -0,0 +1,567 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>营养管理 - 净菜订单生成历史 | 健康CQ原型</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:#F5F7FA;color:#333;min-width:1280px;}
:root{--primary:#1890FF;--primary-light:#E6F7FF;--success:#52C41A;--warning:#FAAD14;--danger:#FF4D4F;--text-primary:#333;--text-secondary:#666;--text-placeholder:#999;--border:#E8E8E8;--background:#F5F7FA;--font-xs:11px;--font-sm:13px;--font-base:15px;--font-lg:17px;--font-xl:20px;--font-xxl:24px;--radius-sm:4px;--radius-md:8px;--radius-lg:12px;--shadow-sm:0 1px 4px rgba(0,0,0,0.08);--shadow-md:0 4px 12px rgba(0,0,0,0.12);}
.top-bar{display:flex;align-items:center;height:56px;background:#001529;padding:0 24px;position:fixed;top:0;left:0;right:0;z-index:100;}
.top-bar__logo{display:flex;align-items:center;gap:8px;color:#fff;font-size:var(--font-lg);font-weight:600;white-space:nowrap;width:240px;flex-shrink:0;}
.top-bar__logo svg{width:28px;height:28px;}
.top-bar__nav{display:flex;align-items:center;gap:4px;flex:1;}
.top-bar__nav a{color:rgba(255,255,255,0.65);text-decoration:none;padding:6px 16px;border-radius:var(--radius-sm);font-size:var(--font-sm);transition:all .2s;}
.top-bar__nav a:hover{color:#fff;background:rgba(255,255,255,0.08);}
.top-bar__nav a.active{color:#fff;background:var(--primary);}
.top-bar__right{display:flex;align-items:center;gap:16px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);}
.top-bar__right .btn-back{color:rgba(255,255,255,0.65);cursor:pointer;display:flex;align-items:center;gap:4px;text-decoration:none;}
.top-bar__right .btn-back:hover{color:#fff;}
.avatar{width:32px;height:32px;border-radius:50%;background:#1890FF;display:flex;align-items:center;justify-content:center;color:#fff;font-size:var(--font-sm);cursor:pointer;}
.sidebar{position:fixed;top:56px;left:0;bottom:0;width:240px;background:#001529;overflow-y:auto;z-index:90;padding-top:8px;}
.sidebar-menu__title{color:rgba(255,255,255,0.35);font-size:var(--font-xs);padding:16px 24px 8px;text-transform:uppercase;letter-spacing:.5px;}
.sidebar-item{display:flex;align-items:center;gap:8px;padding:10px 24px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);cursor:pointer;transition:all .15s;text-decoration:none;}
.sidebar-item:hover{color:#fff;background:rgba(255,255,255,0.06);}
.sidebar-item.active{color:#fff;background:var(--primary);}
.mi{display:inline-block;width:20px;text-align:center;filter:grayscale(1);opacity:0.65;margin-right:2px;}
.sidebar-item.active .mi{opacity:0.9;}
.main{margin-left:240px;margin-top:56px;padding:24px;min-height:calc(100vh - 56px);}
/* 页面标题行 */
.page-header{display:flex;align-items:center;justify-content:space-between;background:#fff;border-radius:var(--radius-md);padding:16px 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);}
.page-header__left{display:flex;align-items:baseline;gap:12px;}
.page-header__title{font-size:var(--font-xl);font-weight:700;color:var(--text-primary);}
.page-header__desc{font-size:var(--font-sm);color:var(--text-placeholder);}
.page-header__right{display:flex;align-items:center;gap:12px;}
/* 搜索区 */
.filter-bar{background:#fff;border-radius:var(--radius-md);padding:20px 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);display:flex;align-items:center;gap:16px;flex-wrap:wrap;}
.filter-item{display:flex;align-items:center;gap:8px;}
.filter-item label{font-size:var(--font-sm);color:var(--text-secondary);white-space:nowrap;}
.filter-input{height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff;min-width:180px;}
.filter-input:focus{border-color:var(--primary);}
.filter-select{height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff 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") no-repeat right 10px center;appearance:none;cursor:pointer;min-width:130px;}
.btn{height:36px;padding:0 16px;border-radius:var(--radius-sm);font-size:var(--font-sm);cursor:pointer;border:none;transition:all .2s;display:inline-flex;align-items:center;gap:6px;}
.btn-primary{background:var(--primary);color:#fff;}.btn-primary:hover{background:#40a9ff;}
.btn-default{background:#fff;color:var(--text-primary);border:1px solid var(--border);text-decoration:none;}.btn-default:hover{color:var(--primary);border-color:var(--primary);}
/* 表格容器 */
.table-card{background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-sm);overflow:hidden;}
.table-card__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);}
.table-card__title{font-size:var(--font-base);font-weight:600;}
.table-card__content{overflow-x:auto;overflow-y:hidden;}
.table-card__content table{min-width:100%;white-space:nowrap;}
table{width:100%;border-collapse:collapse;}
thead th{background:#FAFAFA;padding:12px 16px;text-align:left;font-size:var(--font-sm);color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap;}
tbody td{padding:12px 16px;font-size:var(--font-sm);color:var(--text-primary);border-bottom:1px solid var(--border);white-space:nowrap;}
tbody tr:hover{background:#F8FBFF;}
/* 餐次/人数多行单元格 */
.meal-cell{line-height:1.9;}
/* 标签 */
.tag{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:var(--font-xs);}
.tag-blue{background:var(--primary-light);color:var(--primary);}
.tag-green{background:#F6FFED;color:var(--success);}
.tag-orange{background:#FFF7E6;color:var(--warning);}
.tag-red{background:#FFF1F0;color:var(--danger);}
.tag-gray{background:#F5F5F5;color:#999;}
.tag-cyan{background:#E6FFFB;color:#08979C;border:1px solid #87E8DE;}
.tag-cyan::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#13C2C2;margin-right:5px;vertical-align:middle;animation:statusPulse 1.4s ease-in-out infinite;}
@keyframes statusPulse{0%,100%{opacity:0.3;transform:scale(0.8);}50%{opacity:1;transform:scale(1.2);}}
.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);margin-right:8px;}
.page-btn{min-width:32px;height:32px;padding:0 8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#fff;cursor:pointer;font-size:var(--font-sm);display:inline-flex;align-items:center;justify-content:center;transition:all .2s;color:var(--text-primary);}
.page-btn:hover:not(:disabled):not(.active){color:var(--primary);border-color:var(--primary);}
.page-btn.active{background:var(--primary);color:#fff;border-color:var(--primary);}
.page-btn:disabled{color:#ccc;cursor:not-allowed;background:#f5f5f5;}
/* 弹窗 */
.modal-mask{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:200;align-items:center;justify-content:center;}
.modal-mask.show{display:flex;}
.modal{background:#fff;border-radius:var(--radius-lg);width:1100px;max-width:92vw;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 8px 24px rgba(0,0,0,0.15);}
.modal__header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--border);}
.modal__title{font-size:var(--font-lg);font-weight:600;}
.modal__close{width:32px;height:32px;border:none;background:none;font-size:18px;cursor:pointer;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;color:var(--text-secondary);}
.modal__close:hover{background:#f5f5f5;color:var(--text-primary);}
.modal__body{padding:20px 24px;overflow-y:auto;flex:1;}
.modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 24px;border-top:1px solid var(--border);}
/* 弹窗概要 */
.detail-summary{display:flex;flex-wrap:wrap;gap:20px 40px;padding:14px 16px;background:#F7F9FC;border-radius:var(--radius-sm);margin-bottom:16px;}
.detail-summary__item{display:flex;gap:8px;font-size:var(--font-sm);}
.detail-summary__label{color:var(--text-placeholder);}
.detail-summary__value{color:var(--text-primary);font-weight:500;}
/* 弹窗内餐品表 */
.dish-table{width:100%;border-collapse:collapse;border:1px solid var(--border);}
.dish-table thead th{background:#F7F9FC;padding:8px 12px;font-size:var(--font-xs);color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);text-align:left;white-space:nowrap;}
.dish-table tbody td{padding:10px 12px;font-size:var(--font-sm);color:var(--text-primary);border-bottom:1px solid #f0f0f0;white-space:nowrap;}
.dish-table tr.dish-row{cursor:pointer;}
.dish-table tr.dish-row:hover{background:#F8FBFF;}
.dish-table tr.dish-row.expanded{background:#F0F7FF;}
.dish-table tr.dish-row:last-child td{border-bottom:none;}
.dish-table tr.ingredient-row{background:#FBFCFE;}
.dish-table tr.ingredient-row td{padding:0;border-bottom:1px solid #f0f0f0;}
.expand-arrow{display:inline-block;width:20px;text-align:center;transition:transform .2s;font-size:var(--font-xs);color:var(--text-placeholder);}
.expand-arrow.expanded{transform:rotate(90deg);}
/* 食材子表 */
.ing-table{width:100%;border-collapse:collapse;}
.ing-table thead th{background:#FAFBFC;padding:6px 12px 6px 36px;font-size:var(--font-xs);color:var(--text-placeholder);font-weight:500;border-bottom:1px dashed #e5e5e5;text-align:left;white-space:nowrap;}
.ing-table tbody td{padding:8px 12px 8px 36px;font-size:var(--font-xs);color:var(--text-secondary);border-bottom:1px dashed #f0f0f0;white-space:nowrap;}
.ing-table tbody td:first-child{padding-left:36px;font-weight:500;color:var(--text-primary);}
.ing-table tbody tr:last-child td{border-bottom:none;}
.ing-type{display:inline-block;padding:1px 7px;border-radius:8px;font-size:10px;line-height:16px;}
.ing-type--main{background:var(--primary-light);color:var(--primary);}
.ing-type--aux{background:#F6FFED;color:var(--success);}
.ing-type--season{background:#FFF7E6;color:var(--warning);}
/* 空态 */
.empty-state{text-align:center;color:var(--text-placeholder);padding:60px 0;font-size:var(--font-sm);}
.empty-state .icon{font-size:40px;display:block;margin-bottom:12px;}
</style>
</head>
<body>
<header class="top-bar">
<div class="top-bar__logo"><svg viewBox="0 0 28 28" fill="none"><rect width="28" height="28" rx="6" fill="#1890FF"/><path d="M7 14h14M14 7v14" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/></svg>营养管理</div>
<nav class="top-bar__nav">
<a href="trace-query.html">溯源总览</a><a href="arc-employee.html">档案</a><a href="farm-land.html">绿色种采</a><a href="sup-hygiene-check.html">卫生供应</a><a href="prod-qc-daily.html" class="active">健康生产</a><a href="din-dict-mgmt.html">营养用餐</a><a href="ano-dirty.html">异常事件</a>
</nav>
<div class="top-bar__right"><a class="btn-back" href="../../index.html">← 返回首页</a><div class="avatar"></div><span>系统管理员 ▾</span></div>
</header>
<aside class="sidebar">
<div class="sidebar-menu__title">健康生产</div>
<a href="prod-qc-daily.html" class="sidebar-item"><span class="mi">🛡️</span>食安监控</a>
<a href="prod-recipe-plan.html" class="sidebar-item"><span class="mi">📋</span>食谱规划</a>
<a href="prod-clean-order.html" class="sidebar-item active"><span class="mi">📝</span>净菜订单</a>
<a href="prod-proc-cabinet.html" class="sidebar-item"><span class="mi">🗄️</span>净菜进柜</a>
<a href="prod-proc-combo.html" class="sidebar-item"><span class="mi">🔪</span>净菜组配</a>
<a href="prod-proc-cook.html" class="sidebar-item"><span class="mi">🍳</span>按单烹制</a>
<a href="prod-proc-serve.html" class="sidebar-item"><span class="mi">🍽️</span>上餐监控</a>
<a href="prod-proc-surplus.html" class="sidebar-item"><span class="mi">♻️</span>余量处置</a>
<a href="prod-proc-stats.html" class="sidebar-item"><span class="mi">📊</span>生产统计</a>
</aside>
<div class="main">
<!-- 页面标题行 -->
<div class="page-header">
<div class="page-header__left">
<span class="page-header__title">净菜订单生成历史</span>
<span class="page-header__desc">自动生成的净菜订单记录,点击「数据明细」查看餐品与食材用量对比</span>
</div>
<div class="page-header__right">
<a class="btn btn-default" href="prod-clean-order.html">← 返回净菜订单</a>
</div>
</div>
<!-- 搜索区 -->
<div class="filter-bar">
<div class="filter-item"><label>订单号</label><input class="filter-input" id="fOrderNo" placeholder="请输入订单号"></div>
<div class="filter-item"><label>生成状态</label><select class="filter-select" id="fStatus"><option value="">全部</option><option value="generating">生成中</option><option value="completed">已完成</option></select></div>
<div class="filter-item"><label>生成餐次</label><select class="filter-select" id="fMeal"><option value="">全部</option><option value="早餐">早餐</option><option value="午餐">午餐</option><option value="晚餐">晚餐</option></select></div>
<div class="filter-item"><label>食谱日期</label><input class="filter-input" type="date" id="fDate"></div>
<button class="btn btn-primary" onclick="applyFilter()">查询</button>
<button class="btn btn-default" onclick="resetFilter()">重置</button>
</div>
<!-- 生成记录表格(订单维度) -->
<div class="table-card">
<div class="table-card__header"><div class="table-card__title">生成记录</div><div class="table-card__title" style="font-weight:400;color:var(--text-secondary);" id="tableSubTitle"></div></div>
<div class="table-card__content">
<table id="historyTable">
<thead>
<tr>
<th style="width:56px;">序号</th>
<th>订单号</th>
<th>食谱日期</th>
<th>生成方式</th>
<th>生成状态</th>
<th>餐次</th>
<th>人数</th>
<th>菜品种类</th>
<th>预定配送时间</th>
<th>开始生成时间</th>
<th>生成成功时间</th>
<th>创建人</th>
<th>操作</th>
</tr>
</thead>
<tbody id="historyTableBody"></tbody>
</table>
<div class="empty-state" id="historyEmpty" style="display:none;"><span class="icon">📭</span>暂无符合条件的净菜订单生成记录</div>
</div>
<div class="pagination" id="pagination"></div>
</div>
</div>
<!-- 数据明细弹窗 -->
<div class="modal-mask" id="modal-detail">
<div class="modal">
<div class="modal__header">
<div class="modal__title">数据明细 — <span id="detailOrderNo"></span></div>
<button class="modal__close" onclick="closeDetail()"></button>
</div>
<div class="modal__body">
<div class="detail-summary" id="detailSummary"></div>
<table class="dish-table">
<thead><tr><th style="width:28px;"></th><th>餐次</th><th>餐品名称</th><th>规格</th><th>就餐人数</th><th>打包份数</th><th>内配料</th><th>AI 推荐生重(kg)</th><th>AI 推荐熟重(kg)</th><th>实际制作量(kg)</th><th>差异率</th></tr></thead>
<tbody id="detailDishBody"></tbody>
</table>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeDetail()">关闭</button>
</div>
</div>
</div>
<script>
/* ========== 工具函数 ========== */
function pad2(n){return String(n).padStart(2,'0');}
function dateOffsetStr(offset){
var d=new Date();
d.setDate(d.getDate()+offset);
return d.getFullYear()+'-'+pad2(d.getMonth()+1)+'-'+pad2(d.getDate());
}
/* ========== 餐品食材配方 BOM(每份基准克重) ========== */
var recipeBom={
'宫保鸡丁':[{name:'鸡胸肉',type:'主材',perServing:120},{name:'花生米',type:'辅材',perServing:30},{name:'黄瓜丁',type:'辅材',perServing:40},{name:'干辣椒',type:'调料',perServing:10},{name:'葱姜蒜',type:'调料',perServing:15}],
'清蒸鲈鱼':[{name:'鲈鱼',type:'主材',perServing:200},{name:'姜片',type:'调料',perServing:10},{name:'葱丝',type:'调料',perServing:8}],
'红烧肉':[{name:'五花肉',type:'主材',perServing:200},{name:'酱油',type:'调料',perServing:15},{name:'冰糖',type:'调料',perServing:10},{name:'八角',type:'调料',perServing:3}],
'麻婆豆腐':[{name:'嫩豆腐',type:'主材',perServing:200},{name:'肉末',type:'辅材',perServing:50},{name:'豆瓣酱',type:'调料',perServing:12},{name:'花椒',type:'调料',perServing:2}],
'番茄炒蛋':[{name:'番茄',type:'主材',perServing:150},{name:'鸡蛋',type:'主材',perServing:100},{name:'葱花',type:'调料',perServing:5}],
'炒时蔬':[{name:'西兰花',type:'主材',perServing:120},{name:'胡萝卜',type:'辅材',perServing:50},{name:'大蒜',type:'调料',perServing:8}],
'小米粥':[{name:'小米',type:'主材',perServing:80},{name:'清水',type:'辅材',perServing:250}],
'煮鸡蛋':[{name:'鸡蛋',type:'主材',perServing:60}],
'白粥':[{name:'大米',type:'主材',perServing:60},{name:'清水',type:'辅材',perServing:300}],
'豆浆':[{name:'黄豆',type:'主材',perServing:50},{name:'清水',type:'辅材',perServing:300}],
'馒头':[{name:'面粉',type:'主材',perServing:100},{name:'酵母',type:'调料',perServing:2}],
'茶叶蛋':[{name:'鸡蛋',type:'主材',perServing:60},{name:'茶叶',type:'调料',perServing:5}],
'鱼香肉丝':[{name:'猪里脊',type:'主材',perServing:120},{name:'木耳',type:'辅材',perServing:30},{name:'胡萝卜',type:'辅材',perServing:30},{name:'青椒',type:'辅材',perServing:30},{name:'豆瓣酱',type:'调料',perServing:10}],
'糖醋排骨':[{name:'猪排骨',type:'主材',perServing:180},{name:'白糖',type:'调料',perServing:20},{name:'香醋',type:'调料',perServing:10}],
'清炒西兰花':[{name:'西兰花',type:'主材',perServing:150},{name:'大蒜',type:'调料',perServing:8}],
'紫菜蛋花汤':[{name:'紫菜',type:'主材',perServing:5},{name:'鸡蛋',type:'辅材',perServing:50},{name:'清水',type:'辅材',perServing:300}],
'米饭':[{name:'大米',type:'主材',perServing:100},{name:'清水',type:'辅材',perServing:150}],
'糖醋里脊':[{name:'猪里脊',type:'主材',perServing:150},{name:'白糖',type:'调料',perServing:20},{name:'香醋',type:'调料',perServing:10}],
'冬瓜排骨汤':[{name:'冬瓜',type:'主材',perServing:120},{name:'猪排骨',type:'辅材',perServing:100},{name:'清水',type:'辅材',perServing:300}],
'酸奶':[{name:'酸奶',type:'主材',perServing:200}]
};
var ING_TYPE_CLS={'主材':'ing-type--main','辅材':'ing-type--aux','调料':'ing-type--season'};
/* ========== 净菜订单生成历史数据(订单维度,一个订单可含多个餐次) ========== */
var genOrders=[
{orderNo:'CO-1001',dayOffset:0,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'06:30',startTime:'05:30:00',successTime:'05:31:03',actualRatio:0.98,
meals:[{meal:'早餐',dinerCount:80}],
details:[
{meal:'早餐',foodName:'小米粥',spec:'300g/标准份',dinerCount:80,servings:80,needSeasoning:false,actualRatio:0.97},
{meal:'早餐',foodName:'煮鸡蛋',spec:'60g/标准份',dinerCount:80,servings:80,needSeasoning:false,actualRatio:1.02},
{meal:'早餐',foodName:'白粥',spec:'300g/标准份',dinerCount:80,servings:80,needSeasoning:false,actualRatio:0.99},
{meal:'早餐',foodName:'豆浆',spec:'350g/标准份',dinerCount:80,servings:80,needSeasoning:false,actualRatio:0.98}
]},
{orderNo:'CO-1002',dayOffset:0,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'10:00',startTime:'09:30:00',successTime:'09:31:12',actualRatio:0.97,
meals:[{meal:'午餐',dinerCount:120}],
details:[
{meal:'午餐',foodName:'宫保鸡丁',spec:'500g/标准份',dinerCount:120,servings:120,needSeasoning:true,actualRatio:0.96},
{meal:'午餐',foodName:'清蒸鲈鱼',spec:'600g/标准份',dinerCount:120,servings:120,needSeasoning:false,actualRatio:1.03},
{meal:'午餐',foodName:'红烧肉',spec:'500g/标准份',dinerCount:120,servings:120,needSeasoning:true,actualRatio:1.04},
{meal:'午餐',foodName:'番茄炒蛋',spec:'400g/标准份',dinerCount:120,servings:120,needSeasoning:false,actualRatio:0.97}
]},
{orderNo:'CO-1003',dayOffset:0,genType:'auto',genStatus:'generating',creator:'系统',deliverTime:'16:00',startTime:'14:30:00',successTime:'',actualRatio:0.99,
meals:[{meal:'早餐',dinerCount:80},{meal:'晚餐',dinerCount:100}],
details:[
{meal:'早餐',foodName:'馒头',spec:'100g/标准份',dinerCount:80,servings:160,needSeasoning:false,actualRatio:1.01},
{meal:'早餐',foodName:'茶叶蛋',spec:'60g/标准份',dinerCount:80,servings:80,needSeasoning:true,actualRatio:1.03},
{meal:'晚餐',foodName:'米饭',spec:'250g/标准份',dinerCount:100,servings:200,needSeasoning:false,actualRatio:0.98},
{meal:'晚餐',foodName:'宫保鸡丁',spec:'500g/标准份',dinerCount:100,servings:100,needSeasoning:true,actualRatio:0.96},
{meal:'晚餐',foodName:'馒头',spec:'100g/标准份',dinerCount:100,servings:200,needSeasoning:false,actualRatio:1.00}
]},
{orderNo:'CO-1004',dayOffset:0,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'16:00',startTime:'13:40:00',successTime:'13:41:20',actualRatio:1.01,
meals:[{meal:'午餐',dinerCount:110},{meal:'晚餐',dinerCount:90}],
details:[
{meal:'午餐',foodName:'糖醋排骨',spec:'350g/标准份',dinerCount:110,servings:110,needSeasoning:true,actualRatio:0.95},
{meal:'午餐',foodName:'米饭',spec:'250g/标准份',dinerCount:110,servings:220,needSeasoning:false,actualRatio:0.98},
{meal:'午餐',foodName:'鱼香肉丝',spec:'400g/标准份',dinerCount:110,servings:110,needSeasoning:true,actualRatio:0.99},
{meal:'晚餐',foodName:'米饭',spec:'250g/标准份',dinerCount:90,servings:180,needSeasoning:false,actualRatio:0.98},
{meal:'晚餐',foodName:'红烧肉',spec:'500g/标准份',dinerCount:90,servings:90,needSeasoning:true,actualRatio:1.04},
{meal:'晚餐',foodName:'清蒸鲈鱼',spec:'600g/标准份',dinerCount:90,servings:90,needSeasoning:false,actualRatio:0.99}
]},
{orderNo:'CO-1005',dayOffset:0,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'16:00',startTime:'14:05:00',successTime:'14:06:11',actualRatio:1.01,
meals:[{meal:'晚餐',dinerCount:95}],
details:[
{meal:'晚餐',foodName:'麻婆豆腐',spec:'400g/标准份',dinerCount:95,servings:95,needSeasoning:true,actualRatio:1.01},
{meal:'晚餐',foodName:'炒时蔬',spec:'300g/标准份',dinerCount:95,servings:95,needSeasoning:false,actualRatio:1.05},
{meal:'晚餐',foodName:'冬瓜排骨汤',spec:'400g/标准份',dinerCount:95,servings:95,needSeasoning:true,actualRatio:1.03}
]},
{orderNo:'CO-1006',dayOffset:0,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'06:30',startTime:'05:20:00',successTime:'05:20:58',actualRatio:0.99,
meals:[{meal:'早餐',dinerCount:75}],
details:[
{meal:'早餐',foodName:'馒头',spec:'100g/标准份',dinerCount:75,servings:150,needSeasoning:false,actualRatio:1.01},
{meal:'早餐',foodName:'煮鸡蛋',spec:'60g/标准份',dinerCount:75,servings:75,needSeasoning:false,actualRatio:1.02},
{meal:'早餐',foodName:'豆浆',spec:'350g/标准份',dinerCount:75,servings:75,needSeasoning:false,actualRatio:0.98}
]},
{orderNo:'CO-1007',dayOffset:0,genType:'auto',genStatus:'generating',creator:'系统',deliverTime:'10:00',startTime:'09:40:00',successTime:'',actualRatio:0.98,
meals:[{meal:'午餐',dinerCount:130}],
details:[
{meal:'午餐',foodName:'宫保鸡丁',spec:'500g/标准份',dinerCount:130,servings:130,needSeasoning:true,actualRatio:0.96},
{meal:'午餐',foodName:'鱼香肉丝',spec:'400g/标准份',dinerCount:130,servings:130,needSeasoning:true,actualRatio:0.99},
{meal:'午餐',foodName:'紫菜蛋花汤',spec:'350g/标准份',dinerCount:130,servings:130,needSeasoning:false,actualRatio:1.00},
{meal:'午餐',foodName:'米饭',spec:'250g/标准份',dinerCount:130,servings:260,needSeasoning:false,actualRatio:0.98}
]},
{orderNo:'CO-1008',dayOffset:0,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'16:00',startTime:'14:20:00',successTime:'14:21:05',actualRatio:0.99,
meals:[{meal:'晚餐',dinerCount:85}],
details:[
{meal:'晚餐',foodName:'糖醋里脊',spec:'350g/标准份',dinerCount:85,servings:85,needSeasoning:true,actualRatio:0.96},
{meal:'晚餐',foodName:'清炒西兰花',spec:'300g/标准份',dinerCount:85,servings:85,needSeasoning:false,actualRatio:1.02},
{meal:'晚餐',foodName:'酸奶',spec:'200g/标准份',dinerCount:85,servings:85,needSeasoning:false,actualRatio:0.97}
]},
{orderNo:'CO-1009',dayOffset:-1,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'10:00',startTime:'09:20:00',successTime:'09:21:30',actualRatio:0.99,
meals:[{meal:'午餐',dinerCount:90}],
details:[
{meal:'午餐',foodName:'番茄炒蛋',spec:'400g/标准份',dinerCount:90,servings:90,needSeasoning:false,actualRatio:0.97},
{meal:'午餐',foodName:'炒时蔬',spec:'300g/标准份',dinerCount:90,servings:90,needSeasoning:false,actualRatio:1.05}
]},
{orderNo:'CO-1010',dayOffset:-1,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'16:00',startTime:'14:10:00',successTime:'14:11:45',actualRatio:1.02,
meals:[{meal:'晚餐',dinerCount:90}],
details:[
{meal:'晚餐',foodName:'麻婆豆腐',spec:'400g/标准份',dinerCount:90,servings:90,needSeasoning:true,actualRatio:1.01},
{meal:'晚餐',foodName:'红烧肉',spec:'500g/标准份',dinerCount:90,servings:90,needSeasoning:true,actualRatio:0.98}
]},
{orderNo:'CO-1011',dayOffset:-1,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'06:30',startTime:'05:15:00',successTime:'05:16:02',actualRatio:0.99,
meals:[{meal:'早餐',dinerCount:70}],
details:[
{meal:'早餐',foodName:'白粥',spec:'300g/标准份',dinerCount:70,servings:70,needSeasoning:false,actualRatio:0.99},
{meal:'早餐',foodName:'茶叶蛋',spec:'60g/标准份',dinerCount:70,servings:70,needSeasoning:true,actualRatio:1.03},
{meal:'早餐',foodName:'豆浆',spec:'350g/标准份',dinerCount:70,servings:70,needSeasoning:false,actualRatio:0.98}
]},
{orderNo:'CO-1012',dayOffset:-1,genType:'auto',genStatus:'completed',creator:'系统',deliverTime:'10:00',startTime:'09:10:00',successTime:'09:11:10',actualRatio:1.00,
meals:[{meal:'午餐',dinerCount:100}],
details:[
{meal:'午餐',foodName:'宫保鸡丁',spec:'500g/标准份',dinerCount:100,servings:100,needSeasoning:true,actualRatio:0.96},
{meal:'午餐',foodName:'清蒸鲈鱼',spec:'600g/标准份',dinerCount:100,servings:100,needSeasoning:false,actualRatio:1.03},
{meal:'午餐',foodName:'米饭',spec:'250g/标准份',dinerCount:100,servings:200,needSeasoning:false,actualRatio:0.98}
]}
];
/* 基于名称的确定性哈希 */
function nameHash(s){
var h=0;
for(var i=0;i<s.length;i++){h=(h*31+s.charCodeAt(i))%1000;}
return h;
}
/* ========== 派生:为订单每个餐品补齐食材明细与推荐/实际数值 ========== */
function deriveDetail(order){
return order.details.map(function(d){
var bom=recipeBom[d.foodName]||[];
var baseRatio=d.actualRatio!=null?d.actualRatio:(order.actualRatio||1);
var perDishGram=bom.reduce(function(s,i){return s+i.perServing;},0);
var recRaw=parseFloat((perDishGram*d.servings/1000).toFixed(2));
var recCooked=parseFloat((recRaw*0.85).toFixed(2));
var ingredients=bom.map(function(ing){
var ingRec=parseFloat((ing.perServing*d.servings/1000).toFixed(3));
var offset=(nameHash(ing.name)%13-6)/100;
var ingRatio=baseRatio*(1+offset);
var ingAct=parseFloat((ingRec*ingRatio).toFixed(3));
return {name:ing.name,type:ing.type,perServing:ing.perServing,rec:ingRec,act:ingAct};
});
var actRaw=parseFloat(ingredients.reduce(function(s,i){return s+i.act;},0).toFixed(2));
return {
meal:d.meal,foodName:d.foodName,spec:d.spec,dinerCount:d.dinerCount,servings:d.servings,needSeasoning:d.needSeasoning,
recRaw:recRaw,recCooked:recCooked,actRaw:actRaw,ingredients:ingredients
};
});
}
/* ========== 差异率 ========== */
function diffInfo(act,rec){
if(!rec)return {cls:'flat',text:'— 持平'};
var pct=(act-rec)/rec*100;
if(pct>1)return {cls:'tag-red',text:'↑ 超 '+pct.toFixed(1)+'%'};
if(pct<-1)return {cls:'tag-green',text:'↓ 省 '+Math.abs(pct).toFixed(1)+'%'};
return {cls:'tag-gray',text:'— 持平'};
}
/* ========== 菜品种类(去重) ========== */
function distinctDishCount(o){
var set={};
o.details.forEach(function(d){set[d.foodName]=1;});
return Object.keys(set).length;
}
/* ========== 渲染状态 ========== */
var currentPage=1;
var pageSize=10;
var filteredOrders=[];
function applyFilter(){currentPage=1;renderList();}
function resetFilter(){
document.getElementById('fOrderNo').value='';
document.getElementById('fStatus').value='';
document.getElementById('fMeal').value='';
document.getElementById('fDate').value='';
currentPage=1;renderList();
}
function buildFilteredOrders(){
var fOrderNo=document.getElementById('fOrderNo').value.trim();
var fStatus=document.getElementById('fStatus').value;
var fMeal=document.getElementById('fMeal').value;
var fDate=document.getElementById('fDate').value;
return genOrders.filter(function(o){
if(fOrderNo&&o.orderNo.indexOf(fOrderNo)<0)return false;
if(fStatus&&o.genStatus!==fStatus)return false;
if(fMeal&&!o.meals.some(function(m){return m.meal===fMeal;}))return false;
if(fDate&&dateOffsetStr(o.dayOffset)!==fDate)return false;
return true;
});
}
function renderList(){
filteredOrders=buildFilteredOrders();
renderPage();
}
function renderPage(){
var total=filteredOrders.length;
var totalPages=Math.max(1,Math.ceil(total/pageSize));
if(currentPage<1)currentPage=1;
if(currentPage>totalPages)currentPage=totalPages;
var start=(currentPage-1)*pageSize;
var items=filteredOrders.slice(start,start+pageSize);
document.getElementById('tableSubTitle').textContent='共 '+total+' 单';
var empty=document.getElementById('historyEmpty');
empty.style.display=total?'none':'block';
var body=document.getElementById('historyTableBody');
if(!total){body.innerHTML='';renderPagination(0);return;}
var html='';
items.forEach(function(o,i){
var seq=(currentPage-1)*pageSize+i+1;
html+=renderOrderRow(o,seq);
});
body.innerHTML=html;
renderPagination(total);
}
function renderOrderRow(o,seq){
var genTypeTag='<span class="tag tag-blue">⚡ 自动</span>';
var statusTag=o.genStatus==='generating'
?'<span class="tag tag-cyan">生成中</span>'
:'<span class="tag tag-green">已完成</span>';
var mealCells=o.meals.map(function(m){return '<div>'+m.meal+'</div>';}).join('');
var countCells=o.meals.map(function(m){return '<div>'+m.dinerCount+'</div>';}).join('');
var recipeDate=dateOffsetStr(o.dayOffset);
var deliverDate=dateOffsetStr(o.dayOffset+1);
var successText=o.genStatus==='generating'?'—':(recipeDate+' '+o.successTime);
var html='<tr>';
html+='<td style="color:var(--text-secondary);">'+seq+'</td>';
html+='<td><b>'+o.orderNo+'</b></td>';
html+='<td>'+recipeDate+'</td>';
html+='<td>'+genTypeTag+'</td>';
html+='<td>'+statusTag+'</td>';
html+='<td class="meal-cell">'+mealCells+'</td>';
html+='<td class="meal-cell">'+countCells+'</td>';
html+='<td>'+distinctDishCount(o)+'</td>';
html+='<td style="color:var(--text-secondary);">'+deliverDate+' '+o.deliverTime+'</td>';
html+='<td style="color:var(--text-secondary);">'+recipeDate+' '+o.startTime+'</td>';
html+='<td style="color:var(--text-secondary);">'+successText+'</td>';
html+='<td>'+o.creator+'</td>';
html+='<td><span class="link" onclick="openDetail(\''+o.orderNo+'\')">数据明细</span></td>';
html+='</tr>';
return html;
}
/* ========== 分页 ========== */
function renderPagination(total){
var totalPages=Math.max(1,Math.ceil(total/pageSize));
var start=total?((currentPage-1)*pageSize+1):0;
var end=Math.min(currentPage*pageSize,total);
var html='<span>显示 '+start+'-'+end+' 条,共 '+total+' 条</span>';
html+='<button class="page-btn" '+(currentPage===1?'disabled':'')+' onclick="goPage('+(currentPage-1)+')"></button>';
for(var p=1;p<=totalPages;p++){
html+='<button class="page-btn'+(p===currentPage?' active':'')+'" onclick="goPage('+p+')">'+p+'</button>';
}
html+='<button class="page-btn" '+(currentPage===totalPages?'disabled':'')+' onclick="goPage('+(currentPage+1)+')"></button>';
document.getElementById('pagination').innerHTML=html;
}
function goPage(p){
var totalPages=Math.max(1,Math.ceil(filteredOrders.length/pageSize));
if(p<1||p>totalPages)return;
currentPage=p;
renderPage();
}
/* ========== 数据明细弹窗 ========== */
function openDetail(orderNo){
var o=genOrders.find(function(x){return x.orderNo===orderNo;});
if(!o)return;
document.getElementById('detailOrderNo').textContent=orderNo;
/* 概要 */
var recipeDate=dateOffsetStr(o.dayOffset);
var deliverDate=dateOffsetStr(o.dayOffset+1);
var statusText=o.genStatus==='generating'?'生成中':'已完成';
var summary='';
summary+='<div class="detail-summary__item"><span class="detail-summary__label">食谱日期</span><span class="detail-summary__value">'+recipeDate+'</span></div>';
summary+='<div class="detail-summary__item"><span class="detail-summary__label">生成方式</span><span class="detail-summary__value">自动</span></div>';
summary+='<div class="detail-summary__item"><span class="detail-summary__label">生成状态</span><span class="detail-summary__value">'+statusText+'</span></div>';
summary+='<div class="detail-summary__item"><span class="detail-summary__label">生成餐次</span><span class="detail-summary__value">'+o.meals.map(function(m){return m.meal+' '+m.dinerCount+'人';}).join('、')+'</span></div>';
summary+='<div class="detail-summary__item"><span class="detail-summary__label">预定配送时间</span><span class="detail-summary__value">'+deliverDate+' '+o.deliverTime+'</span></div>';
summary+='<div class="detail-summary__item"><span class="detail-summary__label">创建人</span><span class="detail-summary__value">'+o.creator+'</span></div>';
summary+='<div class="detail-summary__item"><span class="detail-summary__label">菜品种类</span><span class="detail-summary__value">'+distinctDishCount(o)+'</span></div>';
document.getElementById('detailSummary').innerHTML=summary;
/* 餐品列表 */
var details=deriveDetail(o);
var html='';
details.forEach(function(d){
var seasonTag=d.needSeasoning?'<span class="tag tag-blue">✓ 是</span>':'<span class="tag tag-gray">✗ 否</span>';
var diff=diffInfo(d.actRaw,d.recRaw);
html+='<tr class="dish-row" onclick="toggleIng(this)">';
html+='<td><span class="expand-arrow">▶</span></td>';
html+='<td><span class="tag tag-blue">'+d.meal+'</span></td>';
html+='<td style="font-weight:600;">'+d.foodName+'</td>';
html+='<td>'+d.spec+'</td>';
html+='<td>'+d.dinerCount+'</td>';
html+='<td>'+d.servings+'</td>';
html+='<td>'+seasonTag+'</td>';
html+='<td><b>'+d.recRaw.toFixed(2)+'</b></td>';
html+='<td>'+d.recCooked.toFixed(2)+'</td>';
html+='<td><b>'+d.actRaw.toFixed(2)+'</b></td>';
html+='<td><span class="tag '+diff.cls+'">'+diff.text+'</span></td>';
html+='</tr>';
html+='<tr class="ingredient-row" style="display:none;">';
html+='<td colspan="11">'+buildIngredientsDetail(d)+'</td>';
html+='</tr>';
});
document.getElementById('detailDishBody').innerHTML=html;
document.getElementById('modal-detail').classList.add('show');
}
function closeDetail(){document.getElementById('modal-detail').classList.remove('show');}
document.querySelector('.modal-mask').addEventListener('click',function(e){if(e.target===this)closeDetail();});
function buildIngredientsDetail(d){
var html='<table class="ing-table"><thead><tr><th>食材名称</th><th>类别</th><th>每份用量(g)</th><th>AI 推荐总量(kg)</th><th>实际用量(kg)</th><th>差异率</th></tr></thead><tbody>';
d.ingredients.forEach(function(ing){
var cls=ING_TYPE_CLS[ing.type]||'';
var diff=diffInfo(ing.act,ing.rec);
html+='<tr>';
html+='<td>'+ing.name+'</td>';
html+='<td><span class="ing-type '+cls+'">'+ing.type+'</span></td>';
html+='<td>'+ing.perServing+'</td>';
html+='<td>'+ing.rec.toFixed(3)+'</td>';
html+='<td>'+ing.act.toFixed(3)+'</td>';
html+='<td><span class="tag '+diff.cls+'">'+diff.text+'</span></td>';
html+='</tr>';
});
html+='</tbody></table>';
return html;
}
function toggleIng(tr){
var next=tr.nextElementSibling;
var arrow=tr.querySelector('.expand-arrow');
if(next&&next.classList.contains('ingredient-row')){
var isOpen=next.style.display!=='none';
next.style.display=isOpen?'none':'table-row';
tr.classList.toggle('expanded',!isOpen);
arrow.classList.toggle('expanded',!isOpen);
}
}
/* ========== 初始化 ========== */
renderList();
</script>
</body>
</html>
+1 -143
View File
@@ -268,7 +268,7 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<div class="action-bar">
<button class="btn btn-primary" onclick="openModal('modal-order')"> 新建订单</button>
<button class="btn" style="background:#722ED1;color:#fff;" onclick="openModal('modal-recipe-order')">📋 由食谱快速创建订单</button>
<button class="btn" style="background:#13C2C2;color:#fff;" onclick="openModal('modal-gen-history')">📜 净菜订单生成历史</button>
<a class="btn" style="background:#13C2C2;color:#fff;text-decoration:none;" href="prod-clean-order-history.html">📜 净菜订单生成历史</a>
</div>
<div class="table-card">
<div class="table-card__header"><div class="table-card__title">净菜订单</div></div>
@@ -398,34 +398,6 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
</div>
</div>
<!-- 弹窗:净菜订单生成历史 -->
<div class="modal-mask" id="modal-gen-history">
<div class="modal" style="width:1080px;">
<div class="modal__header">
<div class="modal__title">净菜订单生成历史</div>
<button class="modal__close" onclick="closeModal('modal-gen-history')"></button>
</div>
<div class="modal__body">
<!-- 筛选 + 订单切换 -->
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px;">
<div class="filter-item"><label>食谱日期</label><input class="filter-input" type="date" id="ghDate" onchange="renderGenHistory()" style="min-width:150px;"></div>
<button class="btn btn-default" onclick="resetGenHistoryFilter()">重置</button>
</div>
<!-- 生成记录明细(订单信息与餐品明细合并) -->
<div style="overflow-x:auto;">
<table class="ing-table">
<thead><tr><th>食谱日期</th><th>餐次</th><th>餐品名称</th><th>就餐人数</th><th>建议熟重(kg)</th><th>内配料(是否)</th><th>规格</th><th>打包份数</th><th>预定配送时间</th><th>开始生成时间</th><th>生成成功时间</th></tr></thead>
<tbody id="genHistoryInfoBody"></tbody>
</table>
</div>
<div id="genHistoryEmpty" style="display:none;text-align:center;color:var(--text-placeholder);padding:40px 0;">暂无符合条件的生成记录</div>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeModal('modal-gen-history')">关闭</button>
</div>
</div>
</div>
<script>
/* ========== 弹窗开关 ========== */
function openModal(id){
@@ -786,98 +758,6 @@ function genTypeTag(type){
:'<span class="tag tag-gray">✍ 手动</span>';
}
/* ========== 净菜订单生成历史 ========== */
var genHistory=[
{
orderNo:'CO-009',recipeDate:'2026-05-20',deliverTime:'2026-05-21 10:00',startTime:'2026-05-20 09:30:00',successTime:'2026-05-20 09:31:12',
details:[
{meal:'午餐',foodName:'宫保鸡丁',rawWeight:60,cookedWeight:51,dinerCount:120,needSeasoning:true,servings:120,avgWeight:0.5},
{meal:'午餐',foodName:'清蒸鲈鱼',rawWeight:60,cookedWeight:52.8,dinerCount:100,needSeasoning:false,servings:100,avgWeight:0.6}
]
},
{
orderNo:'CO-010',recipeDate:'2026-05-20',deliverTime:'2026-05-21 06:30',startTime:'2026-05-20 05:30:00',successTime:'2026-05-20 05:31:03',
details:[
{meal:'早餐',foodName:'小米粥',rawWeight:24,cookedWeight:20.4,dinerCount:80,needSeasoning:false,servings:80,avgWeight:0.3},
{meal:'早餐',foodName:'煮鸡蛋',rawWeight:4.8,cookedWeight:4.1,dinerCount:80,needSeasoning:false,servings:80,avgWeight:0.06}
]
},
{
orderNo:'CO-011',recipeDate:'2026-05-20',deliverTime:'2026-05-21 16:00',startTime:'2026-05-20 14:30:00',successTime:'2026-05-20 14:31:11',
details:[
{meal:'晚餐',foodName:'红烧肉',rawWeight:50,cookedWeight:42.5,dinerCount:100,needSeasoning:true,servings:100,avgWeight:0.5},
{meal:'晚餐',foodName:'清蒸鲈鱼',rawWeight:60,cookedWeight:52.8,dinerCount:100,needSeasoning:false,servings:100,avgWeight:0.6}
]
},
{
orderNo:'CO-012',recipeDate:'2026-05-19',deliverTime:'2026-05-20 16:00',startTime:'2026-05-19 14:30:00',successTime:'2026-05-19 14:31:15',
details:[
{meal:'晚餐',foodName:'麻婆豆腐',rawWeight:36,cookedWeight:30.6,dinerCount:90,needSeasoning:true,servings:90,avgWeight:0.4}
]
},
{
orderNo:'CO-013',recipeDate:'2026-05-20',deliverTime:'2026-05-21 14:00',startTime:'2026-05-20 13:30:00',successTime:'2026-05-20 13:31:02',
details:[
{meal:'加餐',foodName:'水果拼盘',rawWeight:18,cookedWeight:18,dinerCount:60,needSeasoning:false,servings:60,avgWeight:0.3}
]
}
];
function pad2(n){return String(n).padStart(2,'0');}
function formatDateTime(d){
return d.getFullYear()+'-'+pad2(d.getMonth()+1)+'-'+pad2(d.getDate())+' '+pad2(d.getHours())+':'+pad2(d.getMinutes())+':'+pad2(d.getSeconds());
}
function genSeasoningTag(needSeasoning){
return needSeasoning
?'<span class="tag tag-blue">✓ 是</span>'
:'<span class="tag tag-gray">✗ 否</span>';
}
var genHistoryFiltered=[];
function renderGenHistory(){
var date=document.getElementById('ghDate').value;
genHistoryFiltered=genHistory.filter(function(r){
if(date&&r.recipeDate!==date)return false;
return true;
});
var empty=document.getElementById('genHistoryEmpty');
empty.style.display=genHistoryFiltered.length?'none':'block';
/* 默认展示最近一次(数组首位) */
if(genHistoryFiltered.length>0){
renderGenHistoryOrder(genHistoryFiltered[0].orderNo);
}else{
document.getElementById('genHistoryInfoBody').innerHTML='';
}
}
function renderGenHistoryOrder(orderNo){
var rec=genHistoryFiltered.find(function(r){return r.orderNo===orderNo;});
if(!rec)rec=genHistory.find(function(r){return r.orderNo===orderNo;});
if(!rec)return;
/* 订单信息与餐品明细合并为一张表:每条明细一行,重复订单级字段 */
var infoBody=document.getElementById('genHistoryInfoBody');
var html='';
rec.details.forEach(function(d){
var spec=getDishSpecForRecipe(d.foodName);
var specLabel=spec.baseGram+'g/'+spec.portion;
html+='<tr>'
+'<td>'+rec.recipeDate+'</td>'
+'<td>'+d.meal+'</td>'
+'<td style="font-weight:500;">'+d.foodName+'</td>'
+'<td>'+d.dinerCount+'</td>'
+'<td>'+(parseFloat(d.cookedWeight).toFixed(2))+' kg</td>'
+'<td>'+genSeasoningTag(d.needSeasoning)+'</td>'
+'<td>'+specLabel+'</td>'
+'<td>'+d.servings+'</td>'
+'<td>'+rec.deliverTime+'</td>'
+'<td>'+rec.startTime+'</td>'
+'<td>'+rec.successTime+'</td>'
+'</tr>';
});
infoBody.innerHTML=html;
}
function resetGenHistoryFilter(){
document.getElementById('ghDate').value='';
renderGenHistory();
}
/* ========== 渲染订单表格 ========== */
function renderOrderTable(){
var tbody=document.getElementById('orderTableBody');
@@ -1596,11 +1476,6 @@ function submitRecipeOrder(){
if(missing.length>0){alert('以下餐次尚未填写就餐人数:'+missing.join('、'));return;}
/* 由食谱创建一个包含所选餐次全部菜品的订单,订单初始状态为「生成中」 */
var daily=getDailyMenu(recipeDate);
/* 记录本次生成历史:开始时间与模拟成功时间(1~2 分钟后) */
var genStart=new Date();
var genStartStr=formatDateTime(genStart);
var genSuccessStr=formatDateTime(new Date(genStart.getTime()+(60+Math.round(Math.random()*60))*1000));
var genDetails=[];
var dishMap={};
mealIndices.forEach(function(mi){
var input=document.querySelector('#dailyMenuBody .meal-diner-input[data-meal-idx="'+mi+'"]');
@@ -1612,25 +1487,9 @@ function submitRecipeOrder(){
/* 同菜品跨餐次:累加人数(按总人数核算) */
dishMap[dishName].dinerCount+=dinerCount;
}
/* 收集生成历史明细 */
var ghSpec=getDishSpecForRecipe(dishName);
var ghBaseGram=ghSpec.baseGram||500;
var ghServings=ghBaseGram>4000?Math.ceil(ghBaseGram*dinerCount/4000):dinerCount;
var ghRawWeight=parseFloat((ghBaseGram*dinerCount/1000).toFixed(2));
genDetails.push({
meal:MEAL_NAMES[mi],foodName:dishName,
rawWeight:ghRawWeight,cookedWeight:parseFloat((ghRawWeight*0.85).toFixed(2)),
dinerCount:dinerCount,needSeasoning:recipeSeasoningMap[dishName]===true,
servings:ghServings,avgWeight:parseFloat((ghRawWeight/ghServings).toFixed(2))
});
});
});
var orderNo='CO-'+String(orders.length+1).padStart(3,'0');
/* 写入净菜订单生成历史(订单粒度) */
genHistory.unshift({
orderNo:orderNo,recipeDate:recipeDate,deliverTime:deliveryTime.replace('T',' '),
startTime:genStartStr,successTime:genSuccessStr,details:genDetails
});
var newLines=[];
var di=0;
for(var name in dishMap){
@@ -1691,7 +1550,6 @@ function sanitizeIntegerInput(el){el.value=el.value.replace(/[^\d]/g,'');}
recipeSpecMap={};
onRecipeDateChange();
}
if(id==='modal-gen-history'){document.getElementById('ghDate').value='';renderGenHistory();}
document.getElementById(id).classList.add('show');
};
})();