feat: 结算设备复秤对账(03收银页对账条+08补齐明细智能推荐) + 营养管理端报表中心两页面补齐(场所经营分析/场所目标考核)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
ee6041257f
commit
dd0331bb98
@@ -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()">↻</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">
|
||||
✏️ 补齐明细
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 复秤对账条(自动读秤,对比记录总重与实际复秤总净重) -->
|
||||
<div class="weigh-check" id="weighCheck">
|
||||
<div class="weigh-check__row">
|
||||
<span class="weigh-check__item">📝 记录取餐 <strong id="wcRecord">1240g</strong></span>
|
||||
<span class="weigh-check__item">⚖️ 实际复秤 <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">✅ 核对通过</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) ? '⚠️ 疑似漏记,请补齐' : '⚠️ 疑似多记,请核对';
|
||||
btn.classList.add('header-btn--warn');
|
||||
} else {
|
||||
box.classList.remove('weigh-check--warn');
|
||||
status.innerHTML = '✅ 核对通过';
|
||||
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">⚖️ 复秤对账</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">💡 智能推荐</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">¥46.60</span>
|
||||
<span class="compare-row__value" id="originalAmount">¥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">¥41.60</span>
|
||||
<span class="compare-row__value" id="newPayable">¥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 = '✅ 重量已对齐,可直接结算';
|
||||
} else if (diff > 0) {
|
||||
status.innerHTML = '⚠️ 疑似漏记 ' + diff + 'g,请补录';
|
||||
} else {
|
||||
status.innerHTML = '⚠️ 疑似多记 ' + (-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 + ')">− 一键移除</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">🔥 推荐删除</span>';
|
||||
}
|
||||
|
||||
/* 操作按钮:已移除→恢复,新加→直接删除,正常→移除 */
|
||||
var opBtn = '';
|
||||
@@ -950,6 +1246,7 @@
|
||||
|
||||
/* ================ 渲染:未识别取餐记录列表(添加商品 Tab) ================ */
|
||||
function renderUnmatched() {
|
||||
computeTopRecommend();
|
||||
var html = ''
|
||||
+ '<div class="unmatched-tip">'
|
||||
+ ' <span class="unmatched-tip__icon">⚠️</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">🔥 推荐补录</span>' : '';
|
||||
var opBtn;
|
||||
if (isAdded) {
|
||||
opBtn = '<span class="op-btn op-btn--added">✓ 已加</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">¥' + r.unitPrice.toFixed(2) + '/100g</td>'
|
||||
+ ' <td class="um-weight"><span class="um-weight__pill">' + r.weight + 'g</span></td>'
|
||||
+ ' <td class="um-price">¥' + 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 四页联动)
|
||||
|
||||
### 业务背景
|
||||
|
||||
Reference in New Issue
Block a user