feat: 结算设备复秤对账(03收银页对账条+08补齐明细智能推荐) + 营养管理端报表中心两页面补齐(场所经营分析/场所目标考核)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-20 17:02:25 +08:00
co-authored by Claude Opus 4.7
parent ee6041257f
commit dd0331bb98
6 changed files with 1549 additions and 26 deletions
@@ -1107,6 +1107,54 @@
.toast--success { background: rgba(82, 196, 26, 0.92); }
.toast--warn { background: rgba(250, 173, 22, 0.92); }
.toast--error { background: rgba(245, 34, 45, 0.92); }
/* ============ 复秤对账条 ============ */
.weigh-check {
padding: 9px 24px;
background: rgba(82, 196, 26, 0.06);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
transition: background 0.25s, border-color 0.25s;
}
.weigh-check__row {
display: flex;
align-items: center;
gap: 20px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
.weigh-check__item strong {
font-family: Menlo, Consolas, monospace;
font-size: 14px;
color: #fff;
margin-left: 6px;
}
.weigh-check__diff strong { color: #4ade80; }
.weigh-check__status {
margin-left: auto;
font-weight: 700;
color: #4ade80;
}
.weigh-check--warn {
background: rgba(251, 146, 60, 0.1);
border-bottom-color: rgba(251, 146, 60, 0.45);
}
.weigh-check--warn .weigh-check__diff strong { color: #fb923c; }
.weigh-check--warn .weigh-check__status {
color: #fb923c;
animation: weighBlink 1.2s infinite;
}
@keyframes weighBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
/* 补齐明细按钮警示呼吸态 */
.header-btn--warn {
background: rgba(251, 146, 60, 0.2) !important;
border-color: rgba(251, 146, 60, 0.6) !important;
color: #fb923c !important;
animation: weighBlink 1.2s infinite;
}
</style>
</head>
<body>
@@ -1124,6 +1172,7 @@
<button class="demo-control__btn active" data-scene="normal" onclick="switchScene('normal')">余额充足</button>
<button class="demo-control__btn" data-scene="low-balance" onclick="switchScene('low-balance')">余额不足</button>
<button class="demo-control__btn demo-control__btn--toggle" id="couponToggle" onclick="toggleCouponPreset()">代金券预载</button>
<button class="demo-control__btn demo-control__btn--toggle" id="weighToggle" onclick="toggleWeighMode()">复秤正常</button>
<button class="demo-control__btn" onclick="resetDemo()">&#x21BB;</button>
</div>
</div>
@@ -1160,12 +1209,22 @@
<span class="items-col__count" id="itemCount">7 件</span>
</div>
<div class="items-col__actions">
<a class="header-btn header-btn--primary" href="08-reconcile.html">
<a class="header-btn header-btn--primary" id="reconcileBtn" href="08-reconcile.html">
&#x270F;&#xFE0F; 补齐明细
</a>
</div>
</div>
<!-- 复秤对账条(自动读秤,对比记录总重与实际复秤总净重) -->
<div class="weigh-check" id="weighCheck">
<div class="weigh-check__row">
<span class="weigh-check__item">&#x1F4DD; 记录取餐 <strong id="wcRecord">1240g</strong></span>
<span class="weigh-check__item">&#x2696;&#xFE0F; 实际复秤 <strong id="wcActual">1240g</strong></span>
<span class="weigh-check__item weigh-check__diff">差额 <strong id="wcDiff">0g</strong></span>
<span class="weigh-check__status" id="wcStatus">&#x2705; 核对通过</span>
</div>
</div>
<div class="items-list" id="itemsList">
<!-- 表头(独立表格,固定在列表顶部) -->
<table class="items-table">
@@ -2031,8 +2090,59 @@
showToast('演示已重置', 'success');
}
/* ==================== 复秤对账 ==================== */
/* 记录取餐合计(与明细静态数据一致);复秤三态:正常/偏多(漏记)/偏少(多记) */
var RECORD_WEIGHT = 1240;
var WEIGH_MODES = [
{ key: 'normal', label: '复秤正常', actual: 1240 },
{ key: 'over', label: '漏记2条', actual: 1490 },
{ key: 'under', label: '多记2条', actual: 1040 }
];
var weighModeIdx = 0;
/* 循环切换复秤场景(演示自动读秤) */
function toggleWeighMode() {
weighModeIdx = (weighModeIdx + 1) % WEIGH_MODES.length;
var m = WEIGH_MODES[weighModeIdx];
var toggle = document.getElementById('weighToggle');
toggle.textContent = m.label;
toggle.classList.toggle('on', weighModeIdx !== 0);
refreshWeighCheck();
showToast('已切换复秤:' + m.label, weighModeIdx === 0 ? 'success' : 'warn');
}
/* 刷新复秤对账条:计算差额、判定阈值、联动警示与跳转 */
function refreshWeighCheck() {
var m = WEIGH_MODES[weighModeIdx];
var actual = m.actual;
var diff = actual - RECORD_WEIGHT;
var threshold = Math.max(50, RECORD_WEIGHT * 0.05);
var over = Math.abs(diff) > threshold;
document.getElementById('wcRecord').textContent = RECORD_WEIGHT + 'g';
document.getElementById('wcActual').textContent = actual + 'g';
document.getElementById('wcDiff').textContent = (diff > 0 ? '+' : '') + diff + 'g';
var box = document.getElementById('weighCheck');
var status = document.getElementById('wcStatus');
var btn = document.getElementById('reconcileBtn');
if (over) {
box.classList.add('weigh-check--warn');
status.innerHTML = (diff > 0) ? '&#x26A0;&#xFE0F; 疑似漏记,请补齐' : '&#x26A0;&#xFE0F; 疑似多记,请核对';
btn.classList.add('header-btn--warn');
} else {
box.classList.remove('weigh-check--warn');
status.innerHTML = '&#x2705; 核对通过';
btn.classList.remove('header-btn--warn');
}
/* 跳转补齐明细时携带当前复秤值 */
btn.href = '08-reconcile.html?actual=' + actual;
}
/* 初始化 */
switchPay('online');
refreshWeighCheck();
</script>
</body>
@@ -714,6 +714,105 @@
.toast--success { background: rgba(82, 196, 26, 0.92); }
.toast--warn { background: rgba(250, 173, 22, 0.92); }
.toast--error { background: rgba(245, 34, 45, 0.92); }
/* ============ 复秤对账横条 ============ */
.weigh-bar {
display: flex;
align-items: center;
gap: 22px;
padding: 9px 24px;
background: rgba(26, 43, 74, 0.4);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
transition: background 0.25s, border-color 0.25s;
}
.weigh-bar__title { font-weight: 700; color: #fff; letter-spacing: 1px; }
.weigh-bar__item strong {
font-family: Menlo, Consolas, monospace;
font-size: 14px;
color: #fff;
margin-left: 6px;
}
.weigh-bar__diff strong { color: #4ade80; }
.weigh-bar__status { margin-left: auto; font-weight: 700; }
.weigh-bar--ok .weigh-bar__status { color: #4ade80; }
.weigh-bar--warn {
background: rgba(251, 146, 60, 0.1);
border-bottom-color: rgba(251, 146, 60, 0.45);
}
.weigh-bar--warn .weigh-bar__diff strong { color: #fb923c; }
.weigh-bar--warn .weigh-bar__status {
color: #fb923c;
animation: weighBlink 1.2s infinite;
}
@keyframes weighBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
/* ============ 智能推荐卡片 ============ */
.rec-card {
background: rgba(0, 0, 0, 0.22);
border: 1px solid rgba(251, 191, 36, 0.28);
border-radius: 8px;
padding: 10px 12px;
font-size: 12px;
}
.rec-head {
color: #fbbf24;
font-weight: 700;
margin-bottom: 6px;
line-height: 1.5;
}
.rec-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
border-top: 1px dashed rgba(255, 255, 255, 0.06);
}
.rec-item__rank {
width: 18px; height: 18px; border-radius: 50%;
background: rgba(251, 191, 36, 0.18);
color: #fbbf24; font-size: 10px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.rec-item__body { flex: 1; min-width: 0; }
.rec-item__names {
color: #fff; font-weight: 600; line-height: 1.45;
overflow: hidden; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.rec-item__meta {
font-size: 10px; color: rgba(255, 255, 255, 0.5);
font-family: Menlo, Consolas, monospace; margin-top: 2px;
}
.rec-item__btn {
padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 700;
cursor: pointer; border: 1px solid; font-family: inherit; flex-shrink: 0;
transition: all 0.15s;
}
.rec-item__btn--add {
background: rgba(82, 196, 26, 0.14);
border-color: rgba(82, 196, 26, 0.4); color: #4ade80;
}
.rec-item__btn--add:hover { background: rgba(82, 196, 26, 0.3); color: #fff; }
.rec-item__btn--remove {
background: rgba(245, 34, 45, 0.12);
border-color: rgba(245, 34, 45, 0.35); color: #ff6b6b;
}
.rec-item__btn--remove:hover { background: rgba(245, 34, 45, 0.28); color: #fff; }
.rec-empty { color: rgba(255, 255, 255, 0.4); text-align: center; padding: 8px 0; }
/* 推荐徽标(左列列表内) */
.rec-badge {
display: inline-block; padding: 1px 6px; border-radius: 7px;
background: rgba(251, 191, 36, 0.18); color: #fbbf24;
border: 1px solid rgba(251, 191, 36, 0.4);
font-size: 9px; font-weight: 700; letter-spacing: 0.5px; flex-shrink: 0;
}
</style>
</head>
<body>
@@ -727,6 +826,15 @@
<span class="top-bar__meta">订单 CO-20260806-031</span>
</div>
<!-- 复秤对账横条(实际称重 vs 记录合计,读取结算台传入的复秤值) -->
<div class="weigh-bar" id="weighBar">
<span class="weigh-bar__title">&#x2696;&#xFE0F; 复秤对账</span>
<span class="weigh-bar__item">实际称重 <strong id="wbActual">--</strong></span>
<span class="weigh-bar__item">记录合计 <strong id="wbRecord">--</strong></span>
<span class="weigh-bar__item weigh-bar__diff">差额 <strong id="wbDiff">--</strong></span>
<span class="weigh-bar__status" id="wbStatus"></span>
</div>
<!-- 主内容 -->
<div class="main-area">
@@ -759,12 +867,18 @@
<!-- 右列:差额汇总 -->
<div class="summary-col">
<!-- 智能推荐(根据复秤差额推荐删/补) -->
<div id="recBlock" style="display:none;">
<div class="section-label">&#x1F4A1; 智能推荐</div>
<div class="rec-card" id="recCard" style="margin-top:8px;"></div>
</div>
<div>
<div class="section-label">金额变化对比</div>
<div class="compare-block" style="margin-top:8px;">
<div class="compare-row compare-row--old">
<span class="compare-row__label">原订单合计</span>
<span class="compare-row__value" id="originalAmount">&#xFFE5;46.60</span>
<span class="compare-row__value" id="originalAmount">&#xFFE5;69.40</span>
</div>
<div class="compare-row">
<span class="compare-row__label">本次修正</span>
@@ -778,7 +892,7 @@
</div>
<div class="compare-row compare-row--delta" id="deltaRow">
<span class="compare-row__label">应付金额</span>
<span class="compare-row__value" id="newPayable">&#xFFE5;41.60</span>
<span class="compare-row__value" id="newPayable">&#xFFE5;64.40</span>
</div>
</div>
</div>
@@ -810,20 +924,23 @@
<script>
/* ==================== Mock 数据 ==================== */
/* 原始订单金额(从 03-checkout 同步,演示用) */
var ORIGINAL_TOTAL = 46.60;
var ORIGINAL_TOTAL = 69.40;
var DISCOUNT = 5.00;
/*
* 当前订单明细(每条独立记录,允许同名菜品多条)
* status: normal | removed | added
* origin: recognized(已识别,原订单) | unmatched(从未识别取餐记录加入)
* 与 03-checkout 取餐明细保持一致(7 项,合计 1240g / ¥69.40)
*/
var currentItems = [
{ id: 1001, name: '清炒时蔬', cat: 'line', weight: 150, unitPrice: 4.00, status: 'normal', origin: 'recognized' },
{ id: 1002, name: '米饭', cat: 'line', weight: 250, unitPrice: 1.60, status: 'normal', origin: 'recognized' },
{ id: 1003, name: '红烧肉', cat: 'booth', weight: 180, unitPrice: 12.00, status: 'normal', origin: 'recognized' },
{ id: 1004, name: '鱼香肉丝', cat: 'booth', weight: 160, unitPrice: 7.50, status: 'normal', origin: 'recognized' },
{ id: 1005, name: '冬瓜排骨汤', cat: 'booth', weight: 300, unitPrice: 1.00, status: 'normal', origin: 'recognized' }
{ id: 1001, name: '清炒时蔬', cat: 'line', weight: 150, unitPrice: 4.00, status: 'normal', origin: 'recognized' },
{ id: 1002, name: '米饭', cat: 'line', weight: 250, unitPrice: 1.60, status: 'normal', origin: 'recognized' },
{ id: 1003, name: '红烧肉', cat: 'booth', weight: 180, unitPrice: 12.00, status: 'normal', origin: 'recognized' },
{ id: 1004, name: '鱼香肉丝', cat: 'booth', weight: 160, unitPrice: 7.50, status: 'normal', origin: 'recognized' },
{ id: 1005, name: '冬瓜排骨汤', cat: 'booth', weight: 300, unitPrice: 1.00, status: 'normal', origin: 'recognized' },
{ id: 1006, name: '卤鸡腿', cat: 'booth', weight: 120, unitPrice: 15.00, status: 'normal', origin: 'recognized' },
{ id: 1007, name: '凉拌木耳', cat: 'booth', weight: 80, unitPrice: 6.00, status: 'normal', origin: 'recognized' }
];
/*
@@ -847,6 +964,181 @@
/* 已加入订单的未识别 recordId 集合(用于在补齐列表标记「已加入」) */
var addedRecordIds = new Set();
/* ==================== 复秤对账 ==================== */
/* 实际复秤总净重:优先读取结算台跳转传入的 actual 参数,无则默认 1240g(正常) */
function parseActualWeight() {
var m = location.search.match(/[?&]actual=(\d+(?:\.\d+)?)/);
return m ? parseFloat(m[1]) : 1240;
}
var ACTUAL_WEIGHT = parseActualWeight();
var THRESHOLD_RATIO = 0.05;
/* 当前订单有效记录总重(排除已移除) */
function getRecordWeight() {
return currentItems.reduce(function(s, it) {
return s + (it.status === 'removed' ? 0 : it.weight);
}, 0);
}
/* 出入判定阈值:固定 50g 与记录总重 5% 取大者 */
function getThreshold() {
return Math.max(50, getRecordWeight() * THRESHOLD_RATIO);
}
/* 差额 = 实际复秤 - 记录总重;正=漏记,负=多记 */
function getDiff() {
return ACTUAL_WEIGHT - getRecordWeight();
}
/* 最高推荐方案内条目集合(用于左列列表多条目高亮) */
var topAddKeys = new Set();
var topRemoveKeys = new Set();
/* 当前渲染的方案列表(供一键应用按索引取值) */
var currentPlans = [];
/* 收集候选条目(漏记→未识别池,多记→当前订单有效项) */
function getCandidates() {
var diff = getDiff();
var cands = [];
if (diff > getThreshold()) {
unmatchedRecords.forEach(function(r) {
if (addedRecordIds.has(r.recordId)) return;
cands.push({ key: r.recordId, name: r.name, weight: r.weight, action: 'add' });
});
} else if (diff < -getThreshold()) {
currentItems.forEach(function(it) {
if (it.status === 'removed') return;
cands.push({ key: it.id, name: it.name, weight: it.weight, action: 'remove' });
});
}
return cands;
}
/* 组合推荐:枚举 1~MAX_K 条组合,按匹配度优先、条目数次优排序 */
var MAX_COMBO_K = 2;
function getPlans() {
var diff = getDiff();
var target = Math.abs(diff);
var cands = getCandidates();
var plans = [];
var n = cands.length;
for (var i = 0; i < n; i++) {
/* 单条方案 */
plans.push({
items: [cands[i]],
sum: cands[i].weight,
delta: Math.abs(cands[i].weight - target),
count: 1
});
/* 两条组合方案 */
for (var j = i + 1; j < n; j++) {
var sum2 = cands[i].weight + cands[j].weight;
plans.push({
items: [cands[i], cands[j]],
sum: sum2,
delta: Math.abs(sum2 - target),
count: 2
});
}
}
plans.sort(function(a, b) {
if (a.delta !== b.delta) return a.delta - b.delta;
return a.count - b.count;
});
return plans;
}
/* 更新最高推荐方案内全部条目(供左列列表多条目高亮) */
function computeTopRecommend() {
var plans = getPlans();
topAddKeys.clear();
topRemoveKeys.clear();
if (plans.length > 0) {
plans[0].items.forEach(function(c) {
if (c.action === 'add') topAddKeys.add(c.key);
else topRemoveKeys.add(c.key);
});
}
}
/* 渲染顶部复秤对账横条 */
function renderWeighBar() {
var record = getRecordWeight();
var diff = getDiff();
var over = Math.abs(diff) > getThreshold();
document.getElementById('wbActual').textContent = ACTUAL_WEIGHT + 'g';
document.getElementById('wbRecord').textContent = record + 'g';
var diffEl = document.getElementById('wbDiff');
diffEl.textContent = (diff > 0 ? '+' : '') + diff + 'g';
diffEl.style.color = over ? '#fb923c' : '#4ade80';
var bar = document.getElementById('weighBar');
var status = document.getElementById('wbStatus');
bar.classList.toggle('weigh-bar--warn', over);
bar.classList.toggle('weigh-bar--ok', !over);
if (!over) {
status.innerHTML = '&#x2705; 重量已对齐,可直接结算';
} else if (diff > 0) {
status.innerHTML = '&#x26A0;&#xFE0F; 疑似漏记 ' + diff + 'g,请补录';
} else {
status.innerHTML = '&#x26A0;&#xFE0F; 疑似多记 ' + (-diff) + 'g,请核对';
}
}
/* 渲染右列智能推荐卡片(组合方案列表) */
function renderRecommend() {
var diff = getDiff();
var over = Math.abs(diff) > getThreshold();
var block = document.getElementById('recBlock');
var card = document.getElementById('recCard');
if (!over) {
block.style.display = 'none';
currentPlans = [];
return;
}
var plans = getPlans();
if (plans.length === 0) {
block.style.display = 'none';
currentPlans = [];
return;
}
block.style.display = '';
var head = diff > 0
? '差额 +' + diff + 'g,疑似漏记,推荐补录'
: '差额 ' + diff + 'g,疑似多记,推荐移除';
var html = '<div class="rec-head">' + head + '</div>';
var top = plans.slice(0, 4);
currentPlans = top;
top.forEach(function(p, i) {
var isAdd = p.items[0].action === 'add';
var itemStr = p.items.map(function(c) { return c.name + ' ' + c.weight + 'g'; }).join(' + ');
var deltaStr = p.delta === 0 ? '精确匹配' : '差 ' + p.delta + 'g';
var btn = isAdd
? '<button class="rec-item__btn rec-item__btn--add" onclick="applyPlan(' + i + ')">+ 一键加入</button>'
: '<button class="rec-item__btn rec-item__btn--remove" onclick="applyPlan(' + i + ')">&#x2212; 一键移除</button>';
html += '<div class="rec-item">'
+ '<span class="rec-item__rank">' + (i + 1) + '</span>'
+ '<div class="rec-item__body">'
+ '<div class="rec-item__names">' + itemStr + '</div>'
+ '<div class="rec-item__meta">' + p.count + ' 条 · ' + deltaStr + '</div>'
+ '</div>'
+ btn
+ '</div>';
});
card.innerHTML = html;
}
/* 一键应用组合方案:连续加入/移除方案内全部条目 */
function applyPlan(idx) {
var plan = currentPlans[idx];
if (!plan) return;
plan.items.forEach(function(c) {
if (c.action === 'add') {
addFromUnmatched(c.key);
} else {
removeItem(c.key);
}
});
}
/* 单条小计金额 */
function subtotal(it) {
return it.weight ? it.weight / 100 * it.unitPrice : it.unitPrice;
@@ -859,6 +1151,7 @@
/* ================ 渲染:当前订单(表格化,对齐 03-checkout) ================ */
function renderCurrent() {
computeTopRecommend();
var html = ''
+ '<table class="items-table">'
+ ' <colgroup>'
@@ -898,6 +1191,9 @@
} else if (it.status === 'removed') {
badge = '<span class="change-badge change-badge--remove">已移除</span>';
}
if (topRemoveKeys.has(it.id)) {
badge += '<span class="rec-badge">&#x1F525; 推荐删除</span>';
}
/* 操作按钮:已移除→恢复,新加→直接删除,正常→移除 */
var opBtn = '';
@@ -950,6 +1246,7 @@
/* ================ 渲染:未识别取餐记录列表(添加商品 Tab) ================ */
function renderUnmatched() {
computeTopRecommend();
var html = ''
+ '<div class="unmatched-tip">'
+ ' <span class="unmatched-tip__icon">&#x26A0;&#xFE0F;</span>'
@@ -1001,6 +1298,7 @@
var catLabel = r.cat === 'line' ? '餐线' : '档口';
var catCls = r.cat === 'line' ? 'cell-cat--line' : 'cell-cat--booth';
var isAdded = addedRecordIds.has(r.recordId);
var recBadge = topAddKeys.has(r.recordId) ? '<span class="rec-badge">&#x1F525; 推荐补录</span>' : '';
var opBtn;
if (isAdded) {
opBtn = '<span class="op-btn op-btn--added">&#x2713; 已加</span>';
@@ -1011,7 +1309,7 @@
+ '<tr>'
+ ' <td class="cell-time">' + r.time + '</td>'
+ ' <td><span class="cell-cat ' + catCls + '">' + catLabel + '</span></td>'
+ ' <td><div class="um-name">' + r.name + '</div></td>'
+ ' <td><div class="um-name">' + r.name + recBadge + '</div></td>'
+ ' <td class="um-unit">&#xFFE5;' + r.unitPrice.toFixed(2) + '/100g</td>'
+ ' <td class="um-weight"><span class="um-weight__pill">' + r.weight + 'g</span></td>'
+ ' <td class="um-price">&#xFFE5;' + subtotal(r).toFixed(2) + '</td>'
@@ -1155,6 +1453,10 @@
var deltaRow = document.getElementById('deltaRow');
deltaRow.className = change >= 0 ? 'compare-row compare-row--delta' : 'compare-row compare-row--delta compare-row--delta-minus';
/* 复秤对账横条与智能推荐实时刷新 */
renderWeighBar();
renderRecommend();
}
/* 应用变更 */
@@ -1,5 +1,34 @@
# 结算设备 CHANGELOG
## 2026-08-20 | 收银页复秤对账 + 补齐明细智能推荐(03 / 08)
### 业务背景
结算收银流程中,顾客实际取餐总重与系统记录总重可能存在偏差——漏记未识别到人的取餐记录,或多记了应移除的项,收银员需人工核对。本次在收银页(`03-checkout`)与补齐明细页(`08-reconcile`)引入「复秤对账」能力:秤具自动回传实际总净重,系统对比记录总重,差额超阈值时主动警示并智能推荐补录/移除方案。
### 变更清单
- **`03-checkout.html` 收银页**:
- 取餐明细头部下方新增「复秤对账条」,自动读秤对比「记录取餐总重」与「实际复秤净重」,展示差额
- 顶部演示工具条新增「复秤正常」切换按钮,三态循环:复秤正常(1240g)/ 漏记 2 条(1490g)/ 多记 2 条(1040g)
- 差额超阈值(固定 50g 与记录总重 5% 取大)时:对账条橙色警示 + 状态文案(疑似漏记/多记)+「补齐明细」按钮呼吸警示态
- 跳转 `08-reconcile.html` 时携带 `?actual=xxx` 复秤值参数
- **`08-reconcile.html` 补齐明细页**:
- 顶部新增「复秤对账横条」:记录总重(按当前订单实时计算)/ 实际复秤 / 差额;重量对齐显示 ✅,超阈值警示
- 差额算法:实际复秤 − 记录总重,正=漏记(从未识别池补录),负=多记(从当前订单移除)
- 右列新增「智能推荐卡片」:枚举候选条目(漏记→未识别池,多记→当前订单有效项)的 1~2 条组合,按「匹配度优先、条目数次优」排序,展示 Top4 方案,每方案带「+ 一键加入 / − 一键移除」
- 最高推荐方案内条目在左列列表高亮「🔥 推荐补录 / 推荐删除」徽标
- 一键应用组合方案:连续加入/移除方案内全部条目
### 技术实现
- 复秤对账条/横条共用以 `Math.max(50, 记录总重 * 0.05)` 为出入判定阈值,`weighBlink` 呼吸动画驱动警示态
- 智能推荐采用组合枚举(单条 + 两条),`delta` 越接近差额目标、条目数越少越靠前
- 03 与 08 通过 URL 参数 `?actual=xxx` 串联复秤值,保证跨页语义一致
### 不变更
- 03-checkout 的支付方式体系、取餐明细表格化、代金券/混合支付逻辑保持不变
- 08-reconcile 的表格化结构、未识别取餐记录加入/移除流程、差额汇总与变更日志保持不变
---
## 2026-08-10 | 餐次管理模块新增(10-13 四页联动)
### 业务背景