feat: 配送H5新模块 + 净菜三页物流改造 + 餐次历史内聚到统计页

- 新增 other-module/meal-delivery-h5/ 配送员端 H5 全套 9 页(登录/工作台/派单接听/抢单/订单详情/配送中/送达签收/历史/个人中心)
- 净菜内供/外售/订单三页物流模型统一,支持自营派单/自营抢单/三方物流三种配送方式,新增收货码 + 配送进度时间轴 + 物流详情弹窗
- 删除结算设备 10-meal-center/13-meal-history 两页,餐次控制内聚到 02-standby,餐次历史作为 Tab3 合并进 07-statistics
- 营养管理端新增 din-canteen-shift.html(场所排版设置),din-ingredient 详情页增加删除食材确认弹窗

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-12 18:35:19 +08:00
co-authored by Claude Opus 4.7
parent a16500068c
commit 2346465c90
29 changed files with 10070 additions and 2199 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<!-- Last updated: 2026-08-10 | Commit: 32bd018 -->
<!-- Last updated: 2026-08-10 | Commit: ff48f38 -->
# 项目约束
本项目所有工作必须严格遵守 `项目背景.md` 中定义的全部规范,包括:
+12
View File
@@ -312,6 +312,7 @@ const SITE_MAP = [
{ file:'din-canteen.html', label:'食堂管理' },
{ file:'din-canteen-add.html', label:'新增食堂' },
{ file:'din-canteen-line.html', label:'餐线管理' },
{ file:'din-canteen-shift.html', label:'场所排版设置' },
{ file:'din-dict-mgmt.html', label:'字典管理' },
{ file:'din-dict-config.html', label:'字典配置' },
{ file:'din-algo-population.html', label:'群体算法' },
@@ -872,6 +873,17 @@ const SITE_MAP = [
{label:'扫码溯源', file:'trace-query.html'},
{label:'溯源链路', file:'trace-detail.html'}
] },
{ name:'营养管理餐品配送', dir:'other-module/meal-delivery-h5', entry:'01-home.html', pages:[
{label:'配送员登录', file:'00-login.html'},
{label:'工作台首页(模块入口)', file:'01-home.html'},
{label:'派单接听 · 核心', file:'02-dispatch-incoming.html'},
{label:'抢单大厅', file:'03-grab-pool.html'},
{label:'订单详情', file:'04-order-detail.html'},
{label:'配送中', file:'05-delivering.html'},
{label:'送达签收', file:'06-finish-sign.html'},
{label:'历史订单', file:'07-history.html'},
{label:'个人中心', file:'08-profile.html'}
] },
{ name:'西安应急中心大屏', dir:'other-module/emergency-screen/xian-center', entry:'index-v4.html', pages:[
{label:'应急中心大屏', file:'index-v4.html'}
] },
@@ -37,6 +37,7 @@
background: #1a2b4a;
padding: 0 20px;
flex-shrink: 0;
gap: 12px;
}
.back-btn {
font-size: 13px;
@@ -57,6 +58,36 @@
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
font-family: Menlo, Consolas, monospace;
flex-shrink: 0;
}
.top-bar__right {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
/* 顶部栏内的收银员信息(紧凑版) */
.cashier-info--compact {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px 4px 4px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
}
.cashier-info--compact .cashier-info__avatar {
width: 22px;
height: 22px;
font-size: 10px;
}
.cashier-info--compact .cashier-info__text {
font-size: 11px;
color: rgba(255, 255, 255, 0.8);
}
.logout-btn--compact {
padding: 4px 10px;
font-size: 11px;
}
/* 主内容 */
@@ -132,6 +163,84 @@
color: #ff6b6b;
}
/* 餐次控制区 */
.meal-ctrl {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 18px;
background: rgba(26, 43, 74, 0.4);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
min-width: 200px;
}
.meal-ctrl__status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
}
.meal-ctrl__status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.meal-ctrl__status-dot--live {
background: #4ade80;
box-shadow: 0 0 8px rgba(74, 222, 128, 0.5);
animation: dotBlink2 1.4s infinite;
}
.meal-ctrl__status-dot--off {
background: #94a3b8;
}
@keyframes dotBlink2 {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.meal-ctrl__status-text { color: #fff; }
.meal-ctrl__status-text--live { color: #4ade80; }
.meal-ctrl__status-text--off { color: rgba(255, 255, 255, 0.5); }
.meal-ctrl__buttons {
display: flex;
gap: 8px;
}
.meal-ctrl__btn {
flex: 1;
height: 38px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1px solid;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
transition: all 0.2s;
text-decoration: none;
}
.meal-ctrl__btn--start {
background: linear-gradient(135deg, rgba(74, 222, 128, 0.18), rgba(34, 197, 94, 0.12));
border-color: rgba(74, 222, 128, 0.4);
color: #4ade80;
}
.meal-ctrl__btn--start:hover {
background: linear-gradient(135deg, rgba(74, 222, 128, 0.28), rgba(34, 197, 94, 0.2));
border-color: rgba(74, 222, 128, 0.6);
box-shadow: 0 4px 14px rgba(74, 222, 128, 0.2);
}
.meal-ctrl__btn--stop {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
.meal-ctrl__btn--stop:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.5);
}
/* 中央:核心动作区 */
.hero {
flex: 1;
@@ -466,6 +575,254 @@
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
}
/* ============ 交接班弹窗 ============ */
.shift-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
border-radius: 24px;
}
.shift-modal {
width: 680px;
max-height: 680px;
background: linear-gradient(180deg, #1a2744 0%, #141e36 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 18px;
padding: 28px 32px 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 120px rgba(74, 144, 217, 0.08);
overflow: hidden;
}
.shift-modal__head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.shift-modal__title {
font-size: 18px;
font-weight: 700;
color: #fff;
}
.shift-modal__close {
width: 30px;
height: 30px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: rgba(255, 255, 255, 0.55);
cursor: pointer;
transition: all 0.2s;
}
.shift-modal__close:hover {
background: rgba(245, 34, 45, 0.2);
border-color: rgba(245, 34, 45, 0.4);
color: #ff6b6b;
}
/* 收银员信息条 */
.shift-modal__cashier {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
margin-bottom: 20px;
}
.shift-modal__cashier-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #4a90d9, #0AC4A8);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
flex-shrink: 0;
}
.shift-modal__cashier-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.shift-modal__cashier-name {
font-size: 14px;
font-weight: 600;
color: #fff;
}
.shift-modal__cashier-meta {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
}
/* 统计指标网格 */
.shift-modal__kpi-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.shift-kpi {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 10px;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 6px;
}
.shift-kpi__label {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
}
.shift-kpi__value {
font-size: 20px;
font-weight: 700;
color: #fff;
display: flex;
align-items: baseline;
gap: 4px;
}
.shift-kpi__value--green { color: #4ade80; }
.shift-kpi__value--gold { color: #fbbf24; }
.shift-kpi__unit {
font-size: 12px;
font-weight: 400;
color: rgba(255, 255, 255, 0.55);
}
.shift-kpi__hint {
font-size: 10px;
color: rgba(255, 255, 255, 0.35);
line-height: 1.4;
}
.shift-modal__kpi-grid--two {
grid-template-columns: 1fr 1fr;
}
/* 分区标题 */
.shift-modal__section {
margin-bottom: 12px;
}
.shift-modal__section-title {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
/* 支付方式明细表 */
.shift-pay-table {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
overflow: hidden;
}
.shift-pay-table__head {
display: flex;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.shift-pay-table__th {
font-size: 11px;
color: rgba(255, 255, 255, 0.45);
font-weight: 500;
}
.shift-pay-table__th--name { flex: 1; }
.shift-pay-table__th--num { width: 80px; text-align: right; }
.shift-pay-table__row {
display: flex;
padding: 9px 16px;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
transition: background 0.15s;
}
.shift-pay-table__row:last-child { border-bottom: none; }
.shift-pay-table__row:hover { background: rgba(255, 255, 255, 0.03); }
.shift-pay-table__row--total {
background: rgba(255, 255, 255, 0.04);
font-weight: 700;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.shift-pay-table__td {
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
}
.shift-pay-table__td--name { flex: 1; display: flex; align-items: center; gap: 8px; }
.shift-pay-table__td--num { width: 80px; text-align: right; font-variant-numeric: tabular-nums; font-family: Menlo, Consolas, monospace; font-size: 12px; }
.shift-pay-table__row--total .shift-pay-table__td { color: #fff; }
/* 支付方式色点 */
.shift-pay-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.shift-pay-dot--online { background: #4ade80; box-shadow: 0 0 6px rgba(74, 222, 128, 0.5); }
.shift-pay-dot--cash { background: #fb923c; box-shadow: 0 0 6px rgba(251, 146, 60, 0.5); }
.shift-pay-dot--balance { background: #60a5fa; box-shadow: 0 0 6px rgba(96, 165, 250, 0.5); }
/* 分割线 */
.shift-modal__divider {
height: 1px;
background: rgba(255, 255, 255, 0.08);
margin: 20px 0;
}
/* 底部操作区 */
.shift-modal__actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.shift-modal__btn {
height: 40px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: 1px solid;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 28px;
text-decoration: none;
transition: all 0.2s;
}
.shift-modal__btn--cancel {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.7);
}
.shift-modal__btn--cancel:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.shift-modal__btn--exit {
background: linear-gradient(135deg, rgba(245, 34, 45, 0.18), rgba(239, 68, 68, 0.12));
border-color: rgba(245, 34, 45, 0.4);
color: #ff6b6b;
}
.shift-modal__btn--exit:hover {
background: linear-gradient(135deg, rgba(245, 34, 45, 0.3), rgba(239, 68, 68, 0.22));
border-color: rgba(245, 34, 45, 0.6);
box-shadow: 0 4px 16px rgba(245, 34, 45, 0.2);
}
</style>
</head>
<body>
@@ -477,6 +834,13 @@
<a class="back-btn" href="index.html">&#x25C0; 返回主页</a>
<span class="top-bar__title">结算台待机</span>
<span class="top-bar__meta" id="topMeta">2026-08-06 周四</span>
<div class="top-bar__right">
<div class="cashier-info cashier-info--compact">
<div class="cashier-info__avatar"></div>
<div class="cashier-info__text">CS001 · 张三</div>
</div>
<a class="logout-btn logout-btn--compact" href="javascript:void(0)" onclick="openShiftHandover()">交接班</a>
</div>
</div>
<!-- 主内容 -->
@@ -491,11 +855,21 @@
<div class="welcome__sub">数味餐厅 · 一楼 A 区 2 号餐线 · 等待顾客取餐</div>
</div>
<div class="welcome__right">
<div class="cashier-info">
<div class="cashier-info__avatar"></div>
<div class="cashier-info__text">CS001 · 张三</div>
<!-- 餐次控制区 -->
<div class="meal-ctrl">
<div class="meal-ctrl__status" id="mealCtrlStatus">
<span class="meal-ctrl__status-dot meal-ctrl__status-dot--live" id="mealCtrlDot"></span>
<span class="meal-ctrl__status-text meal-ctrl__status-text--live" id="mealCtrlText">午餐开餐中</span>
</div>
<div class="meal-ctrl__buttons" id="mealCtrlBtns">
<button class="meal-ctrl__btn meal-ctrl__btn--start" id="mealStartBtn" onclick="showMealStartConfirm()">
&#x1F50A; 开餐
</button>
<a class="meal-ctrl__btn meal-ctrl__btn--stop" id="mealStopBtn" href="11-meal-active.html?meal=lunch&action=collect">
&#x1F962; 收餐
</a>
</div>
</div>
<a class="logout-btn" href="01-login.html" onclick="return confirm('确定退出登录吗?');">退出登录</a>
</div>
</div>
@@ -585,7 +959,7 @@
<div class="entry-card__desc">今日数据可视化</div>
</div>
</a>
<a class="entry-card" href="10-meal-center.html">
<a class="entry-card" id="mealMgmtLink" href="11-meal-active.html">
<div class="entry-card__icon entry-card__icon--gold">&#x1F37D;&#xFE0F;</div>
<div class="entry-card__text">
<div class="entry-card__title">餐次管理</div>
@@ -607,6 +981,7 @@
<script>
/* ============ 实时时钟(只显示时分秒,年月日由 topMeta 展示) ============ */
var lastMealHour = -1;
function updateClock() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, '0');
@@ -645,10 +1020,105 @@
mealTag.style.borderColor = '';
mealTag.innerHTML = '&#x1F31F; 欢迎光临';
}
/* 餐次控制区随小时联动(仅在跨整点时更新) */
if (hour !== lastMealHour) {
lastMealHour = hour;
updateMealCtrl(hour);
}
}
updateClock();
setInterval(updateClock, 1000);
/* ============ 餐次控制区状态联动(双按钮常驻演示) ============ */
var mealCtrlState = 'live'; /* 'live' | 'collected' | 'off' */
var currentMealKey = 'lunch';
var currentMealName = '午餐';
function updateMealCtrl(hour) {
var dot = document.getElementById('mealCtrlDot');
var text = document.getElementById('mealCtrlText');
/* 根据时段判断当前餐次 */
var isMealTime = false;
if (hour >= 6 && hour < 10) {
currentMealKey = 'breakfast'; currentMealName = '早餐'; isMealTime = true;
} else if (hour >= 10 && hour < 14) {
currentMealKey = 'lunch'; currentMealName = '午餐'; isMealTime = true;
} else if (hour >= 16 && hour < 21) {
currentMealKey = 'dinner'; currentMealName = '晚餐'; isMealTime = true;
}
if (mealCtrlState === 'collected') {
dot.className = 'meal-ctrl__status-dot meal-ctrl__status-dot--off';
text.className = 'meal-ctrl__status-text meal-ctrl__status-text--off';
text.textContent = '已收餐 · 员工餐时间';
syncMealMgmtLink('collected');
} else if (isMealTime) {
mealCtrlState = 'live';
dot.className = 'meal-ctrl__status-dot meal-ctrl__status-dot--live';
text.className = 'meal-ctrl__status-text meal-ctrl__status-text--live';
text.textContent = currentMealName + ' 开餐中';
syncMealMgmtLink('live');
} else {
mealCtrlState = 'off';
dot.className = 'meal-ctrl__status-dot meal-ctrl__status-dot--off';
text.className = 'meal-ctrl__status-text meal-ctrl__status-text--off';
text.textContent = '非餐时段';
syncMealMgmtLink('off');
}
/* 按钮动态链接随餐次更新 */
var stopBtn = document.getElementById('mealStopBtn');
if (stopBtn) stopBtn.href = '11-meal-active.html?meal=' + currentMealKey + '&action=collect';
}
/* 同步底部"餐次管理"入口卡片的跳转链接 */
function syncMealMgmtLink(state) {
var link = document.getElementById('mealMgmtLink');
if (!link) return;
var desc = link.querySelector('.entry-card__desc');
if (state === 'collected') {
link.href = '11-meal-active.html?meal=' + currentMealKey + '&status=collected';
if (desc) desc.textContent = '员工餐时间 · 余量处置';
} else {
link.href = '11-meal-active.html?meal=' + currentMealKey;
if (desc) desc.textContent = '开餐/结束用餐/历史记录';
}
}
/* 开餐确认 */
function showMealStartConfirm() {
if (!confirm('确认开餐「' + currentMealName + '」?\n\n开启后将进入' + currentMealName + '餐次,系统开始记录取餐结算数据。')) return;
mealCtrlState = 'live';
document.getElementById('mealCtrlDot').className = 'meal-ctrl__status-dot meal-ctrl__status-dot--live';
document.getElementById('mealCtrlText').className = 'meal-ctrl__status-text meal-ctrl__status-text--live';
document.getElementById('mealCtrlText').textContent = currentMealName + ' 开餐中';
syncMealMgmtLink('live');
}
/* 收餐确认 */
function showMealStopConfirm() {
if (!confirm('确认收餐?\n\n收餐后进入员工餐时间,顾客无法继续取餐结算。\n稍后可在下方完成余量处置。')) return;
mealCtrlState = 'collected';
document.getElementById('mealCtrlDot').className = 'meal-ctrl__status-dot meal-ctrl__status-dot--off';
document.getElementById('mealCtrlText').className = 'meal-ctrl__status-text meal-ctrl__status-text--off';
document.getElementById('mealCtrlText').textContent = '已收餐 · 员工餐时间';
syncMealMgmtLink('collected');
}
/* 初始执行:检查 URL 参数是否包含 collected 状态 */
(function () {
var params = new URLSearchParams(window.location.search);
if (params.get('status') === 'collected') {
mealCtrlState = 'collected';
var m = params.get('meal');
if (m) { currentMealKey = m; currentMealName = {breakfast:'早餐',lunch:'午餐',dinner:'晚餐'}[m] || '午餐'; }
history.replaceState(null, '', window.location.pathname);
}
})();
updateMealCtrl(new Date().getHours());
/* ============ 副屏人脸识别状态三态轮播(演示) ============
三态轮播用于在原型中展示收银员需要识别的三种提醒场景:
success: 识别成功 - 显示用户信息,收银员可引导支付
@@ -717,7 +1187,98 @@
/* 初始展示第一态,4 秒后开始轮播 */
rotateFaceState();
setInterval(rotateFaceState, 4000);
/* ============ 交接班弹窗 ============ */
function openShiftHandover() {
document.getElementById('shiftOverlay').style.display = 'flex';
}
function closeShiftHandover() {
document.getElementById('shiftOverlay').style.display = 'none';
}
</script>
<!-- 交接班弹窗 -->
<div class="shift-overlay" id="shiftOverlay" style="display:none;">
<div class="shift-modal">
<div class="shift-modal__head">
<div class="shift-modal__title">&#x1F4CB; 交接班 · 当班营业汇总</div>
<div class="shift-modal__close" onclick="closeShiftHandover()" title="关闭">&#x2715;</div>
</div>
<div class="shift-modal__cashier">
<div class="shift-modal__cashier-avatar"></div>
<div class="shift-modal__cashier-info">
<div class="shift-modal__cashier-name">张三 · CS001</div>
<div class="shift-modal__cashier-meta">登录时间:2026-08-11 07:55 · 已值班 <span id="shiftDuration">4h 35m</span></div>
</div>
</div>
<!-- 汇总指标 -->
<div class="shift-modal__kpi-grid">
<div class="shift-kpi">
<div class="shift-kpi__label">当班营业额</div>
<div class="shift-kpi__value shift-kpi__value--green">&#xFFE5;5,428<span class="shift-kpi__unit">.50</span></div>
<div class="shift-kpi__hint">含早餐 &#xFFE5;1,926 + 午餐 &#xFFE5;3,502.50</div>
</div>
<div class="shift-kpi">
<div class="shift-kpi__label">当班订单数</div>
<div class="shift-kpi__value">186<span class="shift-kpi__unit"></span></div>
<div class="shift-kpi__hint">早餐 71 单 + 午餐 115 单</div>
</div>
<div class="shift-kpi">
<div class="shift-kpi__label">退款笔数 / 金额</div>
<div class="shift-kpi__value">3<span class="shift-kpi__unit"></span></div>
<div class="shift-kpi__hint">合计 &#xFFE5;82.00 · 退款率 1.61%</div>
</div>
<div class="shift-kpi">
<div class="shift-kpi__label">开餐 / 收餐操作</div>
<div class="shift-kpi__value">2<span class="shift-kpi__unit">次开餐</span></div>
<div class="shift-kpi__hint">早餐 07:32 开 · 午餐 10:58 开 · 暂未收餐</div>
</div>
</div>
<!-- 支付方式收款明细 -->
<div class="shift-modal__section">
<div class="shift-modal__section-title">&#x1F4B3; 收款方式明细</div>
<div class="shift-pay-table">
<div class="shift-pay-table__head">
<span class="shift-pay-table__th shift-pay-table__th--name">支付方式</span>
<span class="shift-pay-table__th shift-pay-table__th--num">笔数</span>
<span class="shift-pay-table__th shift-pay-table__th--num">金额</span>
<span class="shift-pay-table__th shift-pay-table__th--num">占比</span>
</div>
<div class="shift-pay-table__row">
<span class="shift-pay-table__td shift-pay-table__td--name"><span class="shift-pay-dot shift-pay-dot--online"></span>在线支付</span>
<span class="shift-pay-table__td shift-pay-table__td--num">152</span>
<span class="shift-pay-table__td shift-pay-table__td--num">&#xFFE5;4,426.50</span>
<span class="shift-pay-table__td shift-pay-table__td--num">81.5%</span>
</div>
<div class="shift-pay-table__row">
<span class="shift-pay-table__td shift-pay-table__td--name"><span class="shift-pay-dot shift-pay-dot--cash"></span>现金</span>
<span class="shift-pay-table__td shift-pay-table__td--num">3</span>
<span class="shift-pay-table__td shift-pay-table__td--num">&#xFFE5;82.00</span>
<span class="shift-pay-table__td shift-pay-table__td--num">1.5%</span>
</div>
<div class="shift-pay-table__row">
<span class="shift-pay-table__td shift-pay-table__td--name"><span class="shift-pay-dot shift-pay-dot--balance"></span>余额支付</span>
<span class="shift-pay-table__td shift-pay-table__td--num">31</span>
<span class="shift-pay-table__td shift-pay-table__td--num">&#xFFE5;920.00</span>
<span class="shift-pay-table__td shift-pay-table__td--num">17.0%</span>
</div>
<div class="shift-pay-table__row shift-pay-table__row--total">
<span class="shift-pay-table__td shift-pay-table__td--name">合计</span>
<span class="shift-pay-table__td shift-pay-table__td--num">186</span>
<span class="shift-pay-table__td shift-pay-table__td--num">&#xFFE5;5,428.50</span>
<span class="shift-pay-table__td shift-pay-table__td--num">100%</span>
</div>
</div>
</div>
<div class="shift-modal__divider"></div>
<div class="shift-modal__actions">
<button class="shift-modal__btn shift-modal__btn--cancel" onclick="closeShiftHandover()">取消</button>
<a class="shift-modal__btn shift-modal__btn--exit" href="01-login.html">确定交接班并退出</a>
</div>
</div>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,343 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>结算设备 - 餐次管理 | 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: #05080f;
color: #fff;
min-height: 100vh;
min-width: 1280px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.device-frame {
width: 1280px;
height: 720px;
background: #0e1326;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.04);
position: relative;
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.top-bar {
display: flex;
align-items: center;
height: 44px;
background: #1a2b4a;
padding: 0 20px;
flex-shrink: 0;
}
.back-btn {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
.top-bar__title {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
}
.top-bar__meta {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
font-family: Menlo, Consolas, monospace;
}
/* 主内容区 */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
padding: 28px 56px;
overflow: hidden;
}
/* 顶部餐次状态摘要 */
.meal-summary {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 20px;
background: rgba(26, 43, 74, 0.5);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
margin-bottom: 24px;
}
.meal-summary__icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(74, 144, 217, 0.18);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.meal-summary__body { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.meal-summary__title {
font-size: 14px;
font-weight: 600;
color: #fff;
}
.meal-summary__sub {
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
}
.meal-summary__time {
font-size: 22px;
font-weight: 600;
font-family: Menlo, Consolas, monospace;
color: #4ade80;
letter-spacing: 1px;
}
/* 导航卡片容器 */
.nav-cards {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
min-height: 0;
}
/* 导航卡片 */
.nav-card {
display: flex;
flex-direction: column;
align-items: center;
width: 260px;
padding: 32px 24px 24px;
border-radius: 16px;
text-decoration: none;
cursor: pointer;
transition: all 0.25s;
border: 1px solid;
text-align: center;
}
.nav-card:hover {
transform: translateY(-3px);
}
/* 开餐开关卡(绿色主调) */
.nav-card--active {
background: linear-gradient(180deg, rgba(74, 222, 128, 0.12) 0%, rgba(26, 43, 74, 0.4) 100%);
border-color: rgba(74, 222, 128, 0.35);
}
.nav-card--active:hover {
background: linear-gradient(180deg, rgba(74, 222, 128, 0.2) 0%, rgba(26, 43, 74, 0.55) 100%);
border-color: rgba(74, 222, 128, 0.55);
box-shadow: 0 8px 24px rgba(74, 222, 128, 0.15);
}
/* 历史记录卡(蓝色主调) */
.nav-card--history {
background: linear-gradient(180deg, rgba(74, 144, 217, 0.12) 0%, rgba(26, 43, 74, 0.4) 100%);
border-color: rgba(74, 144, 217, 0.35);
}
.nav-card--history:hover {
background: linear-gradient(180deg, rgba(74, 144, 217, 0.2) 0%, rgba(26, 43, 74, 0.55) 100%);
border-color: rgba(74, 144, 217, 0.55);
box-shadow: 0 8px 24px rgba(74, 144, 217, 0.15);
}
.nav-card__icon {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
margin-bottom: 16px;
}
.nav-card--active .nav-card__icon {
background: rgba(74, 222, 128, 0.2);
}
.nav-card--history .nav-card__icon {
background: rgba(74, 144, 217, 0.2);
}
.nav-card__title {
font-size: 18px;
font-weight: 700;
color: #fff;
margin-bottom: 6px;
}
.nav-card__desc {
font-size: 12px;
color: rgba(255, 255, 255, 0.55);
line-height: 1.6;
margin-bottom: 16px;
}
/* 状态标签 */
.nav-card__pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.nav-card__pill--live {
background: rgba(74, 222, 128, 0.18);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.35);
}
.nav-card__pill--count {
background: rgba(74, 144, 217, 0.18);
color: #60a5fa;
border: 1px solid rgba(74, 144, 217, 0.35);
}
.nav-card__pill .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
animation: dotBlink 1.4s infinite;
}
@keyframes dotBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* Toast 提示条 */
.toast {
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
padding: 10px 22px;
background: rgba(74, 222, 128, 0.95);
color: #0e1326;
border-radius: 22px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 8px 24px rgba(74, 222, 128, 0.3);
opacity: 0;
pointer-events: none;
transition: all 0.3s;
z-index: 100;
}
.toast--show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<div class="device-frame">
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="02-standby.html">&#x25C0; 返回待机</a>
<span class="top-bar__title">餐次管理</span>
<span class="top-bar__meta" id="topMeta">2026-08-10 周一</span>
</div>
<!-- Toast 提示 -->
<div class="toast" id="toast">余量处置已提交 ✓</div>
<!-- 主内容 -->
<div class="main-area">
<!-- 当前餐次状态摘要 -->
<div class="meal-summary">
<div class="meal-summary__icon">&#x1F96A;</div>
<div class="meal-summary__body">
<div class="meal-summary__title">当前餐次 · 午餐</div>
<div class="meal-summary__sub">数味餐厅 · 一楼 | 共 3 条餐线 · 28 道菜</div>
</div>
<div class="meal-summary__time" id="liveClock">12:30:45</div>
</div>
<!-- 导航卡片区 -->
<div class="nav-cards">
<!-- 餐次开关 -->
<a class="nav-card nav-card--active" href="11-meal-active.html?meal=lunch">
<div class="nav-card__icon">&#x1F50A;</div>
<div class="nav-card__title">餐次开关</div>
<div class="nav-card__desc">查看开餐信息 · 餐品余量<br>通知补餐 · 结束用餐</div>
<span class="nav-card__pill nav-card__pill--live">
<span class="dot"></span> 开餐中
</span>
</a>
<!-- 历史记录 -->
<a class="nav-card nav-card--history" href="13-meal-history.html">
<div class="nav-card__icon">&#x1F4DC;</div>
<div class="nav-card__title">历史记录</div>
<div class="nav-card__desc">查看餐次开餐/补餐<br>闭餐/余量处置流程 · 点击查看明细</div>
<span class="nav-card__pill nav-card__pill--count">今日 2 餐次</span>
</a>
</div>
</div>
</div>
<script>
/* ============ 实时时钟(只显示时分秒) ============ */
function updateClock() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, '0');
var mm = String(now.getMinutes()).padStart(2, '0');
var ss = String(now.getSeconds()).padStart(2, '0');
document.getElementById('liveClock').textContent = hh + ':' + mm + ':' + ss;
/* 右上角元信息:年月日 + 周几 */
var year = now.getFullYear();
var month = String(now.getMonth() + 1).padStart(2, '0');
var date = String(now.getDate()).padStart(2, '0');
var weekDay = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.getDay()];
document.getElementById('topMeta').textContent = year + '-' + month + '-' + date + ' ' + weekDay;
}
updateClock();
setInterval(updateClock, 1000);
/* ============ URL 参数驱动的 Toast 提示 ============
场景:从 12-meal-end-surplus.html 提交余量处置后跳回本页时
携带 ?toast=submitted 参数,本页解析后显示"余量处置已提交"提示
*/
(function () {
var params = new URLSearchParams(window.location.search);
var toastKey = params.get('toast');
if (toastKey === 'submitted') {
var toast = document.getElementById('toast');
setTimeout(function () {
toast.classList.add('toast--show');
setTimeout(function () {
toast.classList.remove('toast--show');
}, 2400);
}, 400);
}
/* 清除 URL 中的 toast 参数,避免刷新重复触发 */
if (toastKey) {
history.replaceState(null, '', window.location.pathname);
}
})();
</script>
</body>
</html>
@@ -459,6 +459,140 @@
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* 已收餐状态:信息条变琥珀色调 */
.meal-info-bar--collected {
background: linear-gradient(135deg, rgba(251, 191, 36, 0.12) 0%, rgba(26, 43, 74, 0.6) 100%);
border-color: rgba(251, 191, 36, 0.3);
}
.meal-info-bar--collected .meal-info-bar__icon {
background: rgba(251, 191, 36, 0.22);
}
.meal-info-bar__live--collected {
background: rgba(251, 191, 36, 0.18);
border-color: rgba(251, 191, 36, 0.4);
color: #fbbf24;
}
.meal-info-bar__live--collected::before {
background: #fbbf24;
}
/* ===== 收餐弹窗:双卡片选项 ===== */
.modal--wide { width: 500px; }
.collect-cards {
display: flex;
gap: 14px;
margin-bottom: 14px;
}
.collect-card {
flex: 1;
padding: 18px 16px 14px;
border-radius: 12px;
border: 1.5px solid;
display: flex;
flex-direction: column;
gap: 8px;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.collect-card:hover {
transform: translateY(-2px);
}
.collect-card--employee {
background: linear-gradient(180deg, rgba(251, 191, 36, 0.12) 0%, rgba(26, 43, 74, 0.4) 100%);
border-color: rgba(251, 191, 36, 0.4);
}
.collect-card--employee:hover {
border-color: rgba(251, 191, 36, 0.65);
box-shadow: 0 6px 20px rgba(251, 191, 36, 0.15);
}
.collect-card--dispose {
background: linear-gradient(180deg, rgba(168, 85, 247, 0.12) 0%, rgba(26, 43, 74, 0.4) 100%);
border-color: rgba(168, 85, 247, 0.4);
}
.collect-card--dispose:hover {
border-color: rgba(168, 85, 247, 0.65);
box-shadow: 0 6px 20px rgba(168, 85, 247, 0.15);
}
.collect-card__step {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
font-size: 11px;
font-weight: 700;
flex-shrink: 0;
}
.collect-card--employee .collect-card__step {
background: rgba(251, 191, 36, 0.25);
color: #fbbf24;
}
.collect-card--dispose .collect-card__step {
background: rgba(168, 85, 247, 0.25);
color: #c084fc;
}
.collect-card__title {
font-size: 15px;
font-weight: 700;
color: #fff;
}
.collect-card__desc {
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
line-height: 1.5;
flex: 1;
}
.collect-arrow {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: rgba(255, 255, 255, 0.3);
flex-shrink: 0;
align-self: center;
}
/* 余量处置按钮(已收餐状态下显示,琥珀渐变) */
.footer-btn--warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border-color: #f59e0b;
color: #fff;
box-shadow: 0 6px 18px rgba(245, 158, 11, 0.35);
}
.footer-btn--warning:hover {
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
box-shadow: 0 8px 22px rgba(245, 158, 11, 0.5);
}
/* 已收餐提示横幅 */
.collected-banner {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 18px;
background: linear-gradient(135deg, rgba(251, 191, 36, 0.1) 0%, rgba(26, 43, 74, 0.4) 100%);
border: 1px solid rgba(251, 191, 36, 0.25);
border-radius: 12px;
margin-bottom: 14px;
}
.collected-banner__icon {
font-size: 24px;
flex-shrink: 0;
}
.collected-banner__text {
flex: 1;
font-size: 13px;
color: #fbbf24;
font-weight: 600;
}
.collected-banner__sub {
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
font-weight: 400;
}
</style>
</head>
<body>
@@ -467,7 +601,7 @@
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="10-meal-center.html">&#x25C0; 返回餐次管理</a>
<a class="back-btn" href="02-standby.html">&#x25C0; 返回待机</a>
<span class="top-bar__title">开餐中 · <span id="mealTitle">午餐</span></span>
<span class="top-bar__meta" id="topMeta">2026-08-10 周一</span>
</div>
@@ -525,26 +659,40 @@
</div>
<!-- 底部固定操作 -->
<div class="footer-actions">
<a class="footer-btn footer-btn--secondary" href="10-meal-center.html">&#x25C0; 返回</a>
<button class="footer-btn footer-btn--danger" onclick="showEndModal()">
&#x26D4; 结束用
<div class="footer-actions" id="footerActions">
<a class="footer-btn footer-btn--secondary" href="02-standby.html">&#x25C0; 返回</a>
<button class="footer-btn footer-btn--danger" id="footerActionBtn" onclick="showEndModal()">
&#x1F962;
</button>
</div>
</div>
<!-- 二次确认对话框 -->
<!-- 收餐对话框 -->
<div class="modal-overlay" id="endModal">
<div class="modal">
<div class="modal__title">&#x26D4; 确认结束用餐?</div>
<div class="modal modal--wide">
<div class="modal__title">&#x1F962; 确认收餐</div>
<div class="modal__desc">
结束后顾客将<strong>无法继续取餐结算</strong>,本餐次进入收尾流程。<br>
系统将引导你完成各餐线餐品的<strong>余量处置</strong>(员工福利/捐赠/废弃/加工再利用)。
收餐后顾客将<strong>无法继续取餐结算</strong>。请选择收餐后的处理方式:
</div>
<div class="collect-cards">
<!-- ① 员工餐时间 -->
<a class="collect-card collect-card--employee" href="javascript:void(0)" onclick="goEmployeeMeal()">
<span class="collect-card__step"></span>
<span class="collect-card__title">&#x1F37D;&#xFE0F; 员工餐时间</span>
<span class="collect-card__desc">先让员工用餐,暂不处置剩余餐品。餐次状态变更为已收餐,稍后再处理余量。</span>
</a>
<!-- 连接箭头 -->
<span class="collect-arrow">&#x279C;</span>
<!-- ② 余量处置 -->
<a class="collect-card collect-card--dispose" href="javascript:void(0)" onclick="goSurplus()">
<span class="collect-card__step"></span>
<span class="collect-card__title">&#x267B;&#xFE0F; 余量处置</span>
<span class="collect-card__desc">直接进入余量处置流程。员工福利 / 捐赠 / 废弃 / 加工再利用。</span>
</a>
</div>
<div class="modal__actions">
<button class="modal__btn modal__btn--cancel" onclick="hideEndModal()">取消</button>
<a class="modal__btn modal__btn--confirm" href="12-meal-end-surplus.html?meal=lunch" style="text-decoration:none;display:inline-flex;align-items:center;justify-content:center;">确认结束 · 去处置余量</a>
</div>
</div>
</div>
@@ -679,6 +827,7 @@
}
/* ============ 二次确认对话框 ============ */
var currentMeal = 'lunch';
function showEndModal() {
document.getElementById('endModal').classList.add('is-show');
}
@@ -686,6 +835,16 @@
document.getElementById('endModal').classList.remove('is-show');
}
/* 员工餐时间:回到待机页面并传递已收餐状态 */
function goEmployeeMeal() {
window.location.href = '02-standby.html?status=collected&meal=' + currentMeal;
}
/* 直接去余量处置 */
function goSurplus() {
window.location.href = '12-meal-end-surplus.html?meal=' + currentMeal;
}
/* ============ 实时时钟 + 开餐时长 ============ */
function updateClock() {
var now = new Date();
@@ -698,12 +857,43 @@
updateClock();
setInterval(updateClock, 30000);
/* ============ URL 参数解析(餐次) ============ */
/* ============ URL 参数解析(餐次 + 收餐状态) ============ */
(function () {
var params = new URLSearchParams(window.location.search);
var meal = params.get('meal') || 'lunch';
currentMeal = meal;
var status = params.get('status'); /* 'collected' 表示已收餐 */
var mealMap = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐' };
document.getElementById('mealTitle').textContent = mealMap[meal] || '午餐';
var mealName = mealMap[meal] || '午餐';
if (status === 'collected') {
/* 已收餐状态:页面标题、信息条、底部按钮全部切换 */
document.getElementById('mealTitle').textContent = mealName + ' · 已收餐';
/* 信息条变琥珀色 */
var infoBar = document.querySelector('.meal-info-bar');
infoBar.classList.add('meal-info-bar--collected');
/* 状态标签变为"员工餐时间" */
var liveBadge = document.querySelector('.meal-info-bar__live');
liveBadge.textContent = '员工餐时间';
liveBadge.classList.add('meal-info-bar__live--collected');
/* 底部按钮变为"余量处置" */
var btn = document.getElementById('footerActionBtn');
btn.textContent = '♻ 余量处置';
btn.className = 'footer-btn footer-btn--warning';
btn.onclick = function () { window.location.href = '12-meal-end-surplus.html?meal=' + currentMeal; };
} else {
document.getElementById('mealTitle').textContent = mealName;
}
/* 从待机页点"收餐"跳转过来时,自动弹出收餐弹窗 */
if (params.get('action') === 'collect') {
setTimeout(function () { showEndModal(); }, 300);
/* 清理 URL 参数避免刷新重复触发 */
history.replaceState(null, '', '11-meal-active.html?meal=' + currentMeal);
}
})();
/* ============ 初始渲染:A 区餐线 ============ */
@@ -128,21 +128,6 @@
border-radius: 10px;
margin-bottom: 12px;
}
.toolbar__select-all {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
user-select: none;
}
.toolbar__select-all input { cursor: pointer; accent-color: #4a90d9; }
.toolbar__divider {
width: 1px;
height: 22px;
background: rgba(255, 255, 255, 0.1);
}
.toolbar__label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
@@ -164,30 +149,11 @@
min-width: 130px;
}
.toolbar__select:focus { border-color: #4a90d9; }
.toolbar__btn {
height: 28px;
padding: 0 14px;
background: linear-gradient(135deg, rgba(74, 144, 217, 0.4), rgba(74, 144, 217, 0.25));
border: 1px solid rgba(74, 144, 217, 0.5);
border-radius: 6px;
color: #fff;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.toolbar__btn:hover {
background: linear-gradient(135deg, rgba(74, 144, 217, 0.6), rgba(74, 144, 217, 0.4));
}
.toolbar__btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.toolbar__spacer { flex: 1; }
.toolbar__hint {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
}
.toolbar__spacer { flex: 1; }
/* 表格容器 */
.table-wrap {
@@ -231,7 +197,6 @@
.surplus-table tbody tr:last-child td { border-bottom: none; }
/* 列宽 */
.col-ck { width: 36px; text-align: center; }
.col-name { width: 130px; }
.col-line { width: 80px; }
.col-num { width: 80px; text-align: right; }
@@ -240,15 +205,6 @@
.col-amount { width: 90px; }
.col-note { width: auto; }
/* 复选框 */
.row-ck {
width: 14px;
height: 14px;
cursor: pointer;
accent-color: #4a90d9;
vertical-align: middle;
}
/* 餐线徽标 */
.line-badge {
display: inline-block;
@@ -519,13 +475,8 @@
<!-- 工具栏 -->
<div class="toolbar">
<label class="toolbar__select-all">
<input type="checkbox" id="selectAll" onchange="toggleAll(this)">
全选
</label>
<div class="toolbar__divider"></div>
<span class="toolbar__label">批量处置方式:</span>
<select class="toolbar__select" id="batchMethod">
<span class="toolbar__label">批量处置方式:</span>
<select class="toolbar__select" id="batchMethod" onchange="onBatchMethodChange(this.value)">
<option value="">请选择...</option>
<option value="welfare">员工福利</option>
<option value="donate">捐赠</option>
@@ -533,16 +484,14 @@
<option value="reprocess">加工再利用</option>
<option value="locker">存入智能货柜</option>
</select>
<button class="toolbar__btn" onclick="applyBatch()">应用到选中项</button>
<div class="toolbar__spacer"></div>
<span class="toolbar__hint" id="selectCount">已选 0 项</span>
<span class="toolbar__hint" id="totalCount">共 20 道菜</span>
</div>
<!-- 余量处置表格 -->
<div class="table-wrap">
<table class="surplus-table">
<colgroup>
<col class="col-ck">
<col class="col-name">
<col class="col-line">
<col class="col-num">
@@ -555,7 +504,6 @@
</colgroup>
<thead>
<tr>
<th class="col-ck"></th>
<th>餐品名称</th>
<th>餐线</th>
<th style="text-align:right;">实际制作(kg)</th>
@@ -672,7 +620,6 @@
html += ''
+ '<tr data-id="' + item.id + '">'
+ ' <td class="col-ck"><input type="checkbox" class="row-ck" onchange="updateSummary()" data-id="' + item.id + '"></td>'
+ ' <td><span style="margin-right:4px;">' + item.emoji + '</span>' + item.name + '</td>'
+ ' <td><span class="line-badge line-badge--' + item.line + '">' + item.line + ' 区</span></td>'
+ ' <td style="text-align:right;" class="num-made">' + item.made.toFixed(1) + '</td>'
@@ -689,18 +636,12 @@
+ ' <option value="locker">存入智能货柜</option>'
+ ' </select>'
+ ' </td>'
+ ' <td><input type="number" class="amount-input" step="0.1" min="0" max="' + surplus.toFixed(1) + '" value="' + surplus.toFixed(1) + '" data-id="' + item.id + '"></td>'
+ ' <td><input type="number" class="amount-input" step="0.1" min="0" max="' + surplus.toFixed(1) + '" value="' + surplus.toFixed(1) + '" data-id="' + item.id + '" onchange="updateSummary()"></td>'
+ ' <td><input type="text" class="note-input" placeholder="如:加班餐/捐赠予..." data-id="' + item.id + '"></td>'
+ '</tr>';
});
document.getElementById('surplusTbody').innerHTML = html;
updateSummary();
}
/* ============ 全选/取消全选 ============ */
function toggleAll(masterCk) {
var cks = document.querySelectorAll('.row-ck');
cks.forEach(function (ck) { ck.checked = masterCk.checked; });
document.getElementById('totalCount').textContent = '共 ' + SURPLUS_DATA.length + ' 道菜';
updateSummary();
}
@@ -711,34 +652,24 @@
updateSummary();
}
/* ============ 批量应用处置方式 ============ */
function applyBatch() {
var batchVal = document.getElementById('batchMethod').value;
if (!batchVal) {
showToast('请先选择批量处置方式', true);
return;
}
var checkedCks = document.querySelectorAll('.row-ck:checked');
if (checkedCks.length === 0) {
showToast('请先勾选要批量处置的餐品', true);
return;
}
checkedCks.forEach(function (ck) {
var id = ck.getAttribute('data-id');
var sel = document.querySelector('.method-select[data-id="' + id + '"]');
/* ============ 批量处置下拉自动应用到全部行 ============ */
function onBatchMethodChange(batchVal) {
if (!batchVal) return;
var allSelects = document.querySelectorAll('.method-select');
allSelects.forEach(function (sel) {
sel.value = batchVal;
sel.className = 'method-select ' + METHOD_MAP[batchVal].cls;
});
updateSummary();
showToast('已应用到 ' + checkedCks.length + ' ✓');
showToast('已批量应用「' + METHOD_MAP[batchVal].label + '」到全部 ' + SURPLUS_DATA.length + ' 道菜 ✓');
}
/* ============ 更新汇总栏 ============ */
function updateSummary() {
var totalSurplus = 0;
var totalDisposed = 0;
var disposedCount = 0;
var methodCount = { welfare: 0, donate: 0, waste: 0, reprocess: 0, locker: 0 };
var checkedCount = 0;
SURPLUS_DATA.forEach(function (item) {
var surplus = +(item.made - item.sold).toFixed(1);
@@ -746,11 +677,9 @@
var sel = document.querySelector('.method-select[data-id="' + item.id + '"]');
var amountInput = document.querySelector('.amount-input[data-id="' + item.id + '"]');
var ck = document.querySelector('.row-ck[data-id="' + item.id + '"]');
if (ck.checked) checkedCount++;
if (sel.value) {
if (sel && sel.value) {
disposedCount++;
var amount = parseFloat(amountInput.value) || 0;
totalDisposed += amount;
if (methodCount.hasOwnProperty(sel.value)) {
@@ -766,7 +695,6 @@
document.getElementById('totalSurplus').textContent = totalSurplus.toFixed(1) + ' kg';
document.getElementById('totalDisposed').textContent = totalDisposed.toFixed(1) + ' kg';
document.getElementById('totalPending').textContent = pending.toFixed(1) + ' kg';
document.getElementById('selectCount').textContent = '已选 ' + checkedCount + ' 项';
/* 处置方式分布 */
var parts = [];
@@ -776,6 +704,9 @@
}
});
document.getElementById('methodDist').textContent = parts.length ? parts.join(' / ') : '—';
/* 工具栏提示 */
document.getElementById('totalCount').textContent = '共 ' + SURPLUS_DATA.length + ' 道 · 已处置 ' + disposedCount + ' 道';
}
/* ============ 提交二次确认 ============ */
@@ -832,7 +763,7 @@
/* ============ 提交跳转(模拟) ============ */
function submitForm() {
/* 实际场景:POST 数据到后端 → 成功后跳转 */
window.location.href = '10-meal-center.html?toast=submitted';
window.location.href = '02-standby.html';
}
/* ============ Toast 提示 ============ */
File diff suppressed because it is too large Load Diff
@@ -292,7 +292,7 @@
<div class="card__icon">&#x1F4CA;</div>
<div class="card__body">
<div class="card__title">运营数据统计</div>
<div class="card__desc">营收 + 餐次 + Top + 时段</div>
<div class="card__desc">营收 + 餐线 + 餐次历史</div>
</div>
</a>
<a class="card" href="08-reconcile.html">
@@ -311,14 +311,6 @@
<div class="card__desc">餐后小票预览与打印</div>
</div>
</a>
<a class="card" href="10-meal-center.html">
<span class="card__status card__status--core">10 新</span>
<div class="card__icon">&#x1F37D;&#xFE0F;</div>
<div class="card__body">
<div class="card__title">餐次管理</div>
<div class="card__desc">开餐开关 / 历史记录入口</div>
</div>
</a>
<a class="card" href="11-meal-active.html?meal=lunch">
<span class="card__status card__status--done">11</span>
<div class="card__icon">&#x1F50A;</div>
@@ -335,14 +327,6 @@
<div class="card__desc">结束用餐引导 · 批量处置</div>
</div>
</a>
<a class="card" href="13-meal-history.html">
<span class="card__status card__status--done">13</span>
<div class="card__icon">&#x1F4DC;</div>
<div class="card__body">
<div class="card__title">餐次历史记录</div>
<div class="card__desc">开餐/补餐/闭餐/余量处置 · 点击查看明细</div>
</div>
</a>
</div>
</div>
+397
View File
@@ -0,0 +1,397 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配送员登录 | 营养管理餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--white: #ffffff;
--danger: #ef4444;
--shadow-md: 0 10px 28px rgba(15, 23, 42, 0.10);
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--white);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #111827;
font-size: 12px;
font-weight: 600;
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal {
width: 18px; height: 10px;
background: #111827;
clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%);
}
.status-bar__wifi {
width: 16px; height: 12px;
background: #111827;
clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%);
}
.status-bar__battery {
width: 24px; height: 12px;
border: 1.5px solid #111827;
border-radius: 3px;
position: relative;
padding: 1.5px;
}
.status-bar__battery::after {
content: '';
display: block;
width: 70%; height: 100%;
background: #111827;
border-radius: 1px;
}
.status-bar__battery::before {
content: '';
position: absolute;
right: -3px; top: 3px;
width: 2px; height: 6px;
background: #111827;
border-radius: 0 1px 1px 0;
}
.content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, var(--primary-light) 0%, var(--white) 30%);
padding: 20px 28px 32px;
}
.hero {
text-align: center;
margin: 32px 0 40px;
}
.hero__logo {
width: 88px; height: 88px;
margin: 0 auto 16px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 44px;
box-shadow: 0 12px 24px rgba(255, 109, 58, 0.35);
}
.hero__title {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 6px;
}
.hero__subtitle {
font-size: 13px;
color: var(--text-secondary);
}
.form-group { margin-bottom: 16px; }
.form-label {
display: block;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.input-wrap {
position: relative;
display: flex;
align-items: center;
background: #f6f8fb;
border: 1.5px solid transparent;
border-radius: 12px;
padding: 0 14px;
transition: border-color 0.2s;
}
.input-wrap:focus-within {
border-color: var(--primary);
background: var(--white);
}
.input-icon { font-size: 18px; margin-right: 8px; }
.input {
flex: 1;
height: 48px;
border: none;
background: transparent;
font-size: 15px;
color: var(--text-primary);
outline: none;
}
.input::placeholder { color: var(--text-tertiary); }
.sms-btn {
background: transparent;
border: none;
color: var(--primary);
font-size: 13px;
font-weight: 600;
padding: 0 4px;
cursor: pointer;
white-space: nowrap;
}
.sms-btn:disabled { color: var(--text-tertiary); cursor: not-allowed; }
.sms-divider {
width: 1px;
height: 18px;
background: var(--border);
margin: 0 8px;
}
.agree-row {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 16px 0 24px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.6;
}
.agree-checkbox {
width: 16px; height: 16px;
border: 1.5px solid var(--border);
border-radius: 4px;
flex-shrink: 0;
margin-top: 2px;
cursor: pointer;
position: relative;
}
.agree-checkbox.checked {
background: var(--primary);
border-color: var(--primary);
}
.agree-checkbox.checked::after {
content: '✓';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 11px;
font-weight: 700;
}
.agree-link { color: var(--primary); text-decoration: none; }
.primary-btn {
width: 100%;
height: 50px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
border: none;
border-radius: 14px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 8px 16px rgba(255, 109, 58, 0.3);
transition: transform 0.1s;
}
.primary-btn:active { transform: scale(0.98); }
.primary-btn:disabled {
background: #d1d5db;
box-shadow: none;
cursor: not-allowed;
}
.wechat-btn {
width: 100%;
height: 50px;
background: var(--white);
color: #07c160;
border: 1.5px solid #07c160;
border-radius: 14px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
margin-top: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.wechat-btn:active { background: #f0fbf4; }
.divider {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0 16px;
color: var(--text-tertiary);
font-size: 12px;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.switch-role {
text-align: center;
margin-top: auto;
padding-top: 24px;
font-size: 13px;
color: var(--text-secondary);
}
.switch-role a {
color: var(--primary);
font-weight: 600;
text-decoration: none;
margin-left: 4px;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
.home-indicator {
height: 8px;
width: 134px;
background: #1f2937;
border-radius: 4px;
margin: 0 auto 8px;
align-self: center;
}
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">9:41</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<div class="content">
<div class="hero">
<div class="hero__logo">🛵</div>
<div class="hero__title">营养餐品配送</div>
<div class="hero__subtitle">配送员端 · 接单抢单高效配送</div>
</div>
<div class="form-group">
<label class="form-label">手机号</label>
<div class="input-wrap">
<span class="input-icon">📱</span>
<input class="input" type="tel" placeholder="请输入手机号" maxlength="11" value="138****6688">
</div>
</div>
<div class="form-group">
<label class="form-label">短信验证码</label>
<div class="input-wrap">
<span class="input-icon">🔑</span>
<input class="input" type="text" placeholder="6位验证码" maxlength="6" value="••••••">
<div class="sms-divider"></div>
<button class="sms-btn" id="smsBtn">获取验证码</button>
</div>
</div>
<div class="agree-row">
<div class="agree-checkbox checked" id="agreeBox"></div>
<div>
我已阅读并同意
<a href="javascript:void(0)" class="agree-link">《配送员服务协议》</a>
<a href="javascript:void(0)" class="agree-link">《隐私政策》</a>
</div>
</div>
<button class="primary-btn" onclick="location.href='01-home.html'">登录 / 注册</button>
<div class="divider">其他登录方式</div>
<button class="wechat-btn" onclick="toast('微信授权中...')">
<span>💬</span>
微信一键登录
</button>
<div class="switch-role">
想要合作配送?
<a href="javascript:void(0)" onclick="toast('请联系站点管理员')">申请入驻</a>
</div>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
let countdown = 0;
const smsBtn = document.getElementById('smsBtn');
smsBtn.addEventListener('click', () => {
if (countdown > 0) return;
countdown = 60;
toast('验证码已发送');
const tick = () => {
if (countdown <= 0) {
smsBtn.disabled = false;
smsBtn.textContent = '获取验证码';
return;
}
smsBtn.disabled = true;
smsBtn.textContent = countdown + 's 后重发';
countdown--;
setTimeout(tick, 1000);
};
tick();
});
document.getElementById('agreeBox').addEventListener('click', e => {
e.target.classList.toggle('checked');
});
</script>
</body>
</html>
+643
View File
@@ -0,0 +1,643 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工作台首页 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--white: #ffffff;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
--shadow-md: 0 10px 28px rgba(15, 23, 42, 0.10);
--status-accepted: #3b82f6;
--status-picking: #14b8a6;
--status-delivering: #8b5cf6;
--status-completed: #10b981;
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal {
width: 18px; height: 10px; background: #fff;
clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%);
}
.status-bar__wifi {
width: 16px; height: 12px; background: #fff;
clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%);
}
.status-bar__battery {
width: 24px; height: 12px;
border: 1.5px solid #fff;
border-radius: 3px;
position: relative;
padding: 1.5px;
}
.status-bar__battery::after {
content: '';
display: block;
width: 70%; height: 100%;
background: #fff;
border-radius: 1px;
}
.status-bar__battery::before {
content: '';
position: absolute;
right: -3px; top: 3px;
width: 2px; height: 6px;
background: #fff;
border-radius: 0 1px 1px 0;
}
.scroll-area {
flex: 1;
overflow-y: auto;
padding-bottom: 80px;
}
.scroll-area::-webkit-scrollbar { display: none; }
/* 顶部配送员信息 + 在线开关 */
.top-bar {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
padding: 0 20px 20px;
color: white;
}
.rider-row {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 0 16px;
}
.rider-avatar {
width: 50px; height: 50px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
border: 2px solid rgba(255, 255, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.rider-info { flex: 1; }
.rider-name {
font-size: 17px;
font-weight: 700;
margin-bottom: 2px;
}
.rider-meta {
font-size: 12px;
opacity: 0.9;
}
.rider-meta .level {
background: rgba(255, 255, 255, 0.25);
padding: 1px 6px;
border-radius: 6px;
margin-right: 6px;
}
.online-switch {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.18);
border-radius: 24px;
padding: 6px 12px 6px 6px;
}
.switch-track {
width: 36px; height: 20px;
background: #10b981;
border-radius: 10px;
position: relative;
transition: background 0.3s;
}
.switch-track::after {
content: '';
position: absolute;
width: 16px; height: 16px;
background: white;
border-radius: 50%;
top: 2px; left: 18px;
transition: left 0.3s;
}
.online-switch.off .switch-track { background: #9ca3af; }
.online-switch.off .switch-track::after { left: 2px; }
.online-text { font-size: 12px; font-weight: 600; }
/* 今日数据 */
.stats-card {
background: white;
margin: -10px 16px 0;
padding: 18px 12px;
border-radius: 14px;
box-shadow: var(--shadow-md);
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
.stat-item { text-align: center; }
.stat-num {
font-size: 18px;
font-weight: 700;
color: var(--primary);
margin-bottom: 2px;
}
.stat-label {
font-size: 11px;
color: var(--text-secondary);
}
/* 派单提醒横幅(模拟收到派单) */
.dispatch-banner {
margin: 14px 16px 0;
background: linear-gradient(135deg, #fff5e6, #ffe5d4);
border: 1.5px solid var(--primary);
border-radius: 14px;
padding: 12px;
display: flex;
align-items: center;
gap: 12px;
position: relative;
overflow: hidden;
animation: bannerPulse 2s ease-in-out infinite;
cursor: pointer;
}
@keyframes bannerPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 109, 58, 0.4); }
50% { box-shadow: 0 0 0 8px rgba(255, 109, 58, 0); }
}
.dispatch-banner__icon {
width: 40px; height: 40px;
background: var(--primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
animation: ringShake 1.5s ease-in-out infinite;
}
@keyframes ringShake {
0%, 100% { transform: rotate(0); }
20% { transform: rotate(-15deg); }
40% { transform: rotate(15deg); }
60% { transform: rotate(-10deg); }
80% { transform: rotate(10deg); }
}
.dispatch-banner__text { flex: 1; }
.dispatch-banner__title {
font-size: 14px;
font-weight: 700;
color: var(--primary-dark);
margin-bottom: 2px;
}
.dispatch-banner__desc {
font-size: 12px;
color: var(--text-secondary);
}
.dispatch-banner__arrow {
color: var(--primary);
font-size: 18px;
font-weight: 700;
}
/* 进行中订单 */
.section {
margin: 18px 16px 0;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.section-title {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
}
.section-title__dot {
width: 4px; height: 14px;
background: var(--primary);
border-radius: 2px;
}
.section-more {
font-size: 12px;
color: var(--text-secondary);
text-decoration: none;
}
.order-card {
background: white;
border-radius: 14px;
padding: 14px;
margin-bottom: 10px;
box-shadow: var(--shadow-sm);
text-decoration: none;
color: inherit;
display: block;
}
.order-card__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.order-num {
font-size: 12px;
color: var(--text-tertiary);
}
.status-pill {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 600;
}
.status-pill--delivering {
background: #f3e8ff;
color: var(--status-delivering);
}
.status-pill--picking {
background: #ccfbf1;
color: var(--status-picking);
}
.status-pill--accepted {
background: #dbeafe;
color: var(--status-accepted);
}
.order-type-tag {
display: inline-block;
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
margin-left: 6px;
vertical-align: middle;
}
.order-type-tag--store {
background: #fef3c7;
color: #92400e;
}
.order-type-tag--home {
background: #dbeafe;
color: #1e40af;
}
.route-line {
display: flex;
flex-direction: column;
gap: 10px;
padding: 4px 0;
}
.route-point {
display: flex;
gap: 10px;
align-items: flex-start;
}
.route-icon {
width: 22px; height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
flex-shrink: 0;
color: white;
}
.route-icon--start { background: var(--primary); }
.route-icon--end { background: var(--status-completed); }
.route-info { flex: 1; padding-top: 1px; }
.route-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.route-addr {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.route-connector {
width: 2px;
height: 14px;
background: var(--border);
margin-left: 10px;
}
.order-meta {
display: flex;
gap: 12px;
margin: 10px 0;
padding: 8px 10px;
background: var(--background);
border-radius: 8px;
font-size: 12px;
color: var(--text-secondary);
}
.order-meta-item {
flex: 1;
display: flex;
flex-direction: column;
}
.order-meta-label { font-size: 11px; color: var(--text-tertiary); }
.order-meta-value { font-size: 13px; color: var(--text-primary); font-weight: 600; }
.order-action {
display: flex;
gap: 8px;
margin-top: 6px;
}
.btn {
flex: 1;
height: 36px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.btn--ghost {
background: var(--background);
color: var(--text-primary);
}
.btn--primary {
background: var(--primary);
color: white;
}
/* 底部 Tab */
.tabbar {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 70px;
background: white;
border-top: 1px solid var(--border);
display: flex;
padding: 8px 0 20px;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
text-decoration: none;
color: var(--text-tertiary);
}
.tab.active { color: var(--primary); }
.tab__icon { font-size: 22px; }
.tab__label { font-size: 10px; font-weight: 600; }
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">9:41</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<div class="scroll-area">
<!-- 顶部配送员信息 -->
<div class="top-bar">
<div class="rider-row">
<div class="rider-avatar">🧑‍🦱</div>
<div class="rider-info">
<div class="rider-name">陈师傅 <span style="font-size:12px;opacity:.85">(陕A·9F8K2</span></div>
<div class="rider-meta">
今日已在线 3h 24min
</div>
</div>
<div class="online-switch" id="onlineSwitch" onclick="toggleOnline()">
<div class="switch-track"></div>
<span class="online-text">在线</span>
</div>
</div>
</div>
<!-- 今日工作量统计 -->
<div class="stats-card">
<div class="stat-item">
<div class="stat-num">8</div>
<div class="stat-label">已完成</div>
</div>
<div class="stat-item">
<div class="stat-num">2</div>
<div class="stat-label">进行中</div>
</div>
<div class="stat-item">
<div class="stat-num">32km</div>
<div class="stat-label">总里程</div>
</div>
<div class="stat-item">
<div class="stat-num">3h24m</div>
<div class="stat-label">在线时长</div>
</div>
</div>
<!-- 派单提醒横幅(模拟收到新派单) -->
<div class="dispatch-banner" onclick="location.href='02-dispatch-incoming.html'">
<div class="dispatch-banner__icon">📲</div>
<div class="dispatch-banner__text">
<div class="dispatch-banner__title">您有新的派单 · 倒计时 25s</div>
<div class="dispatch-banner__desc">味之都食堂 → 高新区科技路 12 号 · 1.8km</div>
</div>
<div class="dispatch-banner__arrow"></div>
</div>
<!-- 进行中订单 -->
<div class="section">
<div class="section-header">
<div class="section-title">
<span class="section-title__dot"></span>
进行中订单
</div>
<a href="07-history.html" class="section-more">查看全部 </a>
</div>
<a href="05-delivering.html" class="order-card">
<div class="order-card__head">
<div class="order-num">#DP20260812-0832
<span class="order-type-tag order-type-tag--home">到家</span>
</div>
<span class="status-pill status-pill--delivering">配送中</span>
</div>
<div class="route-line">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">中央厨房·高新站</div>
<div class="route-addr">科技三路 8 号 · 已取货 14:22</div>
</div>
</div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">万科城 5 号楼 1802</div>
<div class="route-addr">张女士 · 138****6688 · 距离 2.3km</div>
</div>
</div>
</div>
<div class="order-meta">
<div class="order-meta-item">
<span class="order-meta-label">餐品</span>
<span class="order-meta-value">3 份</span>
</div>
<div class="order-meta-item">
<span class="order-meta-label">配送距离</span>
<span class="order-meta-value">2.3km</span>
</div>
<div class="order-meta-item">
<span class="order-meta-label">要求送达</span>
<span class="order-meta-value">15:30 前</span>
</div>
</div>
<div class="order-action">
<button class="btn btn--ghost" onclick="event.preventDefault();toast('已呼叫客户')">📞 联系客户</button>
<button class="btn btn--primary" onclick="event.preventDefault();location.href='05-delivering.html'">查看路线</button>
</div>
</a>
<a href="04-order-detail.html" class="order-card">
<div class="order-card__head">
<div class="order-num">#DP20260812-0905
<span class="order-type-tag order-type-tag--store">到店</span>
</div>
<span class="status-pill status-pill--picking">取货中</span>
</div>
<div class="route-line">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">净菜加工中心·长安站</div>
<div class="route-addr">长安区西长安街 · 已到店 14:48</div>
</div>
</div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">家兴源食堂(科技路店)</div>
<div class="route-addr">科技路 12 号 · 距离 5.6km</div>
</div>
</div>
</div>
<div class="order-action">
<button class="btn btn--ghost" onclick="event.preventDefault();toast('正在呼叫商家')">📞 联系客户</button>
<button class="btn btn--primary" onclick="event.preventDefault();toast('已通知商家准备')">到店报到</button>
</div>
</a>
</div>
</div>
<!-- 底部 Tab -->
<div class="tabbar">
<a href="01-home.html" class="tab active">
<span class="tab__icon">🏠</span>
<span class="tab__label">首页</span>
</a>
<a href="03-grab-pool.html" class="tab">
<span class="tab__icon">🔥</span>
<span class="tab__label">抢单</span>
</a>
<a href="07-history.html" class="tab">
<span class="tab__icon">📚</span>
<span class="tab__label">历史</span>
</a>
<a href="08-profile.html" class="tab">
<span class="tab__icon">👤</span>
<span class="tab__label">我的</span>
</a>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
function toggleOnline() {
const sw = document.getElementById('onlineSwitch');
sw.classList.toggle('off');
const isOnline = !sw.classList.contains('off');
sw.querySelector('.online-text').textContent = isOnline ? '在线' : '离线';
toast(isOnline ? '已上线·开始接单' : '已离线·暂停接单');
}
</script>
</body>
</html>
@@ -0,0 +1,683 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>派单接听 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--white: #ffffff;
--danger: #ef4444;
--success: #10b981;
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: radial-gradient(circle at 50% 20%, #fff0e8 0%, #ffe5d4 40%, var(--primary) 100%);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal {
width: 18px; height: 10px; background: #fff;
clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%);
}
.status-bar__wifi {
width: 16px; height: 12px; background: #fff;
clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%);
}
.status-bar__battery {
width: 24px; height: 12px;
border: 1.5px solid #fff;
border-radius: 3px;
position: relative;
padding: 1.5px;
}
.status-bar__battery::after {
content: '';
display: block;
width: 70%; height: 100%;
background: #fff;
border-radius: 1px;
}
.status-bar__battery::before {
content: '';
position: absolute;
right: -3px; top: 3px;
width: 2px; height: 6px;
background: #fff;
border-radius: 0 1px 1px 0;
}
.content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 20px;
position: relative;
}
/* 派单图标 + 响铃动效 */
.ring-section {
text-align: center;
padding: 8px 0 16px;
}
.ring-icon-wrap {
position: relative;
width: 100px; height: 100px;
margin: 0 auto 12px;
}
.ring-icon {
position: absolute;
inset: 0;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 50px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2);
animation: ringShake 1s ease-in-out infinite;
}
@keyframes ringShake {
0%, 100% { transform: rotate(0); }
20% { transform: rotate(-15deg); }
40% { transform: rotate(15deg); }
60% { transform: rotate(-10deg); }
80% { transform: rotate(10deg); }
}
.ring-pulse {
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.6);
animation: pulseRing 1.5s ease-out infinite;
}
.ring-pulse:nth-child(2) { animation-delay: 0.5s; }
.ring-pulse:nth-child(3) { animation-delay: 1s; }
@keyframes pulseRing {
0% { transform: scale(1); opacity: 0.8; }
100% { transform: scale(2.2); opacity: 0; }
}
.ring-title {
color: white;
font-size: 22px;
font-weight: 700;
margin-bottom: 4px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.ring-subtitle {
color: rgba(255, 255, 255, 0.95);
font-size: 13px;
}
/* 倒计时圆环 */
.countdown-section {
display: flex;
justify-content: center;
padding: 8px 0 16px;
}
.countdown-ring {
position: relative;
width: 110px; height: 110px;
}
.countdown-ring svg {
width: 100%; height: 100%;
transform: rotate(-90deg);
}
.countdown-track {
fill: none;
stroke: rgba(255, 255, 255, 0.3);
stroke-width: 8;
}
.countdown-progress {
fill: none;
stroke: white;
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 283;
transition: stroke-dashoffset 1s linear;
}
.countdown-number {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: white;
text-align: center;
}
.countdown-number__val {
font-size: 36px;
font-weight: 700;
line-height: 1;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.countdown-number__unit {
font-size: 12px;
margin-top: 2px;
opacity: 0.9;
}
/* 订单卡片 */
.order-card {
background: white;
border-radius: 16px;
padding: 16px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
margin-bottom: 14px;
}
.order-card__head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px dashed var(--border);
}
.order-num { font-size: 12px; color: var(--text-tertiary); }
.order-type-tag {
display: inline-block;
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
margin-left: 6px;
vertical-align: middle;
font-weight: 600;
}
.order-type-tag--home {
background: #dbeafe;
color: #1e40af;
}
.order-type-tag--store {
background: #fef3c7;
color: #92400e;
}
.order-distance {
font-size: 12px;
color: var(--primary);
font-weight: 600;
}
.route {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 0;
}
.route-point {
display: flex;
gap: 10px;
align-items: flex-start;
}
.route-icon {
width: 24px; height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
flex-shrink: 0;
color: white;
font-weight: 700;
}
.route-icon--start { background: var(--primary); }
.route-icon--end { background: var(--success); }
.route-info { flex: 1; padding-top: 2px; }
.route-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.route-addr {
font-size: 12px;
color: var(--text-secondary);
}
.route-connector {
width: 2px;
height: 16px;
background: var(--border);
margin-left: 11px;
border-left: 2px dashed var(--border);
background: transparent;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin: 12px 0 0;
padding: 10px;
background: #f6f8fb;
border-radius: 10px;
}
.meta-cell { text-align: center; }
.meta-label {
font-size: 11px;
color: var(--text-tertiary);
margin-bottom: 2px;
}
.meta-value {
font-size: 13px;
font-weight: 700;
color: var(--primary);
}
/* 操作按钮 */
.action-bar {
display: flex;
gap: 10px;
margin-top: 8px;
padding: 8px 0;
}
.btn {
flex: 1;
height: 56px;
border: none;
border-radius: 14px;
font-size: 17px;
font-weight: 700;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
transition: transform 0.1s;
}
.btn:active { transform: scale(0.97); }
.btn--reject {
background: white;
color: var(--danger);
border: 2px solid var(--danger);
}
.btn--accept {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
box-shadow: 0 8px 16px rgba(16, 185, 129, 0.35);
}
.btn__small {
font-size: 11px;
opacity: 0.85;
font-weight: 500;
}
/* 拒单理由弹窗 */
.modal-mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 50;
display: none;
align-items: flex-end;
justify-content: center;
}
.modal-mask.show { display: flex; }
.modal {
width: 100%;
background: white;
border-radius: 20px 20px 0 0;
padding: 20px;
max-height: 80%;
overflow-y: auto;
animation: slideUp 0.3s;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.modal__head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.modal__title {
font-size: 16px;
font-weight: 700;
}
.modal__close {
background: var(--border);
color: var(--text-secondary);
border: none;
width: 28px; height: 28px;
border-radius: 50%;
font-size: 16px;
cursor: pointer;
}
.reason-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.reason-item {
padding: 14px 16px;
background: var(--background, #f6f8fb);
border: 1.5px solid transparent;
border-radius: 10px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: all 0.2s;
}
.reason-item:hover {
background: #fff0e8;
border-color: var(--primary);
}
.reason-item.selected {
background: #fff0e8;
border-color: var(--primary);
}
.reason-icon {
width: 36px; height: 36px;
background: white;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.reason-info { flex: 1; }
.reason-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.reason-desc {
font-size: 11px;
color: var(--text-secondary);
}
.reason-check {
width: 18px; height: 18px;
border: 2px solid var(--border);
border-radius: 50%;
}
.reason-item.selected .reason-check {
border-color: var(--primary);
background: var(--primary);
position: relative;
}
.reason-item.selected .reason-check::after {
content: '✓';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: 700;
}
.modal-tip {
background: #fef3c7;
color: #92400e;
padding: 10px 12px;
border-radius: 8px;
font-size: 12px;
margin-bottom: 14px;
line-height: 1.6;
}
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: white;
border-radius: 3px;
z-index: 10;
}
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">9:41</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<div class="content">
<!-- 派单响铃 -->
<div class="ring-section">
<div class="ring-icon-wrap">
<div class="ring-pulse"></div>
<div class="ring-pulse"></div>
<div class="ring-pulse"></div>
<div class="ring-icon">🛵</div>
</div>
<div class="ring-title">您有新的派单</div>
<div class="ring-subtitle">调度中心为您指派了新订单</div>
</div>
<!-- 倒计时 -->
<div class="countdown-section">
<div class="countdown-ring">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" class="countdown-track"/>
<circle cx="50" cy="50" r="45" class="countdown-progress" id="progress"/>
</svg>
<div class="countdown-number">
<div class="countdown-number__val" id="countdownVal">30</div>
<div class="countdown-number__unit">秒后自动拒绝</div>
</div>
</div>
</div>
<!-- 订单摘要卡片 -->
<div class="order-card">
<div class="order-card__head">
<div>
<span class="order-num">#DP20260812-0928</span>
<span class="order-type-tag order-type-tag--home">到家</span>
</div>
<span class="order-distance">📏 距您 1.8km</span>
</div>
<div class="route">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">味之都食堂·高新店</div>
<div class="route-addr">科技三路 8 号 · 距您 0.8km</div>
</div>
</div>
<div class="route-connector"></div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">万科城 5 号楼 1802</div>
<div class="route-addr">高新区科技路 · 取货后 2.6km</div>
</div>
</div>
</div>
<div class="meta-grid">
<div class="meta-cell">
<div class="meta-label">餐品数量</div>
<div class="meta-value">4 份</div>
</div>
<div class="meta-cell">
<div class="meta-label">配送距离</div>
<div class="meta-value">2.6km</div>
</div>
<div class="meta-cell">
<div class="meta-label">要求送达</div>
<div class="meta-value">15:45 前</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-bar">
<button class="btn btn--reject" onclick="openReject()">
<span>拒绝</span>
<span class="btn__small">需选择理由</span>
</button>
<button class="btn btn--accept" onclick="acceptOrder()">
<span>✓ 接受派单</span>
<span class="btn__small">立即开始配送</span>
</button>
</div>
</div>
<!-- 拒单理由弹窗 -->
<div class="modal-mask" id="rejectModal">
<div class="modal">
<div class="modal__head">
<div class="modal__title">请选择拒单理由</div>
<button class="modal__close" onclick="closeReject()">×</button>
</div>
<div class="modal-tip">
⚠️ 拒单将影响您的接单评分,频繁拒单可能被暂停派单权限。请如实选择理由。
</div>
<div class="reason-list" id="reasonList">
<div class="reason-item" data-reason="车辆故障">
<div class="reason-icon">🛵</div>
<div class="reason-info">
<div class="reason-title">车辆故障</div>
<div class="reason-desc">电动车辆故障,无法继续配送</div>
</div>
<div class="reason-check"></div>
</div>
<div class="reason-item" data-reason="个人原因">
<div class="reason-icon">😣</div>
<div class="reason-info">
<div class="reason-title">个人原因</div>
<div class="reason-desc">身体不适/突发事件等</div>
</div>
<div class="reason-check"></div>
</div>
<div class="reason-item" data-reason="距离过远">
<div class="reason-icon">📍</div>
<div class="reason-info">
<div class="reason-title">距离过远</div>
<div class="reason-desc">当前车辆电量/油量不足</div>
</div>
<div class="reason-check"></div>
</div>
<div class="reason-item" data-reason="已接其他单">
<div class="reason-icon">📋</div>
<div class="reason-info">
<div class="reason-title">已接其他订单</div>
<div class="reason-desc">已有配送中订单无法准时送达</div>
</div>
<div class="reason-check"></div>
</div>
<div class="reason-item" data-reason="其他">
<div class="reason-icon">📝</div>
<div class="reason-info">
<div class="reason-title">其他</div>
<div class="reason-desc">手动填写具体原因</div>
</div>
<div class="reason-check"></div>
</div>
</div>
<button class="btn btn--accept" style="height:48px;font-size:15px" onclick="confirmReject()">
确认拒单
</button>
</div>
</div>
<div class="home-indicator"></div>
</div>
<script>
// 倒计时
const TOTAL = 30;
const CIRC = 2 * Math.PI * 45;
let remaining = TOTAL;
const progressEl = document.getElementById('progress');
const valEl = document.getElementById('countdownVal');
progressEl.style.strokeDasharray = CIRC;
function renderProgress() {
const offset = CIRC * (1 - remaining / TOTAL);
progressEl.style.strokeDashoffset = offset;
valEl.textContent = remaining;
}
renderProgress();
const timer = setInterval(() => {
remaining--;
renderProgress();
if (remaining <= 0) {
clearInterval(timer);
alert('已超时未响应,此单已自动转派');
location.href = '01-home.html';
}
}, 1000);
// 接受
function acceptOrder() {
clearInterval(timer);
alert('✅ 已接单,请尽快到达取货地点');
location.href = '04-order-detail.html?orderId=DP20260812-0928&state=accepted';
}
// 拒单弹窗
let selectedReason = '';
function openReject() {
document.getElementById('rejectModal').classList.add('show');
}
function closeReject() {
document.getElementById('rejectModal').classList.remove('show');
}
document.querySelectorAll('.reason-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.reason-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
selectedReason = item.dataset.reason;
});
});
function confirmReject() {
if (!selectedReason) {
alert('请选择拒单理由');
return;
}
clearInterval(timer);
alert('已拒单 · 理由:' + selectedReason);
location.href = '01-home.html';
}
</script>
</body>
</html>
@@ -0,0 +1,734 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>抢单大厅 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--white: #ffffff;
--success: #10b981;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
--shadow-md: 0 10px 28px rgba(15, 23, 42, 0.10);
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal {
width: 18px; height: 10px; background: #fff;
clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%);
}
.status-bar__wifi {
width: 16px; height: 12px; background: #fff;
clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%);
}
.status-bar__battery {
width: 24px; height: 12px;
border: 1.5px solid #fff;
border-radius: 3px;
position: relative;
padding: 1.5px;
}
.status-bar__battery::after {
content: '';
display: block;
width: 70%; height: 100%;
background: #fff;
border-radius: 1px;
}
.status-bar__battery::before {
content: '';
position: absolute;
right: -3px; top: 3px;
width: 2px; height: 6px;
background: #fff;
border-radius: 0 1px 1px 0;
}
.nav-bar {
flex-shrink: 0;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
padding: 4px 20px 14px;
display: flex;
align-items: center;
gap: 12px;
}
.nav-back {
color: white;
text-decoration: none;
font-size: 22px;
width: 32px;
}
.nav-title {
flex: 1;
font-size: 17px;
font-weight: 700;
}
.nav-count {
background: rgba(255, 255, 255, 0.2);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
}
.filter-bar {
flex-shrink: 0;
background: white;
padding: 10px 16px;
display: flex;
gap: 8px;
overflow-x: auto;
border-bottom: 1px solid var(--border);
}
.filter-bar::-webkit-scrollbar { display: none; }
.filter-pill {
padding: 6px 14px;
border-radius: 16px;
background: var(--background);
color: var(--text-secondary);
font-size: 12px;
white-space: nowrap;
cursor: pointer;
border: 1px solid transparent;
}
.filter-pill.active {
background: var(--primary);
color: white;
}
.sort-bar {
flex-shrink: 0;
padding: 8px 16px;
font-size: 12px;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--background);
}
.sort-bar__left {
display: flex;
align-items: center;
gap: 4px;
}
.sort-bar__count { color: var(--primary); font-weight: 700; }
.sort-toggle {
background: white;
padding: 4px 10px;
border-radius: 12px;
color: var(--text-primary);
font-size: 11px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.scroll-area {
flex: 1;
overflow-y: auto;
padding: 8px 16px 90px;
}
.scroll-area::-webkit-scrollbar { display: none; }
.order-card {
background: white;
border-radius: 14px;
padding: 14px;
margin-bottom: 12px;
box-shadow: var(--shadow-sm);
position: relative;
overflow: hidden;
}
.order-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 4px; height: 100%;
background: var(--primary);
}
.order-card.is-store::before { background: #f59e0b; }
.order-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.order-head__left {
display: flex;
align-items: center;
gap: 6px;
}
.order-num { font-size: 12px; color: var(--text-tertiary); }
.type-tag {
display: inline-block;
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
font-weight: 600;
}
.type-tag--home { background: #dbeafe; color: #1e40af; }
.type-tag--store { background: #fef3c7; color: #92400e; }
.hot-tag {
background: #fee2e2;
color: var(--primary-dark);
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
}
.route {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 0;
}
.route-point {
display: flex;
gap: 10px;
align-items: flex-start;
}
.route-icon {
width: 22px; height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
flex-shrink: 0;
color: white;
font-weight: 700;
}
.route-icon--start { background: var(--primary); }
.route-icon--end { background: var(--success); }
.route-info { flex: 1; padding-top: 1px; }
.route-name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.route-addr {
font-size: 11px;
color: var(--text-secondary);
}
.route-connector {
width: 2px;
height: 12px;
border-left: 2px dashed var(--border);
margin-left: 10px;
}
.meta-row {
display: flex;
gap: 14px;
margin: 10px 0;
padding: 8px 10px;
background: #fff0e8;
border-radius: 8px;
}
.meta-cell { flex: 1; }
.meta-label {
font-size: 10px;
color: var(--text-tertiary);
margin-bottom: 2px;
}
.meta-value {
font-size: 14px;
font-weight: 700;
color: var(--primary);
}
.order-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6px;
}
.distance-text {
font-size: 11px;
color: var(--text-secondary);
}
.countdown-tag {
background: #f3f4f6;
color: var(--text-secondary);
padding: 2px 8px;
border-radius: 8px;
font-size: 11px;
margin-left: 4px;
}
.grab-btn {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
border: none;
padding: 8px 22px;
border-radius: 20px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 4px 12px rgba(255, 109, 58, 0.3);
}
.grab-btn:active { transform: scale(0.96); }
.grab-btn.grabbed {
background: var(--border);
color: var(--text-secondary);
box-shadow: none;
}
.empty {
text-align: center;
padding: 60px 20px;
color: var(--text-tertiary);
font-size: 13px;
}
.empty__icon {
font-size: 48px;
margin-bottom: 8px;
opacity: 0.4;
}
/* 底部 Tab */
.tabbar {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 70px;
background: white;
border-top: 1px solid var(--border);
display: flex;
padding: 8px 0 20px;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
text-decoration: none;
color: var(--text-tertiary);
}
.tab.active { color: var(--primary); }
.tab__icon { font-size: 22px; }
.tab__label { font-size: 10px; font-weight: 600; }
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
}
.float-btn {
position: absolute;
right: 16px;
bottom: 86px;
width: 52px; height: 52px;
background: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
box-shadow: 0 8px 16px rgba(255, 109, 58, 0.4);
cursor: pointer;
z-index: 10;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">9:41</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<div class="nav-bar">
<a href="01-home.html" class="nav-back"></a>
<span class="nav-title">🔥 抢单大厅</span>
<span class="nav-count">大厅 5 单</span>
</div>
<div class="filter-bar">
<div class="filter-pill active" data-filter="all">全部</div>
<div class="filter-pill" data-filter="home">到家</div>
<div class="filter-pill" data-filter="store">到店</div>
<div class="filter-pill" data-filter="near">附近 3km</div>
<div class="filter-pill" data-filter="high">长距离</div>
</div>
<div class="sort-bar">
<div class="sort-bar__left">
📍 当前位置:科技三路 · 附近 <span class="sort-bar__count">5</span>
</div>
<div class="sort-toggle" onclick="toast('已切换为按距离排序')">
按距离 ↑↓
</div>
</div>
<div class="scroll-area">
<!-- 订单 1 -->
<div class="order-card" data-type="home">
<div class="order-head">
<div class="order-head__left">
<span class="order-num">#QD20260812-1421</span>
<span class="type-tag type-tag--home">到家</span>
<span class="hot-tag">🔥 急</span>
</div>
<span class="distance-text">📏 0.8km</span>
</div>
<div class="route">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">味之都食堂·高新店</div>
<div class="route-addr">科技三路 8 号</div>
</div>
</div>
<div class="route-connector"></div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">锦业一号楼 2503</div>
<div class="route-addr">锦业路 · 0.8km</div>
</div>
</div>
</div>
<div class="meta-row">
<div class="meta-cell">
<div class="meta-label">餐品</div>
<div class="meta-value">3 份</div>
</div>
<div class="meta-cell">
<div class="meta-label">配送距离</div>
<div class="meta-value">0.8km</div>
</div>
<div class="meta-cell">
<div class="meta-label">送达时限</div>
<div class="meta-value">30min</div>
</div>
</div>
<div class="order-footer">
<span class="distance-text">⏱ 剩余抢单时间 <span class="countdown-tag">02:35</span></span>
<button class="grab-btn" onclick="grabOrder(this)">立即抢</button>
</div>
</div>
<!-- 订单 2 -->
<div class="order-card is-store" data-type="store">
<div class="order-head">
<div class="order-head__left">
<span class="order-num">#QD20260812-1438</span>
<span class="type-tag type-tag--store">到店</span>
</div>
<span class="distance-text">📏 1.2km</span>
</div>
<div class="route">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">净菜加工中心</div>
<div class="route-addr">科技二路 6 号</div>
</div>
</div>
<div class="route-connector"></div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">家兴源食堂·科技路店</div>
<div class="route-addr">科技路 12 号 · 5.6km</div>
</div>
</div>
</div>
<div class="meta-row">
<div class="meta-cell">
<div class="meta-label">餐品</div>
<div class="meta-value">12 箱</div>
</div>
<div class="meta-cell">
<div class="meta-label">配送距离</div>
<div class="meta-value">5.6km</div>
</div>
<div class="meta-cell">
<div class="meta-label">送达时限</div>
<div class="meta-value">60min</div>
</div>
</div>
<div class="order-footer">
<span class="distance-text">⏱ 剩余抢单时间 <span class="countdown-tag">05:18</span></span>
<button class="grab-btn" onclick="grabOrder(this)">立即抢</button>
</div>
</div>
<!-- 订单 3 -->
<div class="order-card" data-type="home">
<div class="order-head">
<div class="order-head__left">
<span class="order-num">#QD20260812-1445</span>
<span class="type-tag type-tag--home">到家</span>
<span class="hot-tag">📦 大</span>
</div>
<span class="distance-text">📏 2.3km</span>
</div>
<div class="route">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">中央厨房·高新站</div>
<div class="route-addr">科技三路 8 号</div>
</div>
</div>
<div class="route-connector"></div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">紫薇田园都市 12 号楼</div>
<div class="route-addr">紫薇路 · 3.5km</div>
</div>
</div>
</div>
<div class="meta-row">
<div class="meta-cell">
<div class="meta-label">餐品</div>
<div class="meta-value">6 份</div>
</div>
<div class="meta-cell">
<div class="meta-label">配送距离</div>
<div class="meta-value">3.5km</div>
</div>
<div class="meta-cell">
<div class="meta-label">送达时限</div>
<div class="meta-value">45min</div>
</div>
</div>
<div class="order-footer">
<span class="distance-text">⏱ 剩余抢单时间 <span class="countdown-tag">08:42</span></span>
<button class="grab-btn" onclick="grabOrder(this)">立即抢</button>
</div>
</div>
<!-- 订单 4 -->
<div class="order-card is-store" data-type="store">
<div class="order-head">
<div class="order-head__left">
<span class="order-num">#QD20260812-1502</span>
<span class="type-tag type-tag--store">到店</span>
</div>
<span class="distance-text">📏 2.8km</span>
</div>
<div class="route">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">食材配送中心</div>
<div class="route-addr">高新区科技路</div>
</div>
</div>
<div class="route-connector"></div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">味之滨餐厅</div>
<div class="route-addr">长安区 · 4.8km</div>
</div>
</div>
</div>
<div class="meta-row">
<div class="meta-cell">
<div class="meta-label">餐品</div>
<div class="meta-value">8 箱</div>
</div>
<div class="meta-cell">
<div class="meta-label">配送距离</div>
<div class="meta-value">4.8km</div>
</div>
<div class="meta-cell">
<div class="meta-label">送达时限</div>
<div class="meta-value">50min</div>
</div>
</div>
<div class="order-footer">
<span class="distance-text">⏱ 剩余抢单时间 <span class="countdown-tag">12:15</span></span>
<button class="grab-btn" onclick="grabOrder(this)">立即抢</button>
</div>
</div>
<!-- 订单 5 -->
<div class="order-card" data-type="home">
<div class="order-head">
<div class="order-head__left">
<span class="order-num">#QD20260812-1518</span>
<span class="type-tag type-tag--home">到家</span>
</div>
<span class="distance-text">📏 3.0km</span>
</div>
<div class="route">
<div class="route-point">
<div class="route-icon route-icon--start"></div>
<div class="route-info">
<div class="route-name">味之都食堂·高新店</div>
<div class="route-addr">科技三路 8 号</div>
</div>
</div>
<div class="route-connector"></div>
<div class="route-point">
<div class="route-icon route-icon--end"></div>
<div class="route-info">
<div class="route-name">枫林绿洲 6 号楼 502</div>
<div class="route-addr">高新四路 · 3.8km</div>
</div>
</div>
</div>
<div class="meta-row">
<div class="meta-cell">
<div class="meta-label">餐品</div>
<div class="meta-value">2 份</div>
</div>
<div class="meta-cell">
<div class="meta-label">配送距离</div>
<div class="meta-value">3.8km</div>
</div>
<div class="meta-cell">
<div class="meta-label">送达时限</div>
<div class="meta-value">35min</div>
</div>
</div>
<div class="order-footer">
<span class="distance-text">⏱ 剩余抢单时间 <span class="countdown-tag">15:30</span></span>
<button class="grab-btn" onclick="grabOrder(this)">立即抢</button>
</div>
</div>
<div style="text-align:center;padding:16px;color:var(--text-tertiary);font-size:12px">
· 已加载全部 ·
</div>
</div>
<div class="float-btn" onclick="toast('已为您定位附近最近的订单')">🎯</div>
<!-- 底部 Tab -->
<div class="tabbar">
<a href="01-home.html" class="tab">
<span class="tab__icon">🏠</span>
<span class="tab__label">首页</span>
</a>
<a href="03-grab-pool.html" class="tab active">
<span class="tab__icon">🔥</span>
<span class="tab__label">抢单</span>
</a>
<a href="07-history.html" class="tab">
<span class="tab__icon">📚</span>
<span class="tab__label">历史</span>
</a>
<a href="08-profile.html" class="tab">
<span class="tab__icon">👤</span>
<span class="tab__label">我的</span>
</a>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
function grabOrder(btn) {
if (btn.classList.contains('grabbed')) return;
btn.classList.add('grabbed');
btn.textContent = '✓ 已抢';
toast('🎉 抢单成功!请尽快取货');
setTimeout(() => {
location.href = '04-order-detail.html?state=accepted';
}, 1200);
}
// 筛选
document.querySelectorAll('.filter-pill').forEach(pill => {
pill.addEventListener('click', () => {
document.querySelectorAll('.filter-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
const filter = pill.dataset.filter;
document.querySelectorAll('.order-card').forEach(card => {
if (filter === 'all' || card.dataset.type === filter || filter === 'near' || filter === 'high') {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,613 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单详情 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--white: #ffffff;
--success: #10b981;
--info: #3b82f6;
--warn: #f59e0b;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal { width:18px;height:10px;background:#fff;clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%); }
.status-bar__wifi { width:16px;height:12px;background:#fff;clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%); }
.status-bar__battery { width:24px;height:12px;border:1.5px solid #fff;border-radius:3px;position:relative;padding:1.5px;}
.status-bar__battery::after { content:'';display:block;width:70%;height:100%;background:#fff;border-radius:1px;}
.status-bar__battery::before { content:'';position:absolute;right:-3px;top:3px;width:2px;height:6px;background:#fff;border-radius:0 1px 1px 0;}
.nav-bar {
flex-shrink: 0;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
padding: 4px 16px 14px;
display: flex;
align-items: center;
gap: 12px;
}
.nav-back { color: white; text-decoration: none; font-size: 22px; width: 32px; }
.nav-title { flex: 1; font-size: 16px; font-weight: 700; }
.nav-status {
background: rgba(255, 255, 255, 0.2);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
}
.scroll-area {
flex: 1;
overflow-y: auto;
padding: 12px 16px 100px;
}
.scroll-area::-webkit-scrollbar { display: none; }
.card {
background: white;
border-radius: 14px;
padding: 16px;
margin-bottom: 12px;
box-shadow: var(--shadow-sm);
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.card__title {
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
gap: 6px;
}
.card__title::before {
content: '';
width: 4px; height: 14px;
background: var(--primary);
border-radius: 2px;
}
/* 进度条 */
.progress-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0 20px;
position: relative;
}
.progress-line {
position: absolute;
top: 28px; left: 30px; right: 30px;
height: 3px;
background: var(--border);
z-index: 0;
}
.progress-line__filled {
position: absolute;
top: 28px; left: 30px;
width: calc((100% - 60px) * 0.34);
height: 3px;
background: var(--primary);
z-index: 1;
transition: width 0.3s;
}
.step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
position: relative;
z-index: 2;
}
.step__dot {
width: 24px; height: 24px;
border-radius: 50%;
background: white;
border: 3px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: var(--text-tertiary);
font-weight: 700;
}
.step.done .step__dot {
background: var(--primary);
border-color: var(--primary);
color: white;
}
.step.active .step__dot {
background: white;
border-color: var(--primary);
color: var(--primary);
box-shadow: 0 0 0 4px rgba(255, 109, 58, 0.2);
}
.step__label {
font-size: 11px;
color: var(--text-secondary);
}
.step.done .step__label,
.step.active .step__label {
color: var(--text-primary);
font-weight: 600;
}
/* 路线信息 */
.route-card {
background: white;
border-radius: 14px;
padding: 16px;
margin-bottom: 12px;
box-shadow: var(--shadow-sm);
position: relative;
}
.route-row {
display: flex;
gap: 12px;
align-items: flex-start;
}
.route-icons {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 4px;
}
.route-dot {
width: 28px; height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: white;
font-weight: 700;
flex-shrink: 0;
}
.route-dot--start { background: var(--primary); }
.route-dot--end { background: var(--success); }
.route-line-vertical {
width: 2px;
flex: 1;
min-height: 50px;
border-left: 2px dashed var(--border);
margin: 4px 0;
}
.route-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0 0 8px 0;
gap: 28px;
}
.route-block__name {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 2px;
}
.route-block__addr {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.route-block__contact {
margin-top: 6px;
display: flex;
gap: 8px;
align-items: center;
}
.contact-tag {
background: var(--primary-light);
color: var(--primary-dark);
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
/* 餐品列表 */
.goods-list {
padding: 0;
}
.goods-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px dashed var(--border);
}
.goods-item:last-child { border-bottom: none; }
.goods-img {
width: 50px; height: 50px;
background: linear-gradient(135deg, #fef3c7, #fed7aa);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.goods-info { flex: 1; }
.goods-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.goods-spec {
font-size: 11px;
color: var(--text-secondary);
}
.goods-meta {
text-align: right;
font-size: 12px;
color: var(--text-secondary);
}
.goods-qty {
font-size: 14px;
font-weight: 700;
color: var(--primary);
}
/* 订单信息 */
.info-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
.info-label { color: var(--text-secondary); }
.info-value { color: var(--text-primary); font-weight: 600; }
.info-value--price { color: var(--primary); }
.info-divider {
border-top: 1px dashed var(--border);
padding-top: 10px;
}
.info-row.total {
padding-top: 10px;
border-top: 1px solid var(--border);
}
.info-row.total .info-value {
font-size: 18px;
color: var(--primary);
}
/* 操作按钮 */
.action-bar {
position: absolute;
bottom: 0; left: 0; right: 0;
background: white;
padding: 12px 16px 32px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
display: flex;
gap: 10px;
z-index: 10;
}
.btn {
flex: 1;
height: 48px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn--ghost {
background: var(--background);
color: var(--text-primary);
flex: 0 0 110px;
}
.btn--primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
box-shadow: 0 6px 12px rgba(255, 109, 58, 0.25);
}
.tag-list {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 8px;
}
.info-tag {
font-size: 10px;
padding: 2px 8px;
border-radius: 6px;
background: #f3f4f6;
color: var(--text-secondary);
}
.info-tag--warn {
background: #fef3c7;
color: #92400e;
}
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
z-index: 20;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">9:41</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<div class="nav-bar">
<a href="01-home.html" class="nav-back"></a>
<span class="nav-title">订单详情</span>
<span class="nav-status">已接单</span>
</div>
<div class="scroll-area">
<!-- 进度条 -->
<div class="card">
<div class="progress-bar">
<div class="progress-line"></div>
<div class="progress-line__filled"></div>
<div class="step done">
<div class="step__dot"></div>
<div class="step__label">接单</div>
</div>
<div class="step active">
<div class="step__dot">2</div>
<div class="step__label">取货</div>
</div>
<div class="step">
<div class="step__dot">3</div>
<div class="step__label">配送</div>
</div>
<div class="step">
<div class="step__dot">4</div>
<div class="step__label">送达</div>
</div>
</div>
<div style="text-align:center;font-size:12px;color:var(--text-secondary);padding:8px 0 0">
预计 <span style="color:var(--primary);font-weight:700">15:45</span> 前送达 · 剩余 38 分钟
</div>
</div>
<!-- 路线 -->
<div class="route-card">
<div class="card__head">
<div class="card__title">配送路线</div>
<span style="font-size:12px;color:var(--primary);font-weight:600">全程 2.6km · 约 12 分钟</span>
</div>
<div class="route-row">
<div class="route-icons">
<div class="route-dot route-dot--start"></div>
<div class="route-line-vertical"></div>
<div class="route-dot route-dot--end"></div>
</div>
<div class="route-content">
<div>
<div class="route-block__name">味之都食堂·高新店</div>
<div class="route-block__addr">高新区科技三路 8 号 · 联系人:王经理</div>
<div class="route-block__contact">
<a href="javascript:void(0)" class="contact-tag" onclick="toast('正在呼叫商家')">📞 呼叫商家</a>
<a href="javascript:void(0)" class="contact-tag" onclick="toast('已打开导航')">📍 导航</a>
</div>
</div>
<div>
<div class="route-block__name">万科城 5 号楼 1802</div>
<div class="route-block__addr">高新区科技路 · 联系人:张女士 138****6688</div>
<div class="route-block__contact">
<a href="javascript:void(0)" class="contact-tag" onclick="toast('正在呼叫客户')">📞 呼叫客户</a>
<a href="javascript:void(0)" class="contact-tag" onclick="toast('已发送短信提醒')">💬 短信</a>
</div>
</div>
</div>
</div>
</div>
<!-- 餐品清单 -->
<div class="card">
<div class="card__head">
<div class="card__title">餐品清单</div>
<span style="font-size:12px;color:var(--text-secondary)">共 4 份</span>
</div>
<div class="goods-list">
<div class="goods-item">
<div class="goods-img">🍱</div>
<div class="goods-info">
<div class="goods-name">红烧牛肉套餐</div>
<div class="goods-spec">主荤·标准份 · 含米饭/汤</div>
</div>
<div class="goods-meta">
<div class="goods-qty">×2</div>
</div>
</div>
<div class="goods-item">
<div class="goods-img">🥗</div>
<div class="goods-info">
<div class="goods-name">清炒时蔬套餐</div>
<div class="goods-spec">素菜·小份 · 含米饭</div>
</div>
<div class="goods-meta">
<div class="goods-qty">×1</div>
</div>
</div>
<div class="goods-item">
<div class="goods-img">🍲</div>
<div class="goods-info">
<div class="goods-name">番茄鸡蛋汤</div>
<div class="goods-spec">汤类·大份</div>
</div>
<div class="goods-meta">
<div class="goods-qty">×1</div>
</div>
</div>
</div>
<div class="tag-list">
<span class="info-tag info-tag--warn">⚠ 请核对溯源码</span>
<span class="info-tag">📦 含冷链保温箱</span>
<span class="info-tag">🥢 含餐具 4 份</span>
</div>
</div>
<!-- 订单信息 -->
<div class="card">
<div class="card__head">
<div class="card__title">订单信息</div>
</div>
<div class="info-list">
<div class="info-row">
<span class="info-label">订单编号</span>
<span class="info-value">DP20260812-0928</span>
</div>
<div class="info-row">
<span class="info-label">订单类型</span>
<span class="info-value">到家配送</span>
</div>
<div class="info-row">
<span class="info-label">接单方式</span>
<span class="info-value" style="color:var(--primary)">📌 派单</span>
</div>
<div class="info-row">
<span class="info-label">下单时间</span>
<span class="info-value">14:08:25</span>
</div>
<div class="info-row">
<span class="info-label">接单时间</span>
<span class="info-value">14:10:12</span>
</div>
<div class="info-row">
<span class="info-label">餐品份数</span>
<span class="info-value">4 份</span>
</div>
<div class="info-row">
<span class="info-label">配送距离</span>
<span class="info-value">2.6 km</span>
</div>
<div class="info-row">
<span class="info-label">预计时长</span>
<span class="info-value">12 min</span>
</div>
<div class="info-row">
<span class="info-label">收货方式</span>
<span class="info-value" style="color:var(--primary)">🔢 验证码收货</span>
</div>
</div>
</div>
<!-- 客户备注 -->
<div class="card">
<div class="card__head">
<div class="card__title">客户备注</div>
</div>
<div style="font-size:13px;color:var(--text-primary);line-height:1.6">
📝 麻烦放门口即可,不要按门铃,孩子正在午休。电话联系后放在 1802 门口鞋柜上,谢谢!
</div>
</div>
</div>
<div class="action-bar">
<button class="btn btn--ghost" onclick="toast('异常上报页面开发中')">⚠ 异常</button>
<button class="btn btn--primary" onclick="location.href='05-delivering.html'">🚀 到店报到</button>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
</script>
</body>
</html>
@@ -0,0 +1,606 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配送中 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--white: #ffffff;
--success: #10b981;
--info: #3b82f6;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
--shadow-md: 0 10px 28px rgba(15, 23, 42, 0.10);
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
background: linear-gradient(135deg, #8b5cf6, #6d28d9);
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal { width:18px;height:10px;background:#fff;clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%); }
.status-bar__wifi { width:16px;height:12px;background:#fff;clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%); }
.status-bar__battery { width:24px;height:12px;border:1.5px solid #fff;border-radius:3px;position:relative;padding:1.5px;}
.status-bar__battery::after { content:'';display:block;width:70%;height:100%;background:#fff;border-radius:1px;}
.status-bar__battery::before { content:'';position:absolute;right:-3px;top:3px;width:2px;height:6px;background:#fff;border-radius:0 1px 1px 0;}
/* 顶部地图区域(模拟) */
.map-area {
height: 280px;
flex-shrink: 0;
background:
radial-gradient(circle at 30% 40%, #e0e7ff 0%, transparent 60%),
radial-gradient(circle at 70% 60%, #fef3c7 0%, transparent 60%),
linear-gradient(135deg, #e0e7ff, #f0f9ff);
position: relative;
overflow: hidden;
}
/* 地图网格 */
.map-area::before {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(99, 102, 241, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(99, 102, 241, 0.08) 1px, transparent 1px);
background-size: 40px 40px;
}
/* 模拟道路 */
.map-area::after {
content: '';
position: absolute;
width: 200%;
height: 30px;
background: white;
box-shadow: 0 0 0 1px #d1d5db;
top: 60%;
left: -50%;
transform: rotate(-12deg);
}
.nav-bar {
position: absolute;
top: 44px; left: 0; right: 0;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
z-index: 10;
}
.nav-back {
width: 36px; height: 36px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
color: var(--text-primary);
font-size: 20px;
box-shadow: var(--shadow-sm);
}
.nav-status {
flex: 1;
background: rgba(139, 92, 246, 0.95);
color: white;
padding: 8px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
justify-content: center;
}
.nav-action {
width: 36px; height: 36px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
box-shadow: var(--shadow-sm);
cursor: pointer;
}
/* 地图标记 */
.map-marker {
position: absolute;
transform: translate(-50%, -100%);
z-index: 5;
}
.map-marker--start {
top: 60%; left: 22%;
}
.map-marker--end {
top: 30%; left: 78%;
}
.map-marker--rider {
top: 45%; left: 50%;
z-index: 6;
}
.marker-icon {
width: 32px; height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: 700;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
border: 2px solid white;
}
.marker-icon--start { background: var(--primary); }
.marker-icon--end { background: var(--success); }
.marker-icon--rider {
background: #8b5cf6;
width: 40px; height: 40px;
font-size: 20px;
animation: riderPulse 2s ease-in-out infinite;
}
@keyframes riderPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.5), 0 4px 8px rgba(0,0,0,0.2); }
50% { box-shadow: 0 0 0 12px rgba(139, 92, 246, 0), 0 4px 8px rgba(0,0,0,0.2); }
}
.map-route {
position: absolute;
top: 60%; left: 22%;
width: 60%;
height: 100px;
transform: rotate(-25deg);
transform-origin: 0 0;
border-top: 4px dashed var(--primary);
z-index: 2;
pointer-events: none;
}
.map-float-info {
position: absolute;
top: 110px;
left: 16px;
background: white;
padding: 10px 14px;
border-radius: 12px;
box-shadow: var(--shadow-md);
z-index: 10;
font-size: 12px;
min-width: 140px;
}
.map-float-info__label {
color: var(--text-secondary);
font-size: 11px;
}
.map-float-info__value {
color: var(--text-primary);
font-size: 17px;
font-weight: 700;
margin-top: 2px;
}
.map-float-info__eta {
color: var(--primary);
font-size: 12px;
font-weight: 600;
margin-top: 4px;
}
.map-locate {
position: absolute;
right: 16px;
bottom: 16px;
width: 40px; height: 40px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
box-shadow: var(--shadow-md);
cursor: pointer;
z-index: 10;
}
.content-area {
flex: 1;
overflow-y: auto;
padding: 16px;
padding-bottom: 100px;
}
.content-area::-webkit-scrollbar { display: none; }
.card {
background: white;
border-radius: 14px;
padding: 14px;
margin-bottom: 12px;
box-shadow: var(--shadow-sm);
}
/* 配送进度步骤 */
.steps-card {
margin-top: -30px;
position: relative;
z-index: 8;
}
.step-item {
display: flex;
gap: 12px;
padding: 8px 0;
}
.step-item__icon {
width: 28px; height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
flex-shrink: 0;
color: white;
font-weight: 700;
position: relative;
}
.step-item__icon--done { background: var(--success); }
.step-item__icon--active {
background: var(--primary);
box-shadow: 0 0 0 4px rgba(255, 109, 58, 0.2);
}
.step-item__icon--pending {
background: white;
border: 2px dashed var(--border);
color: var(--text-tertiary);
}
.step-item__icon:not(:last-child)::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 24px;
background: var(--border);
}
.step-item:last-child .step-item__icon::after { display: none; }
.step-item__icon--done::after { background: var(--success); }
.step-item__content { flex: 1; padding-top: 4px; }
.step-item__title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 2px;
}
.step-item__time {
font-size: 11px;
color: var(--text-secondary);
}
.step-item__desc {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
line-height: 1.5;
}
.status-mini {
font-size: 10px;
padding: 2px 6px;
border-radius: 6px;
background: var(--primary-light);
color: var(--primary-dark);
font-weight: 600;
}
.status-mini--done {
background: #d1fae5;
color: #065f46;
}
/* 客户信息 */
.customer-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
}
.customer-avatar {
width: 48px; height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #fde68a, #fbbf24);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.customer-info { flex: 1; }
.customer-name {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 2px;
}
.customer-addr {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.customer-actions {
display: flex;
gap: 6px;
}
.icon-btn {
width: 38px; height: 38px;
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
}
.icon-btn--call {
background: var(--success);
color: white;
}
.icon-btn--msg {
background: var(--info);
color: white;
}
.order-summary {
display: flex;
justify-content: space-around;
padding: 12px 14px;
}
.summary-cell { text-align: center; }
.summary-num {
font-size: 17px;
font-weight: 700;
color: var(--primary);
}
.summary-label {
font-size: 11px;
color: var(--text-secondary);
margin-top: 2px;
}
/* 操作按钮 */
.action-bar {
position: absolute;
bottom: 0; left: 0; right: 0;
background: white;
padding: 12px 16px 32px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
display: flex;
gap: 10px;
z-index: 10;
}
.btn {
flex: 1;
height: 50px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn--ghost {
background: var(--background);
color: var(--text-primary);
flex: 0 0 100px;
}
.btn--primary {
background: linear-gradient(135deg, var(--success), #059669);
color: white;
box-shadow: 0 6px 12px rgba(16, 185, 129, 0.25);
}
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
z-index: 20;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">15:08</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<!-- 地图区 -->
<div class="map-area">
<div class="nav-bar">
<a href="04-order-detail.html" class="nav-back"></a>
<div class="nav-status">🛵 配送中 · 已骑行 6 分钟</div>
<div class="nav-action" onclick="toast('已开启导航')">🧭</div>
</div>
<div class="map-float-info">
<div class="map-float-info__label">距离客户</div>
<div class="map-float-info__value">1.8 km</div>
<div class="map-float-info__eta">预计 15:25 送达</div>
</div>
<div class="map-route"></div>
<div class="map-marker map-marker--start">
<div class="marker-icon marker-icon--start"></div>
</div>
<div class="map-marker map-marker--rider">
<div class="marker-icon marker-icon--rider">🛵</div>
</div>
<div class="map-marker map-marker--end">
<div class="marker-icon marker-icon--end"></div>
</div>
<div class="map-locate" onclick="toast('已定位到当前位置')">📍</div>
</div>
<div class="content-area">
<!-- 配送步骤 -->
<div class="card steps-card">
<div class="step-item">
<div class="step-item__icon step-item__icon--done"></div>
<div class="step-item__content">
<div class="step-item__title">
已接单
<span class="status-mini status-mini--done">完成</span>
</div>
<div class="step-item__time">14:10:12</div>
</div>
</div>
<div class="step-item">
<div class="step-item__icon step-item__icon--done"></div>
<div class="step-item__content">
<div class="step-item__title">
已到店取货
<span class="status-mini status-mini--done">完成</span>
</div>
<div class="step-item__time">14:22:08 · 味之都食堂·高新店</div>
<div class="step-item__desc">餐品核对完成,共 4 份,溯源码已扫描 ✓</div>
</div>
</div>
<div class="step-item">
<div class="step-item__icon step-item__icon--active">🛵</div>
<div class="step-item__content">
<div class="step-item__title">
配送中
<span class="status-mini">当前</span>
</div>
<div class="step-item__time">15:02:30 开始配送</div>
<div class="step-item__desc">距离客户 1.8km · 预计 15:25 送达</div>
</div>
</div>
<div class="step-item">
<div class="step-item__icon step-item__icon--pending">4</div>
<div class="step-item__content">
<div class="step-item__title" style="color:var(--text-secondary)">送达签收</div>
<div class="step-item__time" style="color:var(--text-tertiary)">待完成</div>
</div>
</div>
</div>
<!-- 客户信息 -->
<div class="card">
<div class="customer-card">
<div class="customer-avatar">👩</div>
<div class="customer-info">
<div class="customer-name">张女士</div>
<div class="customer-addr">万科城 5 号楼 1802 · 138****6688</div>
</div>
<div class="customer-actions">
<button class="icon-btn icon-btn--call" onclick="toast('正在呼叫张女士')">📞</button>
<button class="icon-btn icon-btn--msg" onclick="toast('已发送到达提醒短信')">💬</button>
</div>
</div>
<div style="padding:8px 14px 14px;font-size:12px;color:var(--text-secondary);line-height:1.6;border-top:1px dashed var(--border);margin-top:4px">
💬 <strong style="color:var(--text-primary)">客户备注:</strong>
麻烦放门口即可,不要按门铃,孩子正在午休。
</div>
</div>
<!-- 订单概览 -->
<div class="card">
<div class="order-summary">
<div class="summary-cell">
<div class="summary-num">4 份</div>
<div class="summary-label">餐品</div>
</div>
<div class="summary-cell">
<div class="summary-num">2.6km</div>
<div class="summary-label">配送距离</div>
</div>
<div class="summary-cell">
<div class="summary-num">15:45</div>
<div class="summary-label">要求送达</div>
</div>
</div>
</div>
</div>
<div class="action-bar">
<button class="btn btn--ghost" onclick="toast('异常上报页面开发中')">⚠ 异常</button>
<button class="btn btn--primary" onclick="location.href='06-finish-sign.html'">📍 已到达 · 验证码签收</button>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
</script>
</body>
</html>
@@ -0,0 +1,634 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>验证码签收 | 营养管理餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--white: #ffffff;
--success: #10b981;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
background: linear-gradient(135deg, #10b981, #059669);
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal { width:18px;height:10px;background:#fff;clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%); }
.status-bar__wifi { width:16px;height:12px;background:#fff;clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%); }
.status-bar__battery { width:24px;height:12px;border:1.5px solid #fff;border-radius:3px;position:relative;padding:1.5px;}
.status-bar__battery::after { content:'';display:block;width:70%;height:100%;background:#fff;border-radius:1px;}
.status-bar__battery::before { content:'';position:absolute;right:-3px;top:3px;width:2px;height:6px;background:#fff;border-radius:0 1px 1px 0;}
.nav-bar {
flex-shrink: 0;
background: linear-gradient(135deg, #10b981, #059669);
color: white;
padding: 4px 16px 14px;
display: flex;
align-items: center;
gap: 12px;
}
.nav-back { color: white; text-decoration: none; font-size: 22px; width: 32px; }
.nav-title { flex: 1; font-size: 17px; font-weight: 700; }
.scroll-area {
flex: 1;
overflow-y: auto;
padding: 16px;
padding-bottom: 110px;
}
.scroll-area::-webkit-scrollbar { display: none; }
.head-banner {
background: white;
border-radius: 14px;
padding: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 14px;
box-shadow: var(--shadow-sm);
}
.head-banner__icon {
width: 52px; height: 52px;
background: linear-gradient(135deg, #d1fae5, #6ee7b7);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
flex-shrink: 0;
}
.head-banner__title {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 2px;
}
.head-banner__desc {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.card {
background: white;
border-radius: 14px;
padding: 16px;
margin-bottom: 12px;
box-shadow: var(--shadow-sm);
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.card__title {
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
gap: 6px;
}
.card__title::before {
content: '';
width: 4px; height: 14px;
background: var(--primary);
border-radius: 2px;
}
.card__required {
color: var(--primary);
font-size: 12px;
font-weight: 600;
}
.card__optional {
color: var(--text-tertiary);
font-size: 12px;
}
/* 验证码输入 */
.code-area {
padding: 8px 0;
}
.code-tip {
font-size: 13px;
color: var(--text-secondary);
text-align: center;
margin-bottom: 14px;
line-height: 1.6;
}
.code-tip strong {
color: var(--text-primary);
}
.code-inputs {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 14px;
}
.code-box {
width: 44px; height: 56px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
text-align: center;
outline: none;
background: var(--background);
transition: all 0.2s;
font-family: monospace;
}
.code-box:focus {
border-color: var(--primary);
background: white;
box-shadow: 0 0 0 3px rgba(255, 109, 58, 0.15);
}
.code-box.filled {
border-color: var(--primary);
background: var(--primary-light);
color: var(--primary-dark);
}
.code-actions {
display: flex;
justify-content: center;
gap: 16px;
padding-top: 4px;
}
.code-action {
background: transparent;
border: none;
color: var(--primary);
font-size: 13px;
font-weight: 600;
cursor: pointer;
padding: 6px 12px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.code-action:disabled {
color: var(--text-tertiary);
cursor: not-allowed;
}
.code-notice {
background: #fef3c7;
color: #92400e;
padding: 10px 12px;
border-radius: 8px;
font-size: 12px;
line-height: 1.6;
margin-top: 12px;
display: flex;
gap: 6px;
}
/* 拍照区 */
.photo-area {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.photo-slot {
aspect-ratio: 1;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 4px;
cursor: pointer;
font-size: 11px;
color: var(--text-secondary);
position: relative;
overflow: hidden;
}
.photo-slot--taken {
background: linear-gradient(135deg, #fde68a, #fbbf24);
color: #1f2937;
}
.photo-slot--taken::after {
content: '📦';
font-size: 32px;
}
.photo-slot--empty {
background: var(--background);
border: 1.5px dashed var(--border);
}
.photo-slot__icon {
font-size: 24px;
color: var(--text-tertiary);
}
.photo-slot--taken .photo-slot__label {
position: absolute;
bottom: 4px;
background: rgba(0,0,0,0.6);
color: white;
padding: 1px 6px;
border-radius: 4px;
font-size: 10px;
}
/* 工作量记录 */
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 13px;
border-bottom: 1px dashed var(--border);
}
.info-row:last-child { border-bottom: none; }
.info-label { color: var(--text-secondary); }
.info-value { font-weight: 600; color: var(--text-primary); }
.info-row--highlight {
padding-top: 12px;
border-top: 2px solid var(--border);
margin-top: 4px;
}
.info-row--highlight .info-value {
font-size: 17px;
color: var(--primary);
}
/* 操作按钮 */
.action-bar {
position: absolute;
bottom: 0; left: 0; right: 0;
background: white;
padding: 12px 16px 32px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
display: flex;
gap: 10px;
z-index: 10;
}
.btn {
flex: 1;
height: 52px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn--primary {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
box-shadow: 0 6px 12px rgba(16, 185, 129, 0.25);
}
.btn--primary:disabled {
background: #d1d5db;
box-shadow: none;
cursor: not-allowed;
}
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
z-index: 20;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
/* 成功弹窗 */
.success-modal {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
}
.success-modal.show { display: flex; }
.success-box {
background: white;
width: 280px;
border-radius: 20px;
padding: 28px 20px 20px;
text-align: center;
animation: popIn 0.3s;
}
@keyframes popIn {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.success-icon {
width: 72px; height: 72px;
background: linear-gradient(135deg, #10b981, #059669);
border-radius: 50%;
margin: 0 auto 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
animation: checkPop 0.5s ease 0.2s both;
}
@keyframes checkPop {
0% { transform: scale(0); }
60% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.success-title {
font-size: 19px;
font-weight: 700;
margin-bottom: 6px;
}
.success-stats {
display: flex;
justify-content: space-around;
padding: 12px 0 16px;
border-top: 1px dashed var(--border);
border-bottom: 1px dashed var(--border);
margin: 12px 0;
}
.success-stat__num {
font-size: 18px;
font-weight: 700;
color: var(--primary);
}
.success-stat__label {
font-size: 11px;
color: var(--text-secondary);
margin-top: 2px;
}
.success-desc {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 18px;
}
.success-btn {
width: 100%;
height: 44px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
}
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">15:22</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<div class="nav-bar">
<a href="05-delivering.html" class="nav-back"></a>
<span class="nav-title">验证码签收</span>
</div>
<div class="scroll-area">
<div class="head-banner">
<div class="head-banner__icon">📍</div>
<div>
<div class="head-banner__title">已到达目的地</div>
<div class="head-banner__desc">万科城 5 号楼 1802 · 张女士 · 提前 23 分钟到达</div>
</div>
</div>
<!-- 验证码输入 -->
<div class="card">
<div class="card__head">
<div class="card__title">验证码签收</div>
<span class="card__required">*必填</span>
</div>
<div class="code-area">
<div class="code-tip">
请向客户索取 <strong>6 位数字验证码</strong><br>
输入正确验证码后可确认送达
</div>
<div class="code-inputs">
<input type="tel" class="code-box" maxlength="1" data-idx="0">
<input type="tel" class="code-box" maxlength="1" data-idx="1">
<input type="tel" class="code-box" maxlength="1" data-idx="2">
<input type="tel" class="code-box" maxlength="1" data-idx="3">
<input type="tel" class="code-box" maxlength="1" data-idx="4">
<input type="tel" class="code-box" maxlength="1" data-idx="5">
</div>
<div class="code-actions">
<button class="code-action" onclick="toast('已发送验证码到客户手机,请客户查收短信')">📲 发送验证码</button>
<button class="code-action" onclick="toast('请拨打客户电话 138****6688 索取验证码')">📞 联系客户</button>
</div>
<div class="code-notice">
<span>⚠️</span>
<span>验证码由系统在下单时发送至客户手机,客户未收到可点击「发送验证码」重新获取。验证码 5 分钟内有效。</span>
</div>
</div>
</div>
<!-- 拍照留证 -->
<div class="card">
<div class="card__head">
<div class="card__title">拍照留证</div>
<span class="card__required">*至少 1 张</span>
</div>
<div class="photo-area">
<div class="photo-slot photo-slot--taken">
<span class="photo-slot__label">餐品放置</span>
</div>
<div class="photo-slot photo-slot--taken">
<span class="photo-slot__label">门口环境</span>
</div>
<div class="photo-slot photo-slot--empty" onclick="toast('打开相机')">
<span class="photo-slot__icon">+</span>
<span>添加照片</span>
</div>
</div>
</div>
<!-- 工作量记录 -->
<div class="card">
<div class="card__head">
<div class="card__title">本次工作量</div>
</div>
<div class="info-row">
<span class="info-label">餐品数量</span>
<span class="info-value">4 份</span>
</div>
<div class="info-row">
<span class="info-label">配送距离</span>
<span class="info-value">2.6 km</span>
</div>
<div class="info-row">
<span class="info-label">配送时长</span>
<span class="info-value">20 分钟</span>
</div>
<div class="info-row">
<span class="info-label">送达时间</span>
<span class="info-value">15:22 · 提前 23 分钟</span>
</div>
<div class="info-row info-row--highlight">
<span class="info-label" style="font-weight:700">本次完成 1 单</span>
<span class="info-value">已记录</span>
</div>
</div>
</div>
<div class="action-bar">
<button class="btn btn--primary" id="finishBtn" disabled onclick="confirmFinish()">✓ 输入验证码后确认送达</button>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
<!-- 成功弹窗 -->
<div class="success-modal" id="successModal">
<div class="success-box">
<div class="success-icon"></div>
<div class="success-title">配送完成 🎉</div>
<div class="success-stats">
<div>
<div class="success-stat__num">4 份</div>
<div class="success-stat__label">餐品</div>
</div>
<div>
<div class="success-stat__num">2.6km</div>
<div class="success-stat__label">距离</div>
</div>
<div>
<div class="success-stat__num">20min</div>
<div class="success-stat__label">时长</div>
</div>
</div>
<div class="success-desc">
已记录本次配送工作量<br>
<span style="color:#10b981;font-weight:600">提前 23 分钟送达 · 准时配送</span>
</div>
<button class="success-btn" onclick="location.href='01-home.html'">返回工作台</button>
</div>
</div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
// 验证码自动跳格
const boxes = document.querySelectorAll('.code-box');
const finishBtn = document.getElementById('finishBtn');
boxes.forEach((box, idx) => {
box.addEventListener('input', e => {
const v = e.target.value.replace(/\D/g, '');
e.target.value = v.slice(0, 1);
if (v && idx < boxes.length - 1) {
boxes[idx + 1].focus();
}
e.target.classList.toggle('filled', !!v);
checkComplete();
});
box.addEventListener('keydown', e => {
if (e.key === 'Backspace' && !e.target.value && idx > 0) {
boxes[idx - 1].focus();
}
});
box.addEventListener('paste', e => {
e.preventDefault();
const text = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '').slice(0, 6);
text.split('').forEach((ch, i) => {
if (boxes[i]) {
boxes[i].value = ch;
boxes[i].classList.add('filled');
}
});
boxes[Math.min(text.length, 5)].focus();
checkComplete();
});
});
function checkComplete() {
const allFilled = Array.from(boxes).every(b => b.value);
finishBtn.disabled = !allFilled;
if (allFilled) {
finishBtn.textContent = '✓ 确认送达';
} else {
finishBtn.textContent = '✓ 输入验证码后确认送达';
}
}
function confirmFinish() {
document.getElementById('successModal').classList.add('show');
}
</script>
</body>
</html>
@@ -0,0 +1,620 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>历史订单 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--white: #ffffff;
--success: #10b981;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
--shadow-md: 0 10px 28px rgba(15, 23, 42, 0.10);
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal { width:18px;height:10px;background:#fff;clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%); }
.status-bar__wifi { width:16px;height:12px;background:#fff;clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%); }
.status-bar__battery { width:24px;height:12px;border:1.5px solid #fff;border-radius:3px;position:relative;padding:1.5px;}
.status-bar__battery::after { content:'';display:block;width:70%;height:100%;background:#fff;border-radius:1px;}
.status-bar__battery::before { content:'';position:absolute;right:-3px;top:3px;width:2px;height:6px;background:#fff;border-radius:0 1px 1px 0;}
/* 顶部收益统计 */
.summary-bar {
flex-shrink: 0;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
padding: 0 16px 20px;
}
.summary-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.summary-title {
font-size: 17px;
font-weight: 700;
}
.summary-date {
background: rgba(255, 255, 255, 0.2);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
cursor: pointer;
}
.summary-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
.summary-item {
text-align: center;
}
.summary-num {
font-size: 20px;
font-weight: 700;
margin-bottom: 2px;
}
.summary-num--lg { font-size: 24px; }
.summary-label {
font-size: 11px;
opacity: 0.9;
}
/* Tab */
.tab-bar {
flex-shrink: 0;
background: white;
display: flex;
padding: 0 16px;
border-bottom: 1px solid var(--border);
}
.tab-item {
flex: 1;
text-align: center;
padding: 12px 0;
font-size: 13px;
color: var(--text-secondary);
cursor: pointer;
position: relative;
font-weight: 500;
}
.tab-item.active {
color: var(--primary);
font-weight: 700;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0; left: 50%;
transform: translateX(-50%);
width: 24px; height: 3px;
background: var(--primary);
border-radius: 2px;
}
.scroll-area {
flex: 1;
overflow-y: auto;
padding: 12px 16px 90px;
}
.scroll-area::-webkit-scrollbar { display: none; }
/* 日期分组 */
.date-group {
margin-bottom: 14px;
}
.date-group__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
margin-bottom: 8px;
}
.date-group__title {
font-size: 13px;
font-weight: 700;
color: var(--text-primary);
}
.date-group__stat {
font-size: 12px;
color: var(--text-secondary);
}
.date-group__stat strong {
color: var(--primary);
}
.order-item {
background: white;
border-radius: 12px;
padding: 14px;
margin-bottom: 8px;
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: transform 0.1s;
}
.order-item:active { transform: scale(0.98); }
.order-item__head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.order-item__num {
font-size: 12px;
color: var(--text-tertiary);
}
.type-tag {
display: inline-block;
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
margin-left: 4px;
font-weight: 600;
}
.type-tag--home { background: #dbeafe; color: #1e40af; }
.type-tag--store { background: #fef3c7; color: #92400e; }
.status-tag {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 600;
}
.status-tag--completed { background: #d1fae5; color: #065f46; }
.status-tag--rejected { background: #f3f4f6; color: #6b7280; }
.status-tag--timeout { background: #f3f4f6; color: #9ca3af; border: 1px dashed var(--text-tertiary); }
.order-route {
display: flex;
gap: 10px;
align-items: center;
font-size: 12px;
color: var(--text-secondary);
padding: 6px 0;
border-top: 1px dashed var(--border);
border-bottom: 1px dashed var(--border);
margin-bottom: 8px;
}
.order-route__point {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.order-route__icon {
width: 18px; height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 10px;
flex-shrink: 0;
}
.order-route__icon--start { background: var(--primary); }
.order-route__icon--end { background: var(--success); }
.order-route__arrow {
color: var(--text-tertiary);
flex-shrink: 0;
}
.order-item__foot {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-time {
font-size: 11px;
color: var(--text-tertiary);
}
.order-income {
font-size: 15px;
font-weight: 700;
color: var(--primary);
}
.order-income--rejected {
color: var(--text-tertiary);
font-size: 12px;
}
.empty {
text-align: center;
padding: 60px 20px;
color: var(--text-tertiary);
font-size: 13px;
}
.empty__icon {
font-size: 48px;
margin-bottom: 8px;
opacity: 0.4;
}
/* 底部 Tab */
.tabbar {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 70px;
background: white;
border-top: 1px solid var(--border);
display: flex;
padding: 8px 0 20px;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
text-decoration: none;
color: var(--text-tertiary);
}
.tab.active { color: var(--primary); }
.tab__icon { font-size: 22px; }
.tab__label { font-size: 10px; font-weight: 600; }
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">9:41</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<!-- 顶部收益 -->
<div class="summary-bar">
<div class="summary-head">
<div class="summary-title">📚 历史订单</div>
<div class="summary-date" onclick="toast('选择日期范围')">📅 本月 </div>
</div>
<div class="summary-stats">
<div class="summary-item">
<div class="summary-num summary-num--lg">186</div>
<div class="summary-label">完成单数</div>
</div>
<div class="summary-item">
<div class="summary-num summary-num--lg">486km</div>
<div class="summary-label">总里程</div>
</div>
<div class="summary-item">
<div class="summary-num">98%</div>
<div class="summary-label">准时率</div>
</div>
<div class="summary-item">
<div class="summary-num">4.9</div>
<div class="summary-label">客户评分</div>
</div>
</div>
</div>
<!-- Tab -->
<div class="tab-bar">
<div class="tab-item active">全部</div>
<div class="tab-item">已完成</div>
<div class="tab-item">已拒单</div>
<div class="tab-item">超时</div>
</div>
<div class="scroll-area">
<!-- 今日 -->
<div class="date-group">
<div class="date-group__head">
<div class="date-group__title">📅 今天 (8月12日)</div>
<div class="date-group__stat">完成 <strong>8</strong> 单 · 总里程 <strong>20.6km</strong></div>
</div>
<div class="order-item" onclick="toast('打开订单详情')">
<div class="order-item__head">
<div>
<span class="order-item__num">#DP20260812-0832</span>
<span class="type-tag type-tag--home">到家</span>
</div>
<span class="status-tag status-tag--completed">✓ 已送达</span>
</div>
<div class="order-route">
<div class="order-route__point">
<span class="order-route__icon order-route__icon--start"></span>
<span>中央厨房·高新站</span>
</div>
<span class="order-route__arrow"></span>
<div class="order-route__point">
<span class="order-route__icon order-route__icon--end"></span>
<span>万科城 5 号楼</span>
</div>
</div>
<div class="order-item__foot">
<span class="order-time">15:08 送达 · 提前 23 分钟</span>
<span class="order-income">2.3km · 20min</span>
</div>
</div>
<div class="order-item" onclick="toast('打开订单详情')">
<div class="order-item__head">
<div>
<span class="order-item__num">#DP20260812-0728</span>
<span class="type-tag type-tag--store">到店</span>
</div>
<span class="status-tag status-tag--completed">✓ 已送达</span>
</div>
<div class="order-route">
<div class="order-route__point">
<span class="order-route__icon order-route__icon--start"></span>
<span>净菜加工中心</span>
</div>
<span class="order-route__arrow"></span>
<div class="order-route__point">
<span class="order-route__icon order-route__icon--end"></span>
<span>家兴源食堂</span>
</div>
</div>
<div class="order-item__foot">
<span class="order-time">13:42 送达 · 准时送达</span>
<span class="order-income">5.6km · 38min</span>
</div>
</div>
<div class="order-item" onclick="toast('打开订单详情')">
<div class="order-item__head">
<div>
<span class="order-item__num">#DP20260812-0905</span>
<span class="type-tag type-tag--store">到店</span>
</div>
<span class="status-tag status-tag--completed">✓ 已送达</span>
</div>
<div class="order-route">
<div class="order-route__point">
<span class="order-route__icon order-route__icon--start"></span>
<span>净菜加工·长安</span>
</div>
<span class="order-route__arrow"></span>
<div class="order-route__point">
<span class="order-route__icon order-route__icon--end"></span>
<span>家兴源·科技路</span>
</div>
</div>
<div class="order-item__foot">
<span class="order-time">12:18 送达 · 准时</span>
<span class="order-income">4.8km · 30min</span>
</div>
</div>
</div>
<!-- 昨日 -->
<div class="date-group">
<div class="date-group__head">
<div class="date-group__title">📅 昨天 (8月11日)</div>
<div class="date-group__stat">完成 <strong>11</strong> 单 · 总里程 <strong>32.4km</strong></div>
</div>
<div class="order-item" onclick="toast('打开订单详情')">
<div class="order-item__head">
<div>
<span class="order-item__num">#DP20260811-1922</span>
<span class="type-tag type-tag--home">到家</span>
</div>
<span class="status-tag status-tag--completed">✓ 已送达</span>
</div>
<div class="order-route">
<div class="order-route__point">
<span class="order-route__icon order-route__icon--start"></span>
<span>味之都·高新店</span>
</div>
<span class="order-route__arrow"></span>
<div class="order-route__point">
<span class="order-route__icon order-route__icon--end"></span>
<span>紫薇田园都市</span>
</div>
</div>
<div class="order-item__foot">
<span class="order-time">19:48 送达 · 准时送达</span>
<span class="order-income">3.5km · 25min</span>
</div>
</div>
<div class="order-item" onclick="toast('打开订单详情')">
<div class="order-item__head">
<div>
<span class="order-item__num">#DP20260811-1438</span>
<span class="type-tag type-tag--home">到家</span>
</div>
<span class="status-tag status-tag--rejected">已拒单</span>
</div>
<div class="order-route">
<div class="order-route__point">
<span class="order-route__icon order-route__icon--start"></span>
<span>味之都·高新店</span>
</div>
<span class="order-route__arrow"></span>
<div class="order-route__point">
<span class="order-route__icon order-route__icon--end"></span>
<span>枫林绿洲 6 号楼</span>
</div>
</div>
<div class="order-item__foot">
<span class="order-time">14:38 拒单 · 理由:车辆故障</span>
<span class="order-income order-income--rejected">未配送</span>
</div>
</div>
<div class="order-item" onclick="toast('打开订单详情')">
<div class="order-item__head">
<div>
<span class="order-item__num">#DP20260811-1126</span>
<span class="type-tag type-tag--store">到店</span>
</div>
<span class="status-tag status-tag--timeout">超时未响应</span>
</div>
<div class="order-route">
<div class="order-route__point">
<span class="order-route__icon order-route__icon--start"></span>
<span>食材配送中心</span>
</div>
<span class="order-route__arrow"></span>
<div class="order-route__point">
<span class="order-route__icon order-route__icon--end"></span>
<span>味之滨餐厅</span>
</div>
</div>
<div class="order-item__foot">
<span class="order-time">11:26 派单 · 超时 30s 未响应</span>
<span class="order-income order-income--rejected">已转派</span>
</div>
</div>
</div>
<!-- 前天 -->
<div class="date-group">
<div class="date-group__head">
<div class="date-group__title">📅 8月10日</div>
<div class="date-group__stat">完成 <strong>9</strong> 单 · 总里程 <strong>28.2km</strong></div>
</div>
<div class="order-item" onclick="toast('打开订单详情')">
<div class="order-item__head">
<div>
<span class="order-item__num">#DP20260810-1812</span>
<span class="type-tag type-tag--home">到家</span>
</div>
<span class="status-tag status-tag--completed">✓ 已送达</span>
</div>
<div class="order-route">
<div class="order-route__point">
<span class="order-route__icon order-route__icon--start"></span>
<span>中央厨房·高新站</span>
</div>
<span class="order-route__arrow"></span>
<div class="order-route__point">
<span class="order-route__icon order-route__icon--end"></span>
<span>锦业一号楼</span>
</div>
</div>
<div class="order-item__foot">
<span class="order-time">18:48 送达 · 准时</span>
<span class="order-income">0.8km · 8min</span>
</div>
</div>
</div>
<div style="text-align:center;padding:16px;color:var(--text-tertiary);font-size:12px">
· 上拉加载更多历史 ·
</div>
</div>
<!-- 底部 Tab -->
<div class="tabbar">
<a href="01-home.html" class="tab">
<span class="tab__icon">🏠</span>
<span class="tab__label">首页</span>
</a>
<a href="03-grab-pool.html" class="tab">
<span class="tab__icon">🔥</span>
<span class="tab__label">抢单</span>
</a>
<a href="07-history.html" class="tab active">
<span class="tab__icon">📚</span>
<span class="tab__label">历史</span>
</a>
<a href="08-profile.html" class="tab">
<span class="tab__icon">👤</span>
<span class="tab__label">我的</span>
</a>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
document.querySelectorAll('.tab-item').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('.tab-item').forEach(i => i.classList.remove('active'));
t.classList.add('active');
});
});
</script>
</body>
</html>
@@ -0,0 +1,539 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>个人中心 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--white: #ffffff;
--success: #10b981;
--info: #3b82f6;
--warn: #f59e0b;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
--shadow-md: 0 10px 28px rgba(15, 23, 42, 0.10);
}
body {
min-height: 100vh;
padding: 24px 0 40px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
.status-bar {
height: 44px;
padding: 0 24px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 12px;
font-weight: 600;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
}
.status-bar__time { font-size: 15px; font-weight: 700; }
.status-bar__icons { display: flex; align-items: center; gap: 6px; }
.status-bar__signal { width:18px;height:10px;background:#fff;clip-path: polygon(0 100%, 18% 64%, 18% 100%, 36% 42%, 36% 100%, 54% 24%, 54% 100%, 72% 8%, 72% 100%, 100% 0, 100% 100%); }
.status-bar__wifi { width:16px;height:12px;background:#fff;clip-path: polygon(0 50%, 50% 0, 100% 50%, 86% 60%, 50% 22%, 14% 60%); }
.status-bar__battery { width:24px;height:12px;border:1.5px solid #fff;border-radius:3px;position:relative;padding:1.5px;}
.status-bar__battery::after { content:'';display:block;width:70%;height:100%;background:#fff;border-radius:1px;}
.status-bar__battery::before { content:'';position:absolute;right:-3px;top:3px;width:2px;height:6px;background:#fff;border-radius:0 1px 1px 0;}
.scroll-area {
flex: 1;
overflow-y: auto;
padding-bottom: 90px;
}
.scroll-area::-webkit-scrollbar { display: none; }
/* 用户信息卡 */
.profile-card {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
padding: 12px 20px 30px;
position: relative;
}
.profile-top {
display: flex;
align-items: center;
gap: 14px;
padding: 4px 0 18px;
}
.profile-avatar {
width: 64px; height: 64px;
background: rgba(255, 255, 255, 0.25);
border: 3px solid rgba(255, 255, 255, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
flex-shrink: 0;
}
.profile-info { flex: 1; }
.profile-name {
font-size: 19px;
font-weight: 700;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.profile-level {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: #78350f;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 700;
}
.profile-meta {
font-size: 12px;
opacity: 0.9;
line-height: 1.5;
}
.profile-meta-item {
display: inline-block;
margin-right: 10px;
}
.setting-entry {
width: 36px; height: 36px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
text-decoration: none;
color: white;
cursor: pointer;
}
/* 收益统计 */
.income-card {
background: white;
margin: -22px 16px 0;
padding: 16px;
border-radius: 14px;
box-shadow: var(--shadow-md);
position: relative;
z-index: 2;
}
.income-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.income-title {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
}
.income-detail {
font-size: 12px;
color: var(--primary);
text-decoration: none;
font-weight: 600;
}
.income-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
text-align: center;
}
.income-item__num {
font-size: 20px;
font-weight: 700;
color: var(--primary);
margin-bottom: 2px;
}
.income-item__label {
font-size: 11px;
color: var(--text-secondary);
}
/* 接单状态 */
.work-status {
background: white;
margin: 12px 16px 0;
border-radius: 14px;
padding: 14px;
box-shadow: var(--shadow-sm);
}
.work-status__head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.work-status__title {
font-size: 14px;
font-weight: 700;
}
.online-tag {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: #d1fae5;
color: #065f46;
font-weight: 600;
}
.work-status__row {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
}
.work-status__label { color: var(--text-secondary); }
.work-status__value { font-weight: 600; }
/* 设置组 */
.menu-group {
background: white;
margin: 12px 16px 0;
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.menu-group__title {
font-size: 13px;
font-weight: 700;
padding: 14px 16px 8px;
color: var(--text-primary);
}
.menu-item {
display: flex;
align-items: center;
padding: 14px 16px;
cursor: pointer;
text-decoration: none;
color: inherit;
border-bottom: 1px solid var(--background);
transition: background 0.2s;
}
.menu-item:last-child { border-bottom: none; }
.menu-item:active { background: var(--background); }
.menu-icon {
width: 32px; height: 32px;
border-radius: 8px;
background: var(--primary-light);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
margin-right: 12px;
flex-shrink: 0;
}
.menu-icon--blue { background: #dbeafe; }
.menu-icon--green { background: #d1fae5; }
.menu-icon--yellow { background: #fef3c7; }
.menu-icon--purple { background: #f3e8ff; }
.menu-icon--gray { background: #f3f4f6; }
.menu-content {
flex: 1;
}
.menu-label {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.menu-desc {
font-size: 11px;
color: var(--text-secondary);
}
.menu-right {
display: flex;
align-items: center;
gap: 6px;
}
.menu-value {
font-size: 12px;
color: var(--text-secondary);
}
.menu-arrow {
color: var(--text-tertiary);
font-size: 18px;
}
.menu-badge {
background: var(--primary);
color: white;
padding: 1px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
}
/* Switch */
.switch {
width: 44px; height: 24px;
background: #d1d5db;
border-radius: 12px;
position: relative;
cursor: pointer;
transition: background 0.3s;
}
.switch.on { background: var(--primary); }
.switch::after {
content: '';
position: absolute;
width: 20px; height: 20px;
background: white;
border-radius: 50%;
top: 2px; left: 2px;
transition: left 0.3s;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.switch.on::after { left: 22px; }
/* 底部 Tab */
.tabbar {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 70px;
background: white;
border-top: 1px solid var(--border);
display: flex;
padding: 8px 0 20px;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
text-decoration: none;
color: var(--text-tertiary);
}
.tab.active { color: var(--primary); }
.tab__icon { font-size: 22px; }
.tab__label { font-size: 10px; font-weight: 600; }
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<div class="status-bar">
<span class="status-bar__time">9:41</span>
<div class="status-bar__icons">
<span class="status-bar__signal"></span>
<span class="status-bar__wifi"></span>
<span class="status-bar__battery"></span>
</div>
</div>
<div class="scroll-area">
<!-- 用户信息 -->
<div class="profile-card">
<div class="profile-top">
<div class="profile-avatar">🧑‍🦱</div>
<div class="profile-info">
<div class="profile-name">
陈师傅
</div>
<div class="profile-meta">
<span class="profile-meta-item">工号:RDR-2026</span>
<span class="profile-meta-item">📱 138****6688</span><br>
<span class="profile-meta-item">📍 高新站 · 接单评分 4.9</span>
</div>
</div>
<a href="javascript:void(0)" class="setting-entry" onclick="toast('打开设置')">⚙️</a>
</div>
</div>
<!-- 工作量统计 -->
<div class="income-card">
<div class="income-head">
<div class="income-title">📊 工作统计</div>
<a href="07-history.html" class="income-detail" onclick="toast('查看配送明细')">查看明细 </a>
</div>
<div class="income-grid">
<div>
<div class="income-item__num">8</div>
<div class="income-item__label">今日单数</div>
</div>
<div>
<div class="income-item__num">186</div>
<div class="income-item__label">本月单数</div>
</div>
<div>
<div class="income-item__num">486km</div>
<div class="income-item__label">本月里程</div>
</div>
</div>
</div>
<!-- 工作状态 -->
<div class="work-status">
<div class="work-status__head">
<div class="work-status__title">🛵 接单状态</div>
<span class="online-tag">● 在线中</span>
</div>
<div class="work-status__row">
<span class="work-status__label">今日在线时长</span>
<span class="work-status__value">3h 24min</span>
</div>
<div class="work-status__row">
<span class="work-status__label">本周接单数</span>
<span class="work-status__value">56 单 · 准时率 98%</span>
</div>
<div class="work-status__row">
<span class="work-status__label">拒单次数(本周)</span>
<span class="work-status__value" style="color:#ef4444">2 次</span>
</div>
</div>
<!-- 接单设置 -->
<div class="menu-group">
<div class="menu-group__title">接单设置</div>
<a href="javascript:void(0)" class="menu-item" onclick="toast('打开派单偏好')">
<div class="menu-icon">📌</div>
<div class="menu-content">
<div class="menu-label">派单偏好</div>
<div class="menu-desc">自动接单距离 / 偏好订单类型</div>
</div>
<div class="menu-right">
<span class="menu-value">3km 内</span>
<span class="menu-arrow"></span>
</div>
</a>
<div class="menu-item">
<div class="menu-icon menu-icon--yellow">🔔</div>
<div class="menu-content">
<div class="menu-label">派单声音提醒</div>
<div class="menu-desc">派单到达时响铃 + 震动</div>
</div>
<div class="menu-right">
<div class="switch on" onclick="toggleSwitch(this)"></div>
</div>
</div>
</div>
<!-- 其他 -->
<div class="menu-group">
<div class="menu-group__title">其他</div>
<a href="javascript:void(0)" class="menu-item" onclick="toast('打开车辆信息')">
<div class="menu-icon">🛵</div>
<div class="menu-content">
<div class="menu-label">我的车辆</div>
<div class="menu-desc">陕A·9F8K2 · 电动车 · 续航 80km</div>
</div>
<div class="menu-right">
<span class="menu-arrow"></span>
</div>
</a>
<a href="javascript:void(0)" class="menu-item" onclick="toast('打开帮助中心')">
<div class="menu-icon menu-icon--gray"></div>
<div class="menu-content">
<div class="menu-label">帮助中心</div>
<div class="menu-desc">常见问题 · 配送指南</div>
</div>
<div class="menu-right">
<span class="menu-arrow"></span>
</div>
</a>
<a href="00-login.html" class="menu-item">
<div class="menu-icon menu-icon--gray">🚪</div>
<div class="menu-content">
<div class="menu-label">退出登录</div>
<div class="menu-desc">切换账号或下线</div>
</div>
<div class="menu-right">
<span class="menu-arrow"></span>
</div>
</a>
</div>
<div style="text-align:center;padding:20px;color:var(--text-tertiary);font-size:11px">
营养管理餐品配送端 v1.0.0<br>
© 2026 寰汐营养管理
</div>
</div>
<!-- 底部 Tab -->
<div class="tabbar">
<a href="01-home.html" class="tab">
<span class="tab__icon">🏠</span>
<span class="tab__label">首页</span>
</a>
<a href="03-grab-pool.html" class="tab">
<span class="tab__icon">🔥</span>
<span class="tab__label">抢单</span>
</a>
<a href="07-history.html" class="tab">
<span class="tab__icon">📚</span>
<span class="tab__label">历史</span>
</a>
<a href="08-profile.html" class="tab active">
<span class="tab__icon">👤</span>
<span class="tab__label">我的</span>
</a>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
function toggleSwitch(el) {
el.classList.toggle('on');
}
</script>
</body>
</html>
@@ -0,0 +1,141 @@
# 营养管理餐品配送 H5 模块变更日志
## 2026-08-12 · 工作台与个人中心简化(第四版)
### 优化
- **`01-home.html` 取货中订单按钮简化**:移除"扫码取货"按钮(溯源码扫描暂不进入业务路径),保留两个核心操作:
- 「📞 联系客户」(ghost 样式,呼叫商家/客户)
- 「到店报到」(primary 样式,通知商家准备)
- **`08-profile.html` 接单设置精简**:由 5 项裁剪为 2 项,仅保留:
- 「派单偏好」(自动接单距离 / 偏好订单类型)
- 「派单声音提醒」(响铃 + 震动开关)
- 移除项:服务范围 / 配送类型 / 夜间免打扰
- **`08-profile.html` 车辆与装备栏目整体去除**:移除「我的车辆 / 保温箱装备 / 工作服装备」整组菜单
- **`08-profile.html` 其他栏目重组**:由 4 项裁剪为 3 项:
- 「我的车辆」(从原"车辆与装备"栏目迁入,陕A·9F8K2 · 电动车 · 续航 80km)
- 「帮助中心」(常见问题 · 配送指南)
- 「退出登录」(切换账号或下线)
- 移除项:配送员培训 / 联系客服
### 业务约定更新
- 取货环节不再要求扫码溯源,改由商家在到店报到环节确认餐品交接
- 个人中心聚焦核心配送设置,装备类信息后续如有需要再单独建模
- 客服联系与培训入口暂不上线,引导至帮助中心自助解决
---
## 2026-08-12 · 配送员称谓统一与等级标签清理(第三版)
### 优化
- **称谓统一**:由于后续可能采用机动车/汽车配送而非仅电动自行车,所有页面"骑手"统一改为"配送员",去除等级化标签
- `00-login.html`:页面标题 "骑手登录" → "配送员登录";副标题 "骑手端" → "配送员端";协议名 "《骑手服务协议》" → "《配送员服务协议》"
- `01-home.html`:删除 `rider-meta` 中的 `金牌骑手` 等级徽标,改为只显示在线时长;CSS 注释 "顶部骑手信息" → "顶部配送员信息"
- `08-profile.html`:删除 `profile-level` 中的 `🏅 金牌骑手` 等级标签;菜单项 "骑手培训" → "配送员培训";页脚 "营养餐品配送骑手端" → "营养管理餐品配送端"
-`index.html` SITE_MAP:"骑手登录" → "配送员登录"
- CHANGELOG 中所有 "骑手" 措辞同步改为 "配送员"
### 业务约定更新
- 配送员可能使用电动自行车、机动车、汽车等多种配送工具,UI 不预设具体配送方式
- 不再使用 "金牌骑手/银牌骑手" 等级化标签,避免等级体系与实际运营模式不符
---
## 2026-08-12 · 模块初始化与运营模式调整(第二版)
### 优化
- **模块名称**:由"营养餐品配送"统一改为"营养管理餐品配送",对齐营养管理业务域
- **入口直连**:删除二级导航页 `index.html`,从根导航卡片点击直接进入小程序主页 `01-home.html`,其余 8 个页面在小程序内串联跳转
- **配送模式调整**:本模块为自营配送,配送员为内部员工,暂不使用计件工资,所有页面去除收益/余额/提现相关元素:
- `01-home.html`:今日数据卡 "今日收益" 改为 "在线时长";快捷功能 "立即提现" 改为 "工作统计"
- `02-dispatch-incoming.html`:订单摘要 "预估收益" 改为 "配送距离"
- `03-grab-pool.html`:5 张订单 "预估收益" 全部改为 "配送距离";筛选标签 "高收益" 改为 "长距离";订单标记 "💰 高" 改为 "📦 大"
- `04-order-detail.html`:餐品清单移除金额列;订单信息卡移除 "餐品总额 / 配送费 / 配送员收益" 三行,补充 "餐品份数 / 配送距离 / 预计时长 / 收货方式" 工作量字段
- `05-delivering.html`:概览卡 "配送收益 ¥15.8" 改为 "配送距离 2.6km";底部按钮文案改为 "已到达 · 验证码签收"
- `07-history.html`:顶部汇总 "本月收益" 改为 "总里程";三个日期分组头部 "收益 ¥xxx" 改为 "总里程 xxxkm";每单右侧 "+¥xx" 改为 "x.xkm · xmin" 工作量标签;拒单/超时单 "无收益" 改为 "未配送"
- `08-profile.html`:钱包卡 "💰 我的钱包" 改为 "📊 工作统计";三项指标由 "今日收益 / 本月收益 / 可提现" 改为 "今日单数 / 本月单数 / 本月里程"
### 重构
- **送达签收页 `06-finish-sign.html` 重构为验证码收货**:
- 移除手写签名区与 SVG 签名组件
- 新增 **6 位验证码输入框**:六宫格独立输入,支持自动跳格 / 退格回跳 / 粘贴整段填充
- 验证码由系统在下单时发送至客户手机,配送员到达后向客户索取,可点击 "发送验证码" 重新获取或 "联系客户" 拨打电话索取
- **送达按钮锁定**:6 位验证码全部填写前,"确认送达" 按钮置灰禁用,全部填写后按钮文案变为 "✓ 确认送达"
- 验证码 5 分钟内有效,失效需重新发送
- 收益结算卡移除,改为 "本次工作量" 列表:餐品数量 / 配送距离 / 配送时长 / 送达时间 + 本次完成 1 单
- 成功弹窗去除金额,改为工作量统计(餐品 / 距离 / 时长)+ 准时提示
### 业务约定更新
- **配送模式**:自营配送,内部员工,不涉及计件工资与收益结算
- **工作量度量**:仅记录 单数 / 里程 / 时长 / 准时率 / 客户评分
- **收货流程**:客户手机接收 6 位验证码 → 配送员到达后索取 → 输入验证码 → 系统校验通过 → 确认送达
- **拒单仍保留五选项**:车辆故障/个人原因/距离过远/已接其他单/其他(影响派单评分,不影响工资)
---
## 2026-08-12 · 模块初始化(首版)
### 新增
### 新增
- 模块导航页 `index.html`:9 个页面入口 + 状态色编码图例
- 配送员登录页 `00-login.html`:手机号 + 验证码 + 微信一键登录 + 协议勾选 + 60s 倒计时
- 工作台首页 `01-home.html`:
- 在线状态开关(可切换上线/下线)
- 今日数据卡片(完成/收益/进行中/里程)
- **派单提醒横幅**(脉冲动画 + 响铃图标,点击进入派单接听)
- 进行中订单卡片(配送中 + 取货中两种状态)
- 快捷功能入口(抢单大厅/历史/提现/设置)
- **派单接听核心页 `02-dispatch-incoming.html`**(派单流程主线):
- 全屏红色脉冲背景 + 响铃动效(三轮波纹)
- **30s 倒计时圆环**(SVG 实现,超时自动拒接)
- 订单摘要卡片(取货/配送路线 + 餐品/收益/时限)
- 接受/拒绝双按钮
- 拒单理由弹窗(车辆故障/个人原因/距离过远/已接其他单/其他)
- 抢单大厅 `03-grab-pool.html`(辅线):
- 5 条公共订单池(到家 + 到店两种类型)
- 筛选 pill(全部/到家/到店/附近/高收益)
- 一键抢单按钮 + 倒计时标签
- 急单/高收益标记
- 订单详情 `04-order-detail.html`:
- 4 步进度条(接单/取货/配送/送达)
- 完整路线(取货点 + 配送点 + 联系按钮)
- 餐品清单(含溯源码校验提示)
- 订单信息(派单方式/时间/收益)
- 客户备注卡片
- 配送中 `05-delivering.html`:
- 模拟地图区(网格 + 道路 + 三点标记 + 路线虚线)
- 浮窗信息(距离/ETA)
- 4 步配送进度(已完成 2 步 + 进行中 + 待完成)
- 客户联系(电话 + 短信)
- 异常上报按钮
- 送达签收 `06-finish-sign.html`:
- 拍照留证(3 卡槽,已拍照占位)
- 客户签名(SVG 路径模拟,支持清除 + 改用验证码)
- 5 星评价 + 多选标签 + 文本备注
- 收益结算明细(基础费 + 距离补贴 + 准时奖励)
- 完成成功弹窗(收益 + 提前送达提示)
- 历史订单 `07-history.html`:
- 顶部本月汇总(单数/收益/准时率/评分)
- 4 个 Tab(全部/已完成/已拒单/超时)
- 按日期分组展示(今日/昨日/前天)
- 订单卡片含路线缩略图 + 状态徽标 + 收益
- 个人中心 `08-profile.html`:
- 用户信息卡(头像/姓名/金牌骑手等级/工号/电话)
- 钱包卡片(今日/本月/可提现)
- 接单状态(在线时长/接单数/拒单数)
- 接单设置(派单偏好/服务范围/配送类型/声音/夜间免打扰)
- 车辆装备(车辆/保温箱/工作服)
- 其他(培训/客服/帮助/退出)
### 设计令牌
- 主色:`#ff6d3a`(配送橙红,区别于小程序绿色)
- 状态色:已派单(橙)/已接受(蓝)/取货中(青)/配送中(紫)/已送达(绿)/已拒单(灰)/超时(虚灰)
- 画布:375 × 812 iPhone 手机外壳(对齐 mini-program 设计)
### 业务约定
- **派单流程为主线**:调度中心主动派单 → 骑手 30s 内响铃接听 → 接受/拒绝(拒绝需填理由) → 进入配送流程
- **抢单流程为辅线**:用于派单被拒、临时增补运力场景,骑手在大厅主动挑选订单
- **配送场景区分**:到家(C 端,蓝色标签) / 到店(B 端,黄色标签)
- **状态徽标七态**:已派单/已接受/取货中/配送中/已送达/已拒单/超时未响应,跨页配色一致
- **拒单理由五选项**:车辆故障/个人原因/距离过远/已接其他单/其他,数据沉淀用于评分
+540
View File
@@ -0,0 +1,540 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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);}
.sidebar-item .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;filter:none;}
.sidebar-subgroup__header{display:flex;align-items:center;gap:6px;padding:10px 24px;font-size:var(--font-sm);color:rgba(255,255,255,0.45);cursor:pointer;user-select:none;}
.sidebar-subgroup__header .arrow{width:10px;height:10px;stroke:currentColor;transition:transform .2s;}
.sidebar-subgroup.open .sidebar-subgroup__header .arrow{transform:rotate(90deg);}
.sidebar-subgroup__body{display:none;padding-left:12px;}
.sidebar-subgroup.open .sidebar-subgroup__body{display:block;}
.main{margin-left:240px;margin-top:56px;padding:24px;min-height:calc(100vh - 56px);}
.breadcrumb{display:flex;align-items:center;gap:6px;font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:16px;}
.breadcrumb a{color:var(--primary);text-decoration:none;}.breadcrumb a:hover{text-decoration:underline;}
.breadcrumb__sep{color:var(--text-placeholder);}
/* 场所信息卡片 */
.info-card{background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-sm);padding:20px 24px;margin-bottom:20px;}
.info-card__title{font-size:var(--font-base);font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:8px;}
.info-card__title .icon{font-size:20px;}
.info-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px 24px;}
.info-item{display:flex;flex-direction:column;gap:4px;}
.info-item__label{font-size:var(--font-xs);color:var(--text-secondary);}
.info-item__value{font-size:var(--font-sm);color:var(--text-primary);font-weight:500;}
/* 工具栏 */
.toolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}
.toolbar__title{font-size:var(--font-base);font-weight:600;display:flex;align-items:center;gap:8px;}
.toolbar__title .dot{width:6px;height:6px;border-radius:50%;background:var(--primary);}
.toolbar__hint{font-size:var(--font-xs);color:var(--text-placeholder);}
/* 按钮 */
.btn{height:32px;padding:0 14px;border:none;border-radius:var(--radius-sm);font-size:var(--font-sm);cursor:pointer;display:inline-flex;align-items:center;gap:4px;white-space:nowrap;}
.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);}.btn-default:hover{border-color:var(--primary);color:var(--primary);}
.btn-link{background:transparent;color:var(--primary);padding:6px 12px;height:auto;border:1px dashed var(--primary);border-radius:var(--radius-sm);font-size:var(--font-sm);cursor:pointer;display:inline-flex;align-items:center;gap:4px;}
.btn-link:hover{background:var(--primary-light);}
/* 班次卡片 */
.shift-list{display:flex;flex-direction:column;gap:16px;}
.shift-card{background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-sm);overflow:hidden;}
.shift-card__header{display:flex;align-items:center;gap:16px;padding:16px 20px;background:var(--primary-light);border-left:4px solid var(--primary);}
.shift-card__name{font-size:var(--font-base);font-weight:600;color:var(--text-primary);min-width:90px;}
.shift-card__meta{display:flex;align-items:center;gap:16px;flex:1;font-size:var(--font-sm);color:var(--text-secondary);}
.shift-card__time{color:var(--primary);font-weight:500;}
.shift-card__weekdays{display:flex;gap:2px;}
.biz-wd{display:inline-block;width:22px;height:20px;border-radius:3px;text-align:center;line-height:20px;font-size:10px;background:#E8E8E8;color:var(--text-placeholder);}
.biz-wd.active{background:var(--primary);color:#fff;}
.shift-card__count{font-size:var(--font-xs);color:var(--text-placeholder);margin-left:auto;margin-right:12px;}
.shift-card__actions{display:flex;gap:8px;}
.link{color:var(--primary);cursor:pointer;text-decoration:none;font-size:var(--font-sm);}
.link:hover{text-decoration:underline;}
.link-danger{color:var(--danger);}.link-danger:hover{color:#ff7875;}
.shift-card__body{padding:20px;}
/* 人员网格 */
.staff-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;}
.staff-chip{background:#FAFAFA;border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;display:flex;align-items:center;gap:10px;transition:all .15s;}
.staff-chip:hover{background:#fff;border-color:#91D5FF;box-shadow:0 2px 6px rgba(24,144,255,0.12);}
.staff-chip__avatar{width:36px;height:36px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:var(--font-sm);font-weight:600;flex-shrink:0;}
.staff-chip__info{flex:1;min-width:0;}
.staff-chip__name{font-size:var(--font-sm);font-weight:500;color:var(--text-primary);margin-bottom:2px;}
.staff-chip__meta{font-size:var(--font-xs);color:var(--text-secondary);}
.staff-chip__remove{width:20px;height:20px;border:none;background:transparent;color:var(--text-placeholder);cursor:pointer;font-size:14px;line-height:1;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.staff-chip__remove:hover{background:var(--danger);color:#fff;}
/* 空班次 */
.staff-empty{grid-column:1 / -1;text-align:center;padding:24px 0;color:var(--text-placeholder);font-size:var(--font-sm);}
.shift-card__add-row{margin-top:12px;padding-top:12px;border-top:1px dashed var(--border);}
/* 弹窗 */
.modal-mask{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:1000;align-items:center;justify-content:center;}
.modal-mask.show{display:flex;}
.modal{background:#fff;border-radius:var(--radius-md);width:520px;box-shadow:var(--shadow-md);}
.modal.modal-lg{width:640px;}
.modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);}
.modal__title{font-size:var(--font-base);font-weight:600;}
.modal__close{font-size:22px;cursor:pointer;color:var(--text-secondary);line-height:1;}.modal__close:hover{color:var(--text-primary);}
.modal__body{padding:24px;max-height:60vh;overflow-y:auto;}
.modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 24px;border-top:1px solid var(--border);}
.form-row{margin-bottom:16px;}
.form-row:last-child{margin-bottom:0;}
.form-label{display:block;font-size:var(--font-sm);color:var(--text-primary);margin-bottom:6px;}
.form-label em{color:var(--danger);font-style:normal;}
.form-input,.form-select{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);}
.form-input:focus,.form-select:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.1);}
.form-row-inline{display:flex;gap:12px;}
.form-row-inline .form-row{flex:1;margin-bottom:16px;}
.form-hint{font-size:var(--font-xs);color:var(--text-placeholder);margin-top:4px;}
/* 周几选择块 */
.weekday-picker{display:flex;gap:6px;flex-wrap:wrap;}
.weekday-chip{padding:4px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--font-xs);cursor:pointer;user-select:none;background:#fff;color:var(--text-secondary);}
.weekday-chip.active{background:var(--primary);color:#fff;border-color:var(--primary);}
/* 员工选择列表 */
.staff-pick{max-height:280px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px;}
.staff-pick-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:var(--radius-sm);cursor:pointer;font-size:var(--font-sm);}
.staff-pick-item:hover{background:#FAFAFA;}
.staff-pick-item input{cursor:pointer;}
.staff-pick-item.disabled{opacity:0.5;cursor:not-allowed;}
</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">健康生产</a>
<a href="din-canteen.html" class="active">营养用餐</a>
<a href="ano-dirty.html">异常事件</a>
</nav>
<div class="top-bar__right">
<a href="../../index.html" class="btn-back">← 返回首页</a>
<div class="avatar"></div>
<span>系统管理员 ▾</span>
</div>
</header>
<!-- 侧边栏 -->
<aside class="sidebar">
<div class="sidebar-menu__title">营养用餐</div>
<a href="din-canteen.html" class="sidebar-item active"><span class="mi">🏢</span>场所管理</a>
<a href="din-dict-mgmt.html" class="sidebar-item"><span class="mi">📖</span>字典管理</a>
<a href="prod-qc-nutrition.html" class="sidebar-item"><span class="mi">🔬</span>营养检验</a>
<a href="din-ingredient.html" class="sidebar-item"><span class="mi">🥦</span>食物成分</a>
<a href="din-dish-all.html" class="sidebar-item"><span class="mi">🍽️</span>餐品总库</a>
<a href="din-dish-own.html" class="sidebar-item"><span class="mi">📦</span>自有餐品库</a>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
📐 算法标准
</div>
<div class="sidebar-subgroup__body">
<a href="din-algo-population.html" class="sidebar-item"><span class="mi">👥</span>人群基础标准</a>
<a href="din-algo-vip.html" class="sidebar-item"><span class="mi">💎</span>VIP定制标准</a>
</div>
</div>
<a href="din-recipe-mgmt.html" class="sidebar-item"><span class="mi">📋</span>食谱管理</a>
<a href="din-order-meal.html" class="sidebar-item"><span class="mi">🍱</span>营养点餐</a>
<a href="din-user-mgmt.html" class="sidebar-item"><span class="mi">👤</span>用户管理</a>
<a href="din-order-line.html" class="sidebar-item"><span class="mi">🍜</span>餐线管理</a>
<a href="din-order-pickup.html" class="sidebar-item"><span class="mi">🎫</span>取餐管理</a>
<a href="din-data-consume.html" class="sidebar-item"><span class="mi">💳</span>消费管理</a>
<a href="din-data-nutri.html" class="sidebar-item"><span class="mi">📊</span>营养管理</a>
<a href="din-data-benefit.html" class="sidebar-item"><span class="mi">📈</span>餐材效益</a>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
📊 数据分析
</div>
<div class="sidebar-subgroup__body">
<a href="din-biz-analysis.html" class="sidebar-item"><span class="mi">📈</span>经营分析</a>
<a href="din-meal-analysis.html" class="sidebar-item"><span class="mi">📊</span>餐品分析</a>
</div>
</div>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
🖥️ 终端管理
</div>
<div class="sidebar-subgroup__body">
<a href="din-terminal.html" class="sidebar-item"><span class="mi">📟</span>设备管理</a>
<a href="din-content-center.html" class="sidebar-item"><span class="mi">📦</span>内容中心</a>
</div>
</div>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
🤖 模型管理
</div>
<div class="sidebar-subgroup__body">
<a href="din-model-llm.html" class="sidebar-item"><span class="mi">🧠</span>大模型管理</a>
<a href="din-model-small.html" class="sidebar-item"><span class="mi">⚙️</span>小模型管理</a>
</div>
</div>
<a href="din-innovation.html" class="sidebar-item"><span class="mi">🌟</span>味养创新</a>
</aside>
<!-- 主内容 -->
<main class="main">
<!-- 面包屑 -->
<div class="breadcrumb">
<a href="din-canteen.html">场所管理</a>
<span class="breadcrumb__sep">/</span>
<span>排版设置</span>
</div>
<!-- 场所基本信息 -->
<div class="info-card">
<div class="info-card__title"><span class="icon">🏢</span> <span id="canteenName">总部一号食堂</span></div>
<div class="info-grid">
<div class="info-item">
<div class="info-item__label">场所编号</div>
<div class="info-item__value" id="canteenCode">ST-2026-001</div>
</div>
<div class="info-item">
<div class="info-item__label">所属单位</div>
<div class="info-item__value" id="canteenUnit">CQ能源总部</div>
</div>
<div class="info-item">
<div class="info-item__label">负责人</div>
<div class="info-item__value" id="canteenLeader">张建国</div>
</div>
<div class="info-item">
<div class="info-item__label">负责人电话</div>
<div class="info-item__value" id="canteenPhone">138****1001</div>
</div>
</div>
</div>
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar__title"><span class="dot"></span>班次与人员排班</div>
<button class="btn btn-primary" onclick="openShiftModal('add')"> 新增班次</button>
</div>
<!-- 班次卡片列表 -->
<div class="shift-list" id="shiftList"></div>
</main>
<!-- 新增/编辑班次弹窗 -->
<div class="modal-mask" id="shiftModal" onclick="closeShiftModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal__header">
<span class="modal__title" id="shiftModalTitle">新增班次</span>
<span class="modal__close" onclick="closeShiftModal()">&times;</span>
</div>
<div class="modal__body">
<div class="form-row">
<label class="form-label"><em>*</em> 班次名称</label>
<input class="form-input" id="shiftName" placeholder="请输入班次名称,如:早班">
</div>
<div class="form-row-inline">
<div class="form-row">
<label class="form-label"><em>*</em> 上班时间</label>
<input class="form-input" id="shiftStart" type="time" value="06:00">
</div>
<div class="form-row">
<label class="form-label"><em>*</em> 下班时间</label>
<input class="form-input" id="shiftEnd" type="time" value="22:00">
</div>
</div>
<div class="form-row">
<label class="form-label"><em>*</em> 每周重复</label>
<div class="weekday-picker" id="weekdayPicker">
<span class="weekday-chip" data-day="1">周一</span>
<span class="weekday-chip" data-day="2">周二</span>
<span class="weekday-chip" data-day="3">周三</span>
<span class="weekday-chip" data-day="4">周四</span>
<span class="weekday-chip" data-day="5">周五</span>
<span class="weekday-chip" data-day="6">周六</span>
<span class="weekday-chip" data-day="7">周日</span>
</div>
<div class="form-hint">点击选择上班日,可多选</div>
</div>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeShiftModal()">取消</button>
<button class="btn btn-primary" onclick="confirmShift()">确定</button>
</div>
</div>
</div>
<!-- 添加人员弹窗 -->
<div class="modal-mask" id="staffModal" onclick="closeStaffModal(event)">
<div class="modal modal-lg" onclick="event.stopPropagation()">
<div class="modal__header">
<span class="modal__title">添加人员到「<span id="staffModalShiftName">全天班</span></span>
<span class="modal__close" onclick="closeStaffModal()">&times;</span>
</div>
<div class="modal__body">
<div class="form-row">
<label class="form-label">从员工库选择(所属单位:<span id="staffModalUnit">CQ能源总部</span></label>
<div class="staff-pick" id="staffPick">
<label class="staff-pick-item"><input type="checkbox" value="1001" data-name="张三" data-dept="餐饮部"> 张三 <span style="color:var(--text-secondary);">(工号:1001,餐饮部)</span></label>
<label class="staff-pick-item"><input type="checkbox" value="1002" data-name="李四" data-dept="餐饮部"> 李四 <span style="color:var(--text-secondary);">(工号:1002,餐饮部)</span></label>
<label class="staff-pick-item"><input type="checkbox" value="1003" data-name="王五" data-dept="餐饮部"> 王五 <span style="color:var(--text-secondary);">(工号:1003,餐饮部)</span></label>
<label class="staff-pick-item"><input type="checkbox" value="1004" data-name="赵六" data-dept="后勤部"> 赵六 <span style="color:var(--text-secondary);">(工号:1004,后勤部)</span></label>
<label class="staff-pick-item"><input type="checkbox" value="1005" data-name="钱七" data-dept="后勤部"> 钱七 <span style="color:var(--text-secondary);">(工号:1005,后勤部)</span></label>
<label class="staff-pick-item"><input type="checkbox" value="1006" data-name="孙八" data-dept="后勤部"> 孙八 <span style="color:var(--text-secondary);">(工号:1006,后勤部)</span></label>
</div>
<div class="form-hint">已在当前班次的人员将置灰,不可重复添加;同一员工可加入多个班次</div>
</div>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeStaffModal()">取消</button>
<button class="btn btn-primary" onclick="confirmStaff()">确定</button>
</div>
</div>
</div>
<script>
function toggleSubgroup(header){header.parentElement.classList.toggle('open');}
(function(){var active=document.querySelector('.sidebar-item.active');if(!active)return;var sub=active.closest('.sidebar-subgroup');if(sub)sub.classList.add('open');})();
/* ========== 数据初始化 ========== */
/* 场所信息映射 */
var canteenMap={
'1':{name:'总部一号食堂',code:'ST-2026-001',unit:'CQ能源总部',leader:'张建国',phone:'138****1001'},
'2':{name:'总部二号食堂',code:'ST-2026-002',unit:'CQ能源总部',leader:'李红梅',phone:'139****2002'},
'3':{name:'采油一厂职工食堂',code:'ST-2026-003',unit:'采油一厂',leader:'王志强',phone:'137****3003'},
'4':{name:'采油二厂综合食堂',code:'ST-2026-004',unit:'采油二厂',leader:'刘文静',phone:'136****4004'},
'5':{name:'采气一厂营养餐厅',code:'ST-2026-005',unit:'采气一厂',leader:'陈伟',phone:'135****5005'}
};
function getQueryParam(key){
var m=new RegExp('[?&]'+key+'=([^&]*)').exec(window.location.href);
return m?decodeURIComponent(m[1]):null;
}
(function initCanteenInfo(){
var id=getQueryParam('id')||'1';
var c=canteenMap[id]||canteenMap['1'];
document.getElementById('canteenName').textContent=c.name;
document.getElementById('canteenCode').textContent=c.code;
document.getElementById('canteenUnit').textContent=c.unit;
document.getElementById('canteenLeader').textContent=c.leader;
document.getElementById('canteenPhone').textContent=c.phone;
document.getElementById('staffModalUnit').textContent=c.unit;
document.title='营养管理 - '+c.name+' 排版设置 | 健康CQ原型';
})();
/* 班次数据(早班 + 晚班两条示例) */
var shifts=[
{id:'s1',name:'早班',start:'06:00',end:'14:00',weekdays:[1,1,1,1,1,0,0]},
{id:'s2',name:'晚班',start:'14:00',end:'22:00',weekdays:[1,1,1,1,1,1,1]}
];
/* 人员排班数据(分布于两个班次) */
var staffList=[
{id:'u1',name:'张建国',jobNo:'1001',dept:'餐饮部',shiftId:'s1'},
{id:'u2',name:'赵玲',jobNo:'1010',dept:'餐饮部',shiftId:'s1'},
{id:'u3',name:'孙强',jobNo:'1015',dept:'餐饮部',shiftId:'s2'},
{id:'u4',name:'周丽',jobNo:'1018',dept:'餐饮部',shiftId:'s2'}
];
var weekdayLabels=['一','二','三','四','五','六','日'];
/* ========== 渲染班次卡片 ========== */
function renderShifts(){
var list=document.getElementById('shiftList');
if(shifts.length===0){
list.innerHTML='<div style="background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,0.08);padding:48px 0;text-align:center;color:var(--text-placeholder);font-size:var(--font-sm);">暂无班次,请点击右上角「新增班次」</div>';
return;
}
list.innerHTML=shifts.map(function(s){
var members=staffList.filter(function(u){return u.shiftId===s.id;});
/* 周几方块 */
var wdHtml='';
s.weekdays.forEach(function(active,i){
wdHtml+='<span class="biz-wd'+(active?' active':'')+'">'+weekdayLabels[i]+'</span>';
});
/* 人员网格 */
var gridHtml='';
if(members.length===0){
gridHtml='<div class="staff-empty">暂无人员,点击下方按钮添加</div>';
}else{
gridHtml='<div class="staff-grid">'+
members.map(function(u){
var initial=u.name.charAt(0);
return '<div class="staff-chip">'+
'<div class="staff-chip__avatar">'+initial+'</div>'+
'<div class="staff-chip__info">'+
'<div class="staff-chip__name">'+u.name+'</div>'+
'<div class="staff-chip__meta">'+u.jobNo+' · '+u.dept+'</div>'+
'</div>'+
'<button class="staff-chip__remove" onclick="removeStaff(\''+u.id+'\')" title="移除">&times;</button>'+
'</div>';
}).join('')+
'</div>';
}
/* 卡片底部添加按钮 */
gridHtml+='<div class="shift-card__add-row">'+
'<button class="btn-link" onclick="openStaffModal(\''+s.id+'\')"> 添加人员到本班次</button>'+
'</div>';
return '<div class="shift-card">'+
'<div class="shift-card__header">'+
'<div class="shift-card__name">'+s.name+'</div>'+
'<div class="shift-card__meta">'+
'<span class="shift-card__time">'+s.start+' - '+s.end+'</span>'+
'<span class="shift-card__weekdays">'+wdHtml+'</span>'+
'</div>'+
'<span class="shift-card__count">'+members.length+' 人</span>'+
'<div class="shift-card__actions">'+
'<span class="link" onclick="openShiftModal(\'edit\',\''+s.id+'\')">编辑</span>'+
'<span class="link link-danger" onclick="deleteShift(\''+s.id+'\')">删除</span>'+
'</div>'+
'</div>'+
'<div class="shift-card__body">'+gridHtml+'</div>'+
'</div>';
}).join('');
}
/* ========== 班次弹窗 ========== */
var editingShiftId=null;
function openShiftModal(mode,id){
editingShiftId=mode==='edit'?id:null;
var title=document.getElementById('shiftModalTitle');
var nameInput=document.getElementById('shiftName');
var startInput=document.getElementById('shiftStart');
var endInput=document.getElementById('shiftEnd');
var chips=document.querySelectorAll('#weekdayPicker .weekday-chip');
if(mode==='edit'){
var s=shifts.find(function(x){return x.id===id;});
if(!s)return;
title.textContent='编辑班次';
nameInput.value=s.name;
startInput.value=s.start;
endInput.value=s.end;
chips.forEach(function(c){
var day=parseInt(c.dataset.day,10);
c.classList.toggle('active',s.weekdays[day-1]===1);
});
}else{
title.textContent='新增班次';
nameInput.value='';
startInput.value='06:00';
endInput.value='22:00';
chips.forEach(function(c){c.classList.remove('active');});
}
document.getElementById('shiftModal').classList.add('show');
}
function closeShiftModal(e){
if(e&&e.target!==e.currentTarget)return;
document.getElementById('shiftModal').classList.remove('show');
}
function confirmShift(){
var name=document.getElementById('shiftName').value.trim();
var start=document.getElementById('shiftStart').value;
var end=document.getElementById('shiftEnd').value;
var weekdays=[0,0,0,0,0,0,0];
document.querySelectorAll('#weekdayPicker .weekday-chip.active').forEach(function(c){
weekdays[parseInt(c.dataset.day,10)-1]=1;
});
if(!name){alert('请输入班次名称');return;}
if(!start||!end){alert('请填写上班时间和下班时间');return;}
if(weekdays.every(function(w){return w===0;})){alert('请至少选择一个上班日');return;}
if(editingShiftId){
var s=shifts.find(function(x){return x.id===editingShiftId;});
if(s){s.name=name;s.start=start;s.end=end;s.weekdays=weekdays;}
}else{
shifts.push({id:'s'+Date.now(),name:name,start:start,end:end,weekdays:weekdays});
}
closeShiftModal();
renderShifts();
}
function deleteShift(id){
var inUse=staffList.filter(function(u){return u.shiftId===id;});
if(inUse.length>0){
var names=inUse.map(function(u){return u.name;}).join('、');
alert('该班次下仍有人员:'+names+',请先移除人员后再删除。');
return;
}
if(!confirm('确认删除该班次?'))return;
shifts=shifts.filter(function(x){return x.id!==id;});
renderShifts();
}
/* ========== 人员弹窗 ========== */
var targetShiftId=null;
function openStaffModal(shiftId){
targetShiftId=shiftId;
var s=shifts.find(function(x){return x.id===shiftId;});
if(!s)return;
document.getElementById('staffModalShiftName').textContent=s.name;
/* 重置选择 + 置灰当前班次已有人员 */
document.querySelectorAll('#staffPick .staff-pick-item').forEach(function(item){
var cb=item.querySelector('input');
var jobNo=cb.value;
var exists=staffList.some(function(u){return u.jobNo===jobNo&&u.shiftId===shiftId;});
item.classList.toggle('disabled',exists);
cb.disabled=exists;
cb.checked=false;
});
document.getElementById('staffModal').classList.add('show');
}
function closeStaffModal(e){
if(e&&e.target!==e.currentTarget)return;
document.getElementById('staffModal').classList.remove('show');
}
function confirmStaff(){
var picked=document.querySelectorAll('#staffPick input:checked');
if(picked.length===0){alert('请至少选择一名员工');return;}
picked.forEach(function(cb){
if(cb.disabled)return;
staffList.push({
id:'u'+Date.now()+Math.random().toString(36).slice(2,6),
name:cb.dataset.name,
jobNo:cb.value,
dept:cb.dataset.dept,
shiftId:targetShiftId
});
});
closeStaffModal();
renderShifts();
}
function removeStaff(id){
var u=staffList.find(function(x){return x.id===id;});
if(!u)return;
if(!confirm('确认将「'+u.name+'」从本班次移除?'))return;
staffList=staffList.filter(function(x){return x.id!==id;});
renderShifts();
}
/* ========== 周几选择块交互 ========== */
document.getElementById('weekdayPicker').addEventListener('click',function(e){
if(e.target.classList.contains('weekday-chip')){
e.target.classList.toggle('active');
}
});
/* 初始渲染 */
renderShifts();
</script>
</body>
</html>
+5
View File
@@ -228,6 +228,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<td class="td-actions">
<span class="link" onclick="openDrawer(0)">详情</span>
<a class="link" href="din-canteen-line.html?id=1&name=总部一号食堂">餐线管理</a>
<a class="link" href="din-canteen-shift.html?id=1&name=总部一号食堂">排班设置</a>
<a class="link" href="din-canteen-add.html?id=1">编辑</a>
<span class="link link-danger">删除</span>
</td>
@@ -237,6 +238,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<td class="td-actions">
<span class="link" onclick="openDrawer(1)">详情</span>
<a class="link" href="din-canteen-line.html?id=2&name=总部二号食堂">餐线管理</a>
<a class="link" href="din-canteen-shift.html?id=2&name=总部二号食堂">排班设置</a>
<a class="link" href="din-canteen-add.html?id=2">编辑</a>
<span class="link link-danger">删除</span>
</td>
@@ -246,6 +248,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<td class="td-actions">
<span class="link" onclick="openDrawer(2)">详情</span>
<a class="link" href="din-canteen-line.html?id=3&name=采油一厂职工食堂">餐线管理</a>
<a class="link" href="din-canteen-shift.html?id=3&name=采油一厂职工食堂">排班设置</a>
<a class="link" href="din-canteen-add.html?id=3">编辑</a>
<span class="link link-danger">删除</span>
</td>
@@ -255,6 +258,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<td class="td-actions">
<span class="link" onclick="openDrawer(3)">详情</span>
<a class="link" href="din-canteen-line.html?id=4&name=采油二厂综合食堂">餐线管理</a>
<a class="link" href="din-canteen-shift.html?id=4&name=采油二厂综合食堂">排班设置</a>
<a class="link" href="din-canteen-add.html?id=4">编辑</a>
<span class="link link-danger">删除</span>
</td>
@@ -264,6 +268,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<td class="td-actions">
<span class="link" onclick="openDrawer(4)">详情</span>
<a class="link" href="din-canteen-line.html?id=5&name=采气一厂营养餐厅">餐线管理</a>
<a class="link" href="din-canteen-shift.html?id=5&name=采气一厂营养餐厅">排班设置</a>
<a class="link" href="din-canteen-add.html?id=5">编辑</a>
<span class="link link-danger">删除</span>
</td>
+42 -1
View File
@@ -453,7 +453,7 @@ function levelTag(level){
function renderDetail(){
const f = FOODS[currentFood];
const body = document.getElementById('detail-body');
const editLink = `<div style="text-align:right;margin-bottom:12px;"><a class="link" href="din-ingredient-add.html?mode=edit&code=${f.code}&tab=${currentTab}">编辑</a></div>`;
const editLink = `<div style="text-align:right;margin-bottom:12px;display:flex;justify-content:flex-end;gap:16px;"><a class="link" href="din-ingredient-add.html?mode=edit&code=${f.code}&tab=${currentTab}">编辑</a><a class="link" style="color:var(--danger);" onclick="openDeleteFoodDialog()">删除食材</a></div>`;
let content = '';
if(currentTab===0) content = renderBasicInfo(f);
else if(currentTab===1) content = renderNutri(f);
@@ -800,6 +800,24 @@ function deleteCat(id){
ingredientCats = ingredientCats.filter(function(c){return c.id!==id;});
renderCatTable();
}
/* ========== 删除食材确认弹窗 ========== */
// 打开删除食材确认弹窗,显示当前选中食材的名称和编码
function openDeleteFoodDialog(){
var f = FOODS[currentFood];
document.getElementById('deleteFoodName').textContent = f.name;
document.getElementById('deleteFoodCode').textContent = f.code;
document.getElementById('deleteFoodDialog').classList.add('open');
}
// 关闭删除食材确认弹窗
function closeDeleteFoodDialog(){
document.getElementById('deleteFoodDialog').classList.remove('open');
}
// 确认删除食材(原型演示:关闭弹窗后提示删除成功)
function confirmDeleteFood(){
closeDeleteFoodDialog();
alert('已删除该食材');
}
</script>
<!-- 食材分类管理弹窗 -->
<div class="dialog-mask" id="categoryDialog">
@@ -847,5 +865,28 @@ function deleteCat(id){
</div>
</div>
</div>
<!-- 删除食材确认弹窗 -->
<div class="dialog-mask" id="deleteFoodDialog">
<div class="dialog" style="width:420px;">
<div class="dialog__header">
<div class="dialog__title">删除食材</div>
<button class="dialog__close" onclick="closeDeleteFoodDialog()"></button>
</div>
<div class="dialog__body">
<div style="display:flex;align-items:flex-start;gap:12px;padding:8px 0;">
<div style="font-size:22px;color:var(--warning);line-height:1;">⚠️</div>
<div>
<div style="font-size:var(--font-sm);color:var(--text-primary);margin-bottom:6px;">确定要删除该食材吗?删除后不可恢复。</div>
<div style="font-size:var(--font-xs);color:var(--text-secondary);">食材名称:<span id="deleteFoodName" style="color:var(--text-primary);font-weight:500;"></span></div>
<div style="font-size:var(--font-xs);color:var(--text-secondary);margin-top:2px;">食材编码:<span id="deleteFoodCode" style="color:var(--text-primary);font-weight:500;"></span></div>
</div>
</div>
</div>
<div class="dialog__footer">
<button class="btn btn-default" onclick="closeDeleteFoodDialog()">取消</button>
<button class="btn btn-primary" style="background:var(--danger);" onclick="confirmDeleteFood()">确定删除</button>
</div>
</div>
</div>
</body>
</html>
+153
View File
@@ -1,5 +1,158 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-12] optimize: 净菜物流列简化 + 配送时间轴 + 收货码角色可见性
### 变更背景
上一版三页物流改造后,列表的物流信息列因为多行展示(徽标 + 配送人 + 状态)在窄列下出现换行,导致表格样式错乱。同时收货码作为客户敏感信息,不该在后端视角(净菜内供/外售)的详情中可见 — 收货码只属于买家(净菜订单聚合页)可见。
### 优化一:列表物流列简化为徽标 + 弹窗
**问题**:列表的物流信息列内含「配送方式徽标 + 配送人/承运方/运单号 + 配送状态徽标」,在窄列下多行换行,导致整行行高不齐。
**方案**:列表只保留配送方式徽标(蓝/紫/橙三色),点击徽标弹出物流详情小弹窗,弹窗内包含:
- 配送方式徽标
- 承运信息 grid(配送员/联系电话/工号 或 承运方/运单号/状态)
- 收货码(仅买家视角的 prod-clean-order 弹窗显示)
- **配送进度时间轴**:4 节点(发货 → 接单 → 取货 → 送达),横向展示,节点状态三态:
- `--done`(绿色实心圆):已完成
- `--current`(蓝色脉冲圆,带 `timelinePulse` 动画):进行中
- `--pending`(灰色虚线圆):未到达
**涉及三页**:`sup-clean-internal.html``sup-clean-external.html``prod-clean-order.html`
### 优化二:收货码角色可见性
| 角色 | 列表是否显示收货码列 | 详情是否显示收货码 | 物流弹窗是否显示收货码 |
|------|----------------------|---------------------|-------------------------|
| 后台(净菜内供) | 否 | 否 | 否 |
| 后台(净菜外售) | 否 | 否 | 否 |
| 买家(净菜订单聚合) | 否 | 是 | 是 |
- `sup-clean-internal.html` / `sup-clean-external.html`:`renderLogisticsDetail()` 移除收货码块
- `prod-clean-order.html`:列表移除「收货码」列(主表从 15 列减为 14 列,子表头/明细行 td 数同步对齐);`renderLogisticsDetail()` 保留收货码块;物流弹窗的 Section 3 单独展示收货码
### 数据模型新增字段
```js
logistics: {
...,
t1_ship:'2026-04-22 08:30', // 发货时间
t2_accept:'2026-04-22 08:32', // 接单时间(self_dispatch/self_grab 配送员接单时生成;三方物流发货即视为接单)
t3_pick:'2026-04-22 08:45', // 取货时间
t4_deliver:'2026-04-22 09:05' // 送达时间(配送完成时生成,null 表示尚未送达)
}
```
时间戳为 null 的节点即当前进行中节点,时间轴用此规则确定 `--current` 态位置。
### 涉及函数
- 新增 `renderLogisticsTimeline(logistics)`:渲染 4 节点横向时间轴
- 新增 `showLogisticsModal(orderNo)`:打开物流详情弹窗(三页同名同参,便于跨页复用)
- 重构 `renderLogistics(logistics,orderNo)`:从多行块简化为可点击徽标,新增 `orderNo` 参数用于弹窗定位
- `renderLogisticsDetail(logistics)`:sup-clean-internal/external 移除收货码块;prod-clean-order 保留收货码块 + 新增时间轴
- `doDeliver()` / `confirmShip()`:提交时初始化 `t1_ship` 为当前时间(third_party 同时设 `t2_accept`,因发货即视为承运方已接单)
### 视觉细节
- 时间轴节点圆 28×28,连接线宽 2px,通过 `position:absolute; top:14px; left:50%; width:100%` 跨节点
- `--current` 态圆使用 `timelinePulse` 关键帧动画,1.6s 循环,box-shadow 从 0 → 8px 蓝色光晕扩散
- 物流弹窗宽度 500px,3~4 个 section(配送方式/承运信息/[收货码]/配送进度)
---
## [2026-08-12] feat: 净菜内供/外售/订单物流改造 — 对接配送 H5(自营派单/抢单/三方物流)
### 变更背景
原净菜内供(`sup-clean-internal.html`)的「立即配送」按钮只录入配送人姓名,净菜外售(`sup-clean-external.html`)的「立即发货」按钮只录入物流公司+单号,二者均无法对接本次新做的「营养管理餐品配送」H5 模块。净菜订单(`prod-clean-order.html`)作为聚合视角,完全不展示物流信息,运维人员需要切回上游页面才能查看配送状态。
本次改造将三页的物流模型统一为共享的 `logistics` 对象,支持三种配送方式:
- **自营物流·派单**(`self_dispatch`):指定配送员,订单作为派单任务推送到配送员 H5(30s 内响铃接听)
- **自营物流·抢单**(`self_grab`):订单发布到抢单大厅,在线配送员自取
- **三方物流**(`third_party`):仅后台登记承运方/运单号,不进入 H5
自营物流模式下,系统自动生成 6 位收货码,客户口述给配送员,配送员在 H5 输入后才能完成签收。
### 列合并方案(避免重复列)
| 页面 | 原列 | 合并后 |
|------|------|--------|
| `sup-clean-internal.html` | 申领单状态 + 预定配送时间 + 配送人(3 列) | 订单状态 + 预定配送 + 物流信息(3 列) |
| `sup-clean-external.html` | 预定配送 + 物流单号 + 订单状态(3 列) | 订单状态 + 预定配送 + 物流信息(3 列) |
| `prod-clean-order.html` | 原表头缺「订单金额」列(12 列) | 补全订单金额 + 新增物流信息 + 收货码(15 列) |
### 共享数据模型(三页统一)
```js
logistics: {
type: 'self_dispatch' | 'self_grab' | 'third_party',
riderName, riderPhone, riderId, // 自营物流字段
carrier, trackingNo, // 三方物流字段
dispatchStatus: 'pending'|'accepted'|'picking'|'delivering'|'completed'|'rejected'|'timeout',
receiveCode: '582491', // 自营物流 6 位收货码
receiveStatus: 'pending'|'verifying'|'verified'
}
```
### 共享渲染函数(三页一致,跨页风格统一)
- `logisticsTypeTag(type)`:配送方式徽标(蓝/紫/橙)
- `dispatchStatusTag(s)`:配送过程七态徽标(对应 H5 状态色)
- `receiveStatusTag(s)`:收货状态徽标(未送达/待验证/已签收)
- `renderLogistics(logistics)`:列表「物流信息」单元(徽标+主信息+状态)
- `renderReceiveCode(logistics)`:列表「收货码」单元(只读高亮显示,prod-clean-order 专用)
- `renderLogisticsDetail(logistics)`:详情抽屉「配送方式 · 物流信息」分区(含配送员/承运方/收货码大字体展示)
### sup-clean-internal.html 变更
- Mock `claims[]` 5 条记录:`deliverPerson` 字段全部移除,改为 `logistics` 对象(INT-001/002 自营派单、INT-005 自营抢单、INT-003/004 为 null)
- 列表表头:3 列合并为「订单状态 + 预定配送 + 物流信息」,子表 td 数同步对齐 13 列
- 「立即配送」弹窗重构:新增 3 选 1 卡片式配送方式选择器(`deliver-mode-card`),3 个子面板:
- 自营派单:配送员下拉 + 收货码(系统生成,占位提示)
- 自营抢单:抢单模式说明 + 收货码(系统生成)
- 三方物流:承运方下拉(顺丰/京东/德邦/圆通/自有车队/其他)+ 运单号输入
- 新增 JS 函数:`switchDeliverMode(mode)` 切换子面板、`genReceiveCode()` 生成 6 位码、`doDeliver()` 按所选模式提交并写入 `logistics` 对象
- 「确认收货」操作:增加二次确认 + 同步 `dispatchStatus='completed'``receiveStatus='verified'`
- 详情抽屉:移除「配送人」字段,新增「配送方式 · 物流信息」独立分区,展示配送员/工号/电话/配送状态 + 6 位收货码(蓝色虚线框 + Consolas 等宽字体)
- 新建申领单弹窗:移除「配送人」字段,改为「配送方式」只读提示
### sup-clean-external.html 变更
- Mock `extOrders[]` 6 条记录:字符串 `logistics:'SF1234567890'` 全部改为对象(EXT-001/006 三方物流、EXT-002 自营派单、EXT-005 自营抢单、EXT-003/004 为 null)
- 列表表头:3 列重排为「订单状态 + 预定配送 + 物流信息」(列数不变)
- 「立即发货」弹窗(`shipModal`)重构:同 internal 风格的 3 选 1 配送方式选择器(`ship-mode-card`),3 个子面板命名独立 `shipPanel-*` 避免冲突
- 新增 JS 函数:`switchShipMode(mode)``genReceiveCodeExt()``confirmShip()` 支持三种模式提交
- 详情抽屉:移除「物流单号」字段,新增「配送方式 · 物流信息」分区
### prod-clean-order.html 变更
- Mock `orders[]` 8 条记录:新增 `logistics` 对象(从上游 INT-/EXT- 同步)+ `sourceOrderNo` 字段(关联上游单号)
- 列表表头:12 列扩为 15 列,补全「订单金额」列 + 新增「物流信息」「收货码」2 列
- 新增 `renderReceiveCode(logistics)`:列表「收货码」专用单元,6 位码 + 收货状态徽标
- 子表头/子表数据行:空 td 数同步对齐到 15 列
- 详情抽屉:新增「来源单号」字段 + 「配送方式 · 物流信息」分区
- 新建订单(`submitCreateOrder`)与食谱快速创建(`submitRecipeOrder`):生成的订单均初始化 `sourceOrderNo:null, logistics:null`
### 业务约定更新
- 三种配送方式的数据沉淀:自营派单/抢单会写入 H5 任务流,三方物流仅作后台物流登记
- 收货码仅在自营物流下生效:三方物流由承运方系统回调或后台手动确认收货
- 配送过程七态(对应 H5):pending(待响应)/accepted(已接单)/picking(取货中)/delivering(配送中)/completed(已送达)/rejected(已拒单)/timeout(超时未响应)
- 净菜订单聚合页为只读物流视角:不发起配送,仅展示上游同步过来的物流状态与收货码
---
## [2026-08-12] feat: 场所管理新增「排版设置」入口 + 班次/人员排班独立页
### 变更背景
原场所管理列表(`din-canteen.html`)操作列只有「详情/餐线管理/编辑/删除」,缺少对场所内部班次与上班人员编排的入口。不同场所的班次时段、上班人员差异较大(如总部一号食堂三班倒、采气一厂营养餐厅仅午晚两班),需要独立页面承载编排逻辑,且班次与餐次解耦(班次仅由上下班时间与每周重复决定,不绑定具体餐次)。
### 新增
- **`din-canteen-shift.html`(场所排版设置页)**
- 顶部面包屑(场所管理 / 排版设置)+ 场所信息卡片(名称/编号/所属单位/负责人/电话),按 URL 参数 `id` 渲染对应场所信息(5 个场所全量映射)
- **班次设置区**:表格列(班次名称/上班时间/下班时间/每周重复/操作),周几沿用详情抽屉蓝色方块视觉;新增/编辑弹窗(班次名称 + 时间 + 周几多选 chip);删除前校验是否有人员正在使用该班次,有则阻断并提示人员名单
- **上班人员设置区**:表格列(员工姓名/工号/所属部门/分配班次/操作);添加人员弹窗从员工库多选(已添加人员置灰禁用);修改班次弹窗支持单独调整某员工的班次;班次下拉项来源于班次设置区,无班次时禁止添加人员
- 底部操作栏:保存(原型演示提示)、取消(返回场所管理)
- **`din-canteen.html`**:5 行操作列在「餐线管理」与「编辑」之间新增「排版设置」链接,跳转 `din-canteen-shift.html?id=xxx&name=xxx`
- **`index.html`**:`SITE_MAP``din-canteen-line.html` 后新增 `din-canteen-shift.html` 条目(label:场所排版设置)
### 设计约束
- **班次与餐次解耦**:班次仅由上下班时间 + 每周重复决定,不绑定早餐/午餐等餐次
- **最小集合**:不做轮班/倒班、不做批量分配、不做导出排班表、不复制到其他场所
- **班次删除保护**:被人员引用的班次不可删除,需先调整人员班次
- **人员添加前置校验**:班次列表为空时禁止添加人员,引导用户先配置班次
- **URL 参数**:沿用 `din-canteen-line.html``?id=xxx&name=xxx` 约定,原型仅 `id=1` 完整匹配,其他 id 回退到默认数据
- **视觉一致**:复用 `din-canteen-line.html` 的 info-card / table-card / modal / form 组件,无新增样式令牌
---
## [2026-08-11] feat: 采供管理新增「待确认」状态 + 立即确认补全入口
### 变更背景
+238 -13
View File
@@ -184,6 +184,34 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
.seasoning-radio-group{display:inline-flex;align-items:center;gap:12px;white-space:nowrap;}
.seasoning-radio-group label{display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:var(--font-xs);color:var(--text-primary);}
.seasoning-radio-group input[type="radio"]{width:auto;height:auto;margin:0;cursor:pointer;accent-color:var(--primary);}
/* 物流徽标单元(列表悬浮交互) */
.logistics-cell{display:inline-flex;align-items:center;transition:transform .15s;}
.logistics-cell:hover{transform:scale(1.05);}
.logistics-cell .tag{cursor:pointer;}
/* 配送进度时间轴(横向 4 节点) */
.logistics-timeline{display:flex;align-items:flex-start;gap:0;padding:8px 4px 4px;}
.timeline-node{display:flex;flex-direction:column;align-items:center;flex-shrink:0;position:relative;flex:1;max-width:110px;}
.timeline-dot{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:var(--font-xs);font-weight:700;z-index:2;transition:all .3s;}
.timeline-node--done .timeline-dot{background:var(--success);color:#fff;border:2px solid var(--success);}
.timeline-node--current .timeline-dot{background:#fff;color:var(--primary);border:2px solid var(--primary);animation:timelinePulse 1.6s ease-in-out infinite;}
.timeline-node--pending .timeline-dot{background:#fff;color:var(--text-placeholder);border:2px dashed var(--border);}
.timeline-content{text-align:center;margin-top:6px;}
.timeline-label{font-size:var(--font-xs);color:var(--text-secondary);font-weight:500;}
.timeline-time{font-size:10px;color:var(--text-placeholder);margin-top:2px;line-height:1.3;white-space:nowrap;}
.timeline-line{position:absolute;top:14px;left:50%;width:100%;height:2px;background:var(--border);z-index:1;}
.timeline-line--done{background:var(--success);}
@keyframes timelinePulse{0%,100%{box-shadow:0 0 0 0 rgba(24,144,255,0.45);}50%{box-shadow:0 0 0 8px rgba(24,144,255,0);}}
/* 物流详情弹窗 */
.logistics-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);}
.logistics-modal-title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);}
.logistics-modal-body{padding:20px 24px;max-height:70vh;overflow-y:auto;}
.logistics-modal-section{margin-bottom:20px;}
.logistics-modal-section:last-child{margin-bottom:0;}
.logistics-modal-section__title{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:10px;display:flex;align-items:center;gap:6px;}
.logistics-modal-info{display:grid;grid-template-columns:1fr 1fr;gap:8px 20px;font-size:var(--font-sm);}
.logistics-modal-info__item{display:flex;gap:8px;}
.logistics-modal-info__label{color:var(--text-placeholder);flex-shrink:0;}
.logistics-modal-info__value{color:var(--text-primary);font-weight:500;}
</style>
</head>
<body>
@@ -245,7 +273,7 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<div class="table-card__header"><div class="table-card__title">净菜订单</div></div>
<div class="table-card__content">
<table id="orderTable">
<thead><tr><th style="width:36px;"></th><th>订单号</th><th>订单类型</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总计量(kg)</th><th>订单状态</th><th>申领时间</th><th>预定配送时间</th><th>创建人</th><th>操作</th></tr></thead>
<thead><tr><th style="width:36px;"></th><th>订单号</th><th>订单类型</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总计量(kg)</th><th>订单金额</th><th>订单状态</th><th>申领时间</th><th>预定配送时间</th><th>创建人</th><th>物流信息</th><th>操作</th></tr></thead>
<tbody id="orderTableBody"></tbody>
</table>
</div>
@@ -358,6 +386,17 @@ 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-logistics">
<div class="modal" style="width:500px;">
<div class="logistics-modal-header">
<span class="logistics-modal-title">🚚 物流详情 — <span id="logisticsOrderNo"></span></span>
<button class="modal__close" onclick="closeModal('modal-logistics')"></button>
</div>
<div class="logistics-modal-body" id="logisticsModalBody"></div>
</div>
</div>
<script>
/* ========== 弹窗开关 ========== */
function openModal(id){
@@ -424,37 +463,49 @@ function getDishNamesWithFallback(){
return Object.keys(FALLBACK_DISHES);
}
/* 订单主子表数据 */
/* 订单主子表数据
* logistics 字段从上游申领单(internal)/外售订单(external) 同步而来
* 此页为聚合视角,不可编辑物流,仅展示
* sourceOrderNo: 关联上游单号(INT-XXX / EXT-XXX),便于追溯
*/
var orders=[
{
orderNo:'CO-001',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 07:30',deliverTime:'2026-04-22 10:00',creator:'张厨师',
orderNo:'CO-001',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 07:30',deliverTime:'2026-04-22 10:00',creator:'张厨师',sourceOrderNo:'INT-001',
logistics:{type:'self_dispatch',riderName:'李配送',riderPhone:'138****1234',riderId:'RDR-1024',dispatchStatus:'completed',receiveCode:'582491',receiveStatus:'verified',
t1_ship:'2026-04-22 08:30',t2_accept:'2026-04-22 08:32',t3_pick:'2026-04-22 08:45',t4_deliver:'2026-04-22 09:05'},
lines:[
{id:'L1',ncType:'nc',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,price:5,subtotal:125,status:'已完成'},
{id:'L2',ncType:'nc',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,price:6,subtotal:120,status:'已完成'}
]
},
{
orderNo:'CO-002',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',
orderNo:'CO-002',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',sourceOrderNo:'INT-002',
logistics:{type:'self_dispatch',riderName:'王配送',riderPhone:'139****5678',riderId:'RDR-1025',dispatchStatus:'delivering',receiveCode:'347820',receiveStatus:'pending',
t1_ship:'2026-04-22 08:30',t2_accept:'2026-04-22 08:32',t3_pick:'2026-04-22 08:45',t4_deliver:null},
lines:[
{id:'L3',ncType:'nc',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,price:8,subtotal:320,status:'配送中'},
{id:'L4',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:30,weight:15,price:12,subtotal:180,status:'已完成',needSeasoning:true}
]
},
{
orderNo:'CO-003',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',
orderNo:'CO-003',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',sourceOrderNo:'EXT-002',
logistics:{type:'self_dispatch',riderName:'陈师傅',riderPhone:'138****6688',riderId:'RDR-2026',dispatchStatus:'delivering',receiveCode:'347820',receiveStatus:'pending',
t1_ship:'2026-04-22 10:00',t2_accept:'2026-04-22 10:02',t3_pick:'2026-04-22 10:15',t4_deliver:null},
lines:[
{id:'L5',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(500g',qty:15,weight:7.5,price:15,subtotal:112.5,status:'配送中',needSeasoning:false}
]
},
{
orderNo:'CO-004',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 09:00',deliverTime:'2026-04-22 11:00',creator:'王厨师',
orderNo:'CO-004',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 09:00',deliverTime:'2026-04-22 11:00',creator:'王厨师',sourceOrderNo:'INT-004',
logistics:null,
lines:[
{id:'L6',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,price:4,subtotal:80,status:'待配送'},
{id:'L7',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:5,subtotal:75,status:'备货中'}
]
},
{
orderNo:'CO-005',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:00',creator:'张厨师',
orderNo:'CO-005',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:00',creator:'张厨师',sourceOrderNo:'INT-005',
logistics:null,
lines:[
{id:'L8',ncType:'cp',foodName:'红烧排骨',spec:'标准份(800g',qty:10,weight:8,price:18,subtotal:144,status:'备货中',needSeasoning:true},
{id:'L9',ncType:'nc',ingredient:'鸡胸肉',ncName:'鸡胸肉丁',spec:'500g/袋',qty:15,weight:7.5,price:10,subtotal:75,status:'备货中'},
@@ -462,21 +513,27 @@ var orders=[
]
},
{
orderNo:'CO-006',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 10:00',deliverTime:'2026-04-22 11:30',creator:'王厨师',
orderNo:'CO-006',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 10:00',deliverTime:'2026-04-22 11:30',creator:'王厨师',sourceOrderNo:'EXT-006',
logistics:{type:'third_party',carrier:'京东物流',trackingNo:'JD5566778899',dispatchStatus:'completed',receiveCode:null,receiveStatus:'verified',
t1_ship:'2026-04-22 13:00',t2_accept:'2026-04-22 13:05',t3_pick:'2026-04-22 13:30',t4_deliver:'2026-04-22 14:00'},
lines:[
{id:'L11',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:25,weight:12.5,price:12,subtotal:150,status:'已完成',needSeasoning:true},
{id:'L12',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g',qty:20,weight:12,price:15,subtotal:180,status:'已完成',needSeasoning:false}
]
},
{
orderNo:'CO-007',orderType:'外售',ncType:'nc',claimTime:'2026-04-22 10:30',deliverTime:'2026-04-22 14:00',creator:'李主管',
orderNo:'CO-007',orderType:'外售',ncType:'nc',claimTime:'2026-04-22 10:30',deliverTime:'2026-04-22 14:00',creator:'李主管',sourceOrderNo:'EXT-005',
logistics:{type:'self_grab',riderName:'张师傅',riderPhone:'137****8888',riderId:'RDR-2026',dispatchStatus:'delivering',receiveCode:'915374',receiveStatus:'pending',
t1_ship:'2026-04-22 11:30',t2_accept:'2026-04-22 11:35',t3_pick:'2026-04-22 11:50',t4_deliver:null},
lines:[
{id:'L13',ncType:'nc',ingredient:'土豆',ncName:'土豆块',spec:'2kg/袋',qty:10,weight:20,price:4,subtotal:80,status:'已完成'},
{id:'L14',ncType:'nc',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:20,weight:10,price:5,subtotal:50,status:'配送中'}
]
},
{
orderNo:'CO-008',orderType:'内供',ncType:'cp',claimTime:'2026-04-22 11:00',deliverTime:'2026-04-22 13:00',creator:'张厨师',
orderNo:'CO-008',orderType:'内供',ncType:'cp',claimTime:'2026-04-22 11:00',deliverTime:'2026-04-22 13:00',creator:'张厨师',sourceOrderNo:'INT-001',
logistics:{type:'self_dispatch',riderName:'李配送',riderPhone:'138****1234',riderId:'RDR-1024',dispatchStatus:'completed',receiveCode:'582491',receiveStatus:'verified',
t1_ship:'2026-04-22 12:00',t2_accept:'2026-04-22 12:02',t3_pick:'2026-04-22 12:15',t4_deliver:'2026-04-22 12:40'},
lines:[
{id:'L15',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:5,subtotal:75,status:'已完成'},
{id:'L16',ncType:'cp',foodName:'蒜蓉菠菜',spec:'标准份(400g',qty:15,weight:6,price:10,subtotal:60,status:'已完成',needSeasoning:false},
@@ -489,6 +546,162 @@ var orders=[
function calcOrderTotal(o){return o.lines.reduce(function(s,l){return s+(parseFloat(l.subtotal)||0);},0);}
function formatMoney(n){var v=parseFloat(n)||0;return '¥'+v.toLocaleString('zh-CN',{minimumFractionDigits:0,maximumFractionDigits:2});}
/* ========== 物流信息共享渲染(只读视图,订单聚合页不发起配送,仅展示上游 internal/external 的物流信息) ========== */
function logisticsTypeTag(type){
var m={
'self_dispatch':'<span class="tag tag-blue">🛵 自营·派单</span>',
'self_grab':'<span class="tag tag-purple">🔥 自营·抢单</span>',
'third_party':'<span class="tag tag-orange">🚚 三方物流</span>'
};
return m[type]||'<span class="tag tag-gray">— 未配送</span>';
}
function dispatchStatusTag(s){
var m={
'pending':'<span class="tag tag-gray">⏳ 待响应</span>',
'accepted':'<span class="tag tag-blue">✓ 已接单</span>',
'picking':'<span class="tag tag-orange">📦 取货中</span>',
'delivering':'<span class="tag tag-purple">🛵 配送中</span>',
'completed':'<span class="tag tag-green">✓ 已送达</span>',
'rejected':'<span class="tag tag-red">✗ 已拒单</span>',
'timeout':'<span class="tag tag-gray">⏰ 超时未响应</span>'
};
return m[s]||'';
}
function receiveStatusTag(s){
var m={
'pending':'<span class="tag tag-gray">未送达</span>',
'verifying':'<span class="tag tag-orange">待验证</span>',
'verified':'<span class="tag tag-green">已签收</span>'
};
return m[s]||'<span class="tag tag-gray">未送达</span>';
}
/* 列表用「物流信息」单元 — 只显示配送方式徽标,点击弹出物流详情(含收货码) */
function renderLogistics(logistics,orderNo){
if(!logistics){
return '<span style="color:var(--text-placeholder);">—</span>';
}
return '<span class="logistics-cell" onclick="event.stopPropagation();showLogisticsModal(\''+orderNo+'\')" style="cursor:pointer;">'
+logisticsTypeTag(logistics.type)
+'</span>';
}
/* 物流详情时间轴:发货 → 接单 → 取货 → 送达 */
function renderLogisticsTimeline(logistics){
var nodes=[
{key:'t1_ship',label:'发货',icon:'📦'},
{key:'t2_accept',label:'接单',icon:'✓'},
{key:'t3_pick',label:'取货',icon:'🛵'},
{key:'t4_deliver',label:'送达',icon:'🏁'}
];
var currentIdx=-1;
for(var i=0;i<nodes.length;i++){
if(!logistics[nodes[i].key]){
currentIdx=i;break;
}
}
if(currentIdx===-1)currentIdx=nodes.length;
var h='<div class="logistics-timeline">';
nodes.forEach(function(n,i){
var state='done';
if(i===currentIdx)state='current';
else if(i>currentIdx)state='pending';
h+='<div class="timeline-node timeline-node--'+state+'">';
h+='<div class="timeline-dot">'+(state==='done'?'✓':(state==='current'?'●':n.icon))+'</div>';
h+='<div class="timeline-content">';
h+='<div class="timeline-label">'+n.label+'</div>';
h+='<div class="timeline-time">'+(logistics[n.key]||'—')+'</div>';
h+='</div>';
if(i<nodes.length-1){
var lineState=(i<currentIdx)?'done':'pending';
h+='<div class="timeline-line timeline-line--'+lineState+'"></div>';
}
h+='</div>';
});
h+='</div>';
return h;
}
/* 详情抽屉用「物流信息」单元(完整版,买家视角保留收货码) */
function renderLogisticsDetail(logistics){
if(!logistics){
return '<div style="color:var(--text-placeholder);font-size:var(--font-sm);">该订单尚未发起配送(上游申领单/外售订单未点立即配送)</div>';
}
var html='<div style="display:flex;flex-direction:column;gap:14px;">';
html+='<div>'+logisticsTypeTag(logistics.type)+'</div>';
if(logistics.type==='self_dispatch'||logistics.type==='self_grab'){
html+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;font-size:var(--font-sm);">';
html+='<div><span style="color:var(--text-placeholder);">配送员:</span> <b>'+logistics.riderName+'</b></div>';
html+='<div><span style="color:var(--text-placeholder);">联系电话:</span> '+logistics.riderPhone+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送员工号:</span> '+logistics.riderId+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送状态:</span> '+dispatchStatusTag(logistics.dispatchStatus)+'</div>';
html+='</div>';
}else if(logistics.type==='third_party'){
html+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;font-size:var(--font-sm);">';
html+='<div><span style="color:var(--text-placeholder);">承运方:</span> <b>'+logistics.carrier+'</b></div>';
html+='<div><span style="color:var(--text-placeholder);">运单号:</span> '+logistics.trackingNo+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送状态:</span> '+dispatchStatusTag(logistics.dispatchStatus)+'</div>';
html+='</div>';
}
/* 配送进度时间轴 */
html+='<div><div style="font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:8px;font-weight:600;">📊 配送进度</div>'+renderLogisticsTimeline(logistics)+'</div>';
/* 收货码(买家视角可见) */
if(logistics.receiveCode){
html+='<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--primary-light);border-radius:var(--radius-md);border:1px dashed var(--primary);">';
html+='<span style="font-size:var(--font-sm);color:var(--primary);font-weight:600;">🔑 收货码</span>';
html+='<span style="font-size:var(--font-xl);font-weight:700;color:var(--primary);letter-spacing:4px;font-family:Consolas,Monaco,monospace;">'+logistics.receiveCode+'</span>';
html+='<span style="font-size:var(--font-xs);color:var(--text-secondary);">('+receiveStatusTag(logistics.receiveStatus)+')</span>';
html+='</div>';
}
html+='</div>';
return html;
}
/* 物流详情弹窗:点击列表徽标触发(订单视角包含收货码) */
function showLogisticsModal(orderNo){
var o=orders.find(function(x){return x.orderNo===orderNo;});
if(!o||!o.logistics){alert('该订单尚未发起配送');return;}
var lg=o.logistics;
document.getElementById('logisticsOrderNo').textContent=orderNo;
var h='';
/* Section 1: 配送方式徽标 */
h+='<div class="logistics-modal-section">';
h+='<div class="logistics-modal-section__title">🚚 配送方式</div>';
h+='<div>'+logisticsTypeTag(lg.type)+'</div>';
h+='</div>';
/* Section 2: 承运信息 */
h+='<div class="logistics-modal-section">';
h+='<div class="logistics-modal-section__title">📋 承运信息</div>';
if(lg.type==='self_dispatch'||lg.type==='self_grab'){
h+='<div class="logistics-modal-info">';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">配送员:</span><span class="logistics-modal-info__value">'+lg.riderName+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">联系电话:</span><span class="logistics-modal-info__value">'+lg.riderPhone+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">工号:</span><span class="logistics-modal-info__value">'+lg.riderId+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">状态:</span><span class="logistics-modal-info__value">'+dispatchStatusTag(lg.dispatchStatus)+'</span></div>';
h+='</div>';
}else if(lg.type==='third_party'){
h+='<div class="logistics-modal-info">';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">承运方:</span><span class="logistics-modal-info__value">'+lg.carrier+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">运单号:</span><span class="logistics-modal-info__value">'+lg.trackingNo+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">状态:</span><span class="logistics-modal-info__value">'+dispatchStatusTag(lg.dispatchStatus)+'</span></div>';
h+='</div>';
}
h+='</div>';
/* Section 3: 收货码(买家视角可见) */
if(lg.receiveCode){
h+='<div class="logistics-modal-section">';
h+='<div class="logistics-modal-section__title">🔑 收货码</div>';
h+='<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--primary-light);border-radius:var(--radius-md);border:1px dashed var(--primary);">';
h+='<span style="font-size:var(--font-xl);font-weight:700;color:var(--primary);letter-spacing:4px;font-family:Consolas,Monaco,monospace;">'+lg.receiveCode+'</span>';
h+='<span style="font-size:var(--font-xs);color:var(--text-secondary);">('+receiveStatusTag(lg.receiveStatus)+')</span>';
h+='</div>';
h+='</div>';
}
/* Section 4: 配送进度时间轴 */
h+='<div class="logistics-modal-section">';
h+='<div class="logistics-modal-section__title">📊 配送进度</div>';
h+=renderLogisticsTimeline(lg);
h+='</div>';
document.getElementById('logisticsModalBody').innerHTML=h;
openModal('modal-logistics');
}
/* ========== 状态聚合逻辑 ========== */
function aggregateStatus(lines){
var statuses=lines.map(function(l){return l.status;});
@@ -549,6 +762,8 @@ function renderOrderTable(){
html+='<td>'+order.claimTime+'</td>';
html+='<td>'+order.deliverTime+'</td>';
html+='<td>'+order.creator+'</td>';
/* 物流信息(来自上游申领单/外售订单) — 列表只展示徽标,点击查看详情(含收货码) */
html+='<td>'+renderLogistics(order.logistics,order.orderNo)+'</td>';
/* 操作列:状态按钮 + 详情 + 删除 */
html+='<td class="td-actions">';
if(aggStatus==='生成中'){
@@ -566,9 +781,9 @@ function renderOrderTable(){
/* 子表头 */
if(order.lines.length>0){
if(order.ncType==='nc'){
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td></tr>';
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
}else{
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td colspan="2">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td></tr>';
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td colspan="2">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
}
}
/* 明细行 */
@@ -586,7 +801,7 @@ function renderOrderTable(){
html+='<td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td>';
/* 内配调料列:仅 cp 类型显示是否需要,nc 类型显示 — */
html+='<td>'+seasoningTag(line)+'</td>';
html+='<td></td><td></td><td></td>';
html+='<td></td><td></td><td></td><td></td>';
html+='<td class="td-actions"></td></tr>';
});
});
@@ -876,6 +1091,8 @@ function submitCreateOrder(){
claimTime:new Date().toISOString().slice(0,16).replace('T',' '),
deliverTime:deliverTime,creator:creator,
ncType:newOrderNcType,
sourceOrderNo:null,
logistics:null,
lines:validLines.map(function(l,i){
var base={
id:'L'+(Date.now()+i),
@@ -966,8 +1183,14 @@ function showDetail(orderNo){
html+='<div class="drawer-field"><div class="drawer-field__label">申领时间</div><div class="drawer-field__value">'+order.claimTime+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">预定配送时间</div><div class="drawer-field__value">'+order.deliverTime+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">创建人</div><div class="drawer-field__value">'+order.creator+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">来源单号</div><div class="drawer-field__value">'+(order.sourceOrderNo||'—')+'</div></div>';
html+='</div></div>';
/* 物流方式独立分区(只读,数据来自上游申领单/外售订单) */
html+='<div class="drawer-section"><div class="drawer-section__title">配送方式 · 物流信息</div>';
html+=renderLogisticsDetail(order.logistics);
html+='</div>';
/* 订单明细列表 */
html+='<div class="drawer-section"><div class="drawer-section__title">订单明细('+order.lines.length+' 道菜品)</div>';
var isNcOrder=order.ncType==='nc';
@@ -1235,6 +1458,8 @@ function submitRecipeOrder(){
orderNo:orderNo,orderType:'内供',ncType:'cp',
claimTime:new Date().toISOString().slice(0,16).replace('T',' '),
deliverTime:deliveryTime.replace('T',' '),creator:'系统',
sourceOrderNo:null,
logistics:null,
lines:newLines
});
closeModal('modal-recipe-order');
+276 -6
View File
@@ -181,6 +181,41 @@ tbody tr.row-active td.td-actions{background:var(--primary-light);}
.drawer-field{display:flex;flex-direction:column;gap:2px;}
.drawer-field__label{font-size:var(--font-xs);color:var(--text-placeholder);}
.drawer-field__value{font-size:var(--font-sm);color:var(--text-primary);font-weight:500;min-height:22px;}
/* 操作列「更多」下拉菜单 */
.action-cell{position:relative;display:inline-flex;align-items:center;gap:8px;}
.action-popover{position:absolute;top:calc(100% + 4px);right:0;min-width:96px;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-md);z-index:10;padding:4px 0;display:none;}
.action-popover.show{display:block;}
.action-popover__item{padding:8px 14px;font-size:var(--font-sm);color:var(--text-primary);cursor:pointer;white-space:nowrap;}
.action-popover__item:hover{background:var(--primary-light);color:var(--primary);}
/* 历史记录弹窗专用样式(1100px × 80vh 居中弹窗) */
.history-modal-mask{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:1500;display:flex;align-items:center;justify-content:center;animation:modalFadeIn .2s ease;}
.history-modal-mask.closing{animation:modalFadeOut .2s ease forwards;}
.history-modal{width:1100px;max-width:94vw;height:80vh;max-height:90vh;background:#fff;border-radius:var(--radius-md);box-shadow:0 12px 40px rgba(0,0,0,.18);display:flex;flex-direction:column;overflow:hidden;animation:historyModalIn .22s ease;}
.history-modal-mask.closing .history-modal{animation:historyModalOut .18s ease forwards;}
@keyframes historyModalIn{from{transform:translateY(16px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
@keyframes historyModalOut{from{transform:translateY(0) scale(1);opacity:1}to{transform:translateY(16px) scale(.98);opacity:0}}
.history-modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);flex-shrink:0;}
.history-modal__title{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);}
.history-modal__close{width:32px;height:32px;border:none;background:none;font-size:20px;cursor:pointer;border-radius:var(--radius-sm);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;}
.history-modal__close:hover{background:#f5f5f5;color:var(--text-primary);}
.history-modal__body{padding:20px 24px;overflow-y:auto;flex:1;}
.history-modal__footer{padding:12px 24px;border-top:1px solid var(--border);flex-shrink:0;background:#FAFBFC;}
.history-summary{display:flex;align-items:center;gap:16px;flex-wrap:wrap;background:#F8FBFF;border:1px solid var(--border);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:16px;}
.history-summary__item{display:flex;align-items:center;gap:6px;font-size:var(--font-sm);color:var(--text-secondary);}
.history-summary__item strong{color:var(--text-primary);font-weight:600;}
.history-summary__item .label{color:var(--text-placeholder);font-size:var(--font-xs);}
.history-summary__count{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:var(--font-xs);}
.history-summary__count.in{background:#F6FFED;color:var(--success);}
.history-summary__count.out{background:#FFF7E6;color:var(--warning);}
.history-table{width:100%;border-collapse:collapse;}
.history-table thead th{background:#FAFAFA;padding:10px 12px;text-align:left;font-size:var(--font-xs);color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap;}
.history-table tbody td{padding:10px 12px;font-size:var(--font-sm);color:var(--text-primary);border-bottom:1px solid var(--border);white-space:nowrap;}
.history-table tbody tr:last-child td{border-bottom:none;}
.history-delta{display:inline-flex;align-items:center;gap:2px;font-weight:600;}
.history-delta.pos{color:var(--success);}
.history-delta.neg{color:var(--danger);}
.history-pagination{display:flex;align-items:center;justify-content:flex-end;padding:14px 0 0;gap:6px;border-top:1px solid var(--border);margin-top:12px;}
.history-pagination__total{font-size:var(--font-sm);color:var(--text-secondary);margin-right:auto;}
</style>
</head>
<body>
@@ -332,12 +367,12 @@ tbody tr.row-active td.td-actions{background:var(--primary-light);}
<table>
<thead><tr><th>格口</th><th>关联订单</th><th>订单菜品</th><th>存放净菜</th><th>类型</th><th>初始重量(kg)</th><th>当前余量(kg)</th><th>操作人</th><th>温度(℃)</th><th>保质剩余</th><th>存入时间</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="detailTable">
<tr class="row-active" data-slot="0" data-type="净菜" data-order="JC-2026-0422-001" data-dish="土豆丝" data-trace="NC-CAB-BOCA-0422-01"><td>A1-01</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-001</a></td><td>土豆丝</td><td>土豆(土豆丝)</td><td><span class="tag tag-green">🥬 净菜</span></td><td>12.5</td><td>10.2</td><td>张厨师</td><td>4.2</td><td>剩余32h</td><td>04-22 09:15</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-01','土豆')">视频</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link">编辑</span></td></tr>
<tr data-slot="1" data-type="净菜" data-order="JC-2026-0422-002" data-dish="净胡萝卜" data-trace="NC-CAB-HLUB-0422-01"><td>A1-02</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-002</a></td><td>净胡萝卜</td><td>净胡萝卜</td><td><span class="tag tag-green">🥬 净菜</span></td><td>18.0</td><td>15.6</td><td>李主管</td><td>4.1</td><td>剩余46h</td><td>04-22 09:20</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-02','净胡萝卜')">视频</span> <span class="link" onclick="showDetail(this)">详情</span><span class="link">编辑</span></td></tr>
<tr data-slot="2" data-type="净菜" data-order="JC-2026-0422-003" data-dish="净香菇" data-trace="NC-CAB-XGUO-0422-01"><td>A1-03</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-003</a></td><td>净香菇</td><td>净香菇</td><td><span class="tag tag-green">🥬 净菜</span></td><td>9.0</td><td>6.8</td><td>王厨师</td><td>4.3</td><td>剩余6h</td><td>04-22 09:30</td><td><span class="tag tag-orange">临期预警</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-03','净香菇')">视频</span> <span class="link" onclick="showDetail(this)">详情</span><span class="link">编辑</span></td></tr>
<tr data-slot="3" data-type="净菜" data-order="JC-2026-0422-004" data-dish="净西兰花" data-trace="NC-CAB-XLHU-0422-01"><td>A1-04</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-004</a></td><td>净西兰花</td><td>净西兰花</td><td><span class="tag tag-green">🥬 净菜</span></td><td>10.0</td><td>8.5</td><td>张厨师</td><td>4.0</td><td>剩余50h</td><td>04-22 09:40</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-04','净西兰花')">视频</span> <span class="link" onclick="showDetail(this)">详情</span><span class="link">编辑</span></td></tr>
<tr data-slot="4" data-type="餐品包" data-order="JC-2026-0422-005" data-dish="宫保鸡丁" data-trace="PKG-DH-001"><td>A1-05</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-005</a></td><td>宫保鸡丁</td><td>宫保鸡丁</td><td><span class="tag tag-orange">🍱 餐品包</span></td><td>6.0</td><td>5.4</td><td>李主管</td><td>3.8</td><td>剩余44h</td><td>04-22 10:20</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-05','宫保鸡丁')">视频</span> <span class="link" onclick="showDetail(this)">详情</span><span class="link">编辑</span></td></tr>
<tr data-slot="5" data-type="净菜" data-order="JC-2026-0422-006" data-dish="净白菜" data-trace="NC-CAB-BCAI-0422-01"><td>A1-06</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-006</a></td><td>净白菜</td><td>净白菜</td><td><span class="tag tag-green">🥬 净菜</span></td><td>22.0</td><td>18.0</td><td>王厨师</td><td>7.8</td><td>剩余40h</td><td>04-22 08:50</td><td><span class="tag tag-red">已过期</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-06','净白菜')">视频</span> <span class="link" onclick="showDetail(this)">详情</span><span class="link">编辑</span></td></tr>
<tr class="row-active" data-slot="0" data-type="净菜" data-order="JC-2026-0422-001" data-dish="土豆丝" data-trace="NC-CAB-BOCA-0422-01"><td>A1-01</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-001</a></td><td>土豆丝</td><td>土豆(土豆丝)</td><td><span class="tag tag-green">🥬 净菜</span></td><td>12.5</td><td>10.2</td><td>张厨师</td><td>4.2</td><td>剩余32h</td><td>04-22 09:15</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-01','土豆')">视频</span> <span class="link" onclick="showHistory(0)">历史</span> <div class="action-cell"><span class="link" onclick="toggleActionMenu(event,this)">更多 ▾</span><div class="action-popover"><div class="action-popover__item" onclick="showDetail(this)">详情</div><div class="action-popover__item" onclick="showEdit(this)">编辑</div></div></div></td></tr>
<tr data-slot="1" data-type="净菜" data-order="JC-2026-0422-002" data-dish="净胡萝卜" data-trace="NC-CAB-HLUB-0422-01"><td>A1-02</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-002</a></td><td>净胡萝卜</td><td>净胡萝卜</td><td><span class="tag tag-green">🥬 净菜</span></td><td>18.0</td><td>15.6</td><td>李主管</td><td>4.1</td><td>剩余46h</td><td>04-22 09:20</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-02','净胡萝卜')">视频</span> <span class="link" onclick="showHistory(1)">历史</span> <div class="action-cell"><span class="link" onclick="toggleActionMenu(event,this)">更多 ▾</span><div class="action-popover"><div class="action-popover__item" onclick="showDetail(this)">详情</div><div class="action-popover__item" onclick="showEdit(this)">编辑</div></div></div></td></tr>
<tr data-slot="2" data-type="净菜" data-order="JC-2026-0422-003" data-dish="净香菇" data-trace="NC-CAB-XGUO-0422-01"><td>A1-03</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-003</a></td><td>净香菇</td><td>净香菇</td><td><span class="tag tag-green">🥬 净菜</span></td><td>9.0</td><td>6.8</td><td>王厨师</td><td>4.3</td><td>剩余6h</td><td>04-22 09:30</td><td><span class="tag tag-orange">临期预警</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-03','净香菇')">视频</span> <span class="link" onclick="showHistory(2)">历史</span> <div class="action-cell"><span class="link" onclick="toggleActionMenu(event,this)">更多 ▾</span><div class="action-popover"><div class="action-popover__item" onclick="showDetail(this)">详情</div><div class="action-popover__item" onclick="showEdit(this)">编辑</div></div></div></td></tr>
<tr data-slot="3" data-type="净菜" data-order="JC-2026-0422-004" data-dish="净西兰花" data-trace="NC-CAB-XLHU-0422-01"><td>A1-04</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-004</a></td><td>净西兰花</td><td>净西兰花</td><td><span class="tag tag-green">🥬 净菜</span></td><td>10.0</td><td>8.5</td><td>张厨师</td><td>4.0</td><td>剩余50h</td><td>04-22 09:40</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-04','净西兰花')">视频</span> <span class="link" onclick="showHistory(3)">历史</span> <div class="action-cell"><span class="link" onclick="toggleActionMenu(event,this)">更多 ▾</span><div class="action-popover"><div class="action-popover__item" onclick="showDetail(this)">详情</div><div class="action-popover__item" onclick="showEdit(this)">编辑</div></div></div></td></tr>
<tr data-slot="4" data-type="餐品包" data-order="JC-2026-0422-005" data-dish="宫保鸡丁" data-trace="PKG-DH-001"><td>A1-05</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-005</a></td><td>宫保鸡丁</td><td>宫保鸡丁</td><td><span class="tag tag-orange">🍱 餐品包</span></td><td>6.0</td><td>5.4</td><td>李主管</td><td>3.8</td><td>剩余44h</td><td>04-22 10:20</td><td><span class="tag tag-green">存放中</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-05','宫保鸡丁')">视频</span> <span class="link" onclick="showHistory(4)">历史</span> <div class="action-cell"><span class="link" onclick="toggleActionMenu(event,this)">更多 ▾</span><div class="action-popover"><div class="action-popover__item" onclick="showDetail(this)">详情</div><div class="action-popover__item" onclick="showEdit(this)">编辑</div></div></div></td></tr>
<tr data-slot="5" data-type="净菜" data-order="JC-2026-0422-006" data-dish="净白菜" data-trace="NC-CAB-BCAI-0422-01"><td>A1-06</td><td><a class="link" href="prod-clean-order.html">JC-2026-0422-006</a></td><td>净白菜</td><td>净白菜</td><td><span class="tag tag-green">🥬 净菜</span></td><td>22.0</td><td>18.0</td><td>王厨师</td><td>7.8</td><td>剩余40h</td><td>04-22 08:50</td><td><span class="tag tag-red">已过期</span></td><td class="td-actions"><a class="link" href="trace-timeline-ingredient.html">溯源</a> <span class="link" onclick="showVideo('A1-06','净白菜')">视频</span> <span class="link" onclick="showHistory(5)">历史</span> <div class="action-cell"><span class="link" onclick="toggleActionMenu(event,this)">更多 ▾</span><div class="action-popover"><div class="action-popover__item" onclick="showDetail(this)">详情</div><div class="action-popover__item" onclick="showEdit(this)">编辑</div></div></div></td></tr>
<tr data-slot="6"><td>A1-07</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td>4.1</td><td></td><td></td><td><span class="tag tag-gray">空闲</span></td><td class="td-actions"></td></tr>
<tr data-slot="7"><td>A1-08</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td>4.1</td><td></td><td></td><td><span class="tag tag-gray">空闲</span></td><td class="td-actions"></td></tr>
<tr data-slot="8"><td>A1-09</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td>4.2</td><td></td><td></td><td><span class="tag tag-gray">空闲</span></td><td class="td-actions"></td></tr>
@@ -589,6 +624,34 @@ var Drawer={
}
};
/* 历史记录居中弹窗组件(1100×80vh,含遮罩/动画/ESC关闭) */
var HistoryModal={
open:function(title,bodyHtml,footerHtml){
this.close();
var mask=document.createElement('div');
mask.className='history-modal-mask';
var html='<div class="history-modal">'
+'<div class="history-modal__header"><div class="history-modal__title">'+title+'</div>'
+'<button class="history-modal__close" onclick="HistoryModal.close()">&times;</button></div>'
+'<div class="history-modal__body">'+bodyHtml+'</div>';
if(footerHtml){html+='<div class="history-modal__footer">'+footerHtml+'</div>';}
html+='</div>';
mask.innerHTML=html;
mask.addEventListener('click',function(e){if(e.target===mask)HistoryModal.close();});
document.body.appendChild(mask);
/* ESC 关闭 */
this._escHandler=function(e){if(e.key==='Escape')HistoryModal.close();};
document.addEventListener('keydown',this._escHandler);
},
close:function(){
var m=document.querySelector('.history-modal-mask');
if(!m)return;
m.classList.add('closing');
setTimeout(function(){m.remove();},200);
if(this._escHandler){document.removeEventListener('keydown',this._escHandler);this._escHandler=null;}
}
};
/* 详情抽屉 — 根据类型区分渲染 */
function showDetail(el){
var row=el.closest('tr');
@@ -648,6 +711,213 @@ function showDetail(el){
Drawer.open('保温柜详情 — '+slotId,html);
}
/* ============== 「更多」操作下拉菜单(渲染到 body,避免被表格 overflow 裁剪) ============== */
var __ACTION_ROW__=null; /* 当前展开菜单对应的行引用(一次只展开一个) */
var __ACTION_POP__=null; /* 当前展开的菜单节点 */
function closeActionMenu(){
if(__ACTION_POP__){__ACTION_POP__.remove();__ACTION_POP__=null;}
__ACTION_ROW__=null;
}
function positionActionMenu(pop,el){
var rect=el.getBoundingClientRect();
pop.style.left=rect.right+'px';
/* 优先在按钮下方展开;若下方空间不足则向上 */
var spaceBelow=window.innerHeight-rect.bottom;
if(spaceBelow<160){
pop.style.top=(rect.top-8)+'px';
pop.style.transform='translateY(-100%)';
}else{
pop.style.top=(rect.bottom+6)+'px';
pop.style.transform='none';
}
}
function toggleActionMenu(e,el){
e.stopPropagation();
e.preventDefault();
/* 若当前已有菜单展开,且点的是同一个按钮:直接收起 */
if(__ACTION_POP__&&__ACTION_ROW__===el.closest('tr')){
closeActionMenu();
return;
}
closeActionMenu();
var row=el.closest('tr');
__ACTION_ROW__=row;
var pop=document.createElement('div');
pop.className='action-popover show'; /* show 类用于复用样式 */
pop.style.position='fixed';
pop.style.right='auto';
pop.style.minWidth='104px';
pop.innerHTML='<div class="action-popover__item" onclick="showDetail(__ACTION_ROW__);closeActionMenu();">详情</div>'
+'<div class="action-popover__item" onclick="showEdit(__ACTION_ROW__);closeActionMenu();">编辑</div>';
document.body.appendChild(pop);
__ACTION_POP__=pop;
positionActionMenu(pop,el);
}
/* 滚动/缩放时关闭菜单(避免定位错位) */
window.addEventListener('scroll',closeActionMenu,true);
window.addEventListener('resize',closeActionMenu);
/* 点击页面其他位置收起「更多」菜单 */
document.addEventListener('click',function(e){
if(__ACTION_POP__&&!__ACTION_POP__.contains(e.target)&&!e.target.matches('.action-cell .link')){
closeActionMenu();
}
});
/* 「编辑」占位(仅 toast 提示) */
function showEdit(el){
document.querySelectorAll('.action-popover.show').forEach(function(p){p.classList.remove('show');});
var row=el.closest('tr');
var slotId=row.querySelector('td').textContent.trim();
showToast('「'+slotId+'」编辑功能待开发(占位)');
}
/* 轻量 toast(不依赖其他组件) */
function showToast(msg){
var old=document.getElementById('cabinetToast');
if(old) old.remove();
var t=document.createElement('div');
t.id='cabinetToast';
t.textContent=msg;
t.style.cssText='position:fixed;top:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.78);color:#fff;padding:10px 20px;border-radius:6px;font-size:14px;z-index:3000;box-shadow:0 4px 12px rgba(0,0,0,.2);';
document.body.appendChild(t);
setTimeout(function(){if(t)t.remove();},2200);
}
/* ============== 格口历史记录(含同格口换菜品场景) ============== */
/* Mock 数据:6 个已占用格口各 4~9 条记录,按时间倒序(最早存入在最后) */
var HISTORY_DATA={
0:{ /* A1-01 · 当前 土豆(土豆丝),中途换过净西兰花 */
cabinet:'A区智能柜-1号',slot:'A1-01',
rows:[
{time:'2026-04-22 14:20',name:'净西兰花',type:'取用',before:8.0,after:7.2,op:'王厨师',order:'JC-2026-0422-014',remark:'做清炒西兰花'},
{time:'2026-04-22 12:05',name:'净西兰花',type:'存入',before:0.0,after:8.0,op:'张厨师',order:'JC-2026-0422-012',remark:'换菜存入'},
{time:'2026-04-22 11:30',name:'土豆丝',type:'取用',before:11.0,after:10.2,op:'李主管',order:'JC-2026-0422-005',remark:'做宫保鸡丁'},
{time:'2026-04-22 10:30',name:'土豆丝',type:'取用',before:11.8,after:11.0,op:'李主管',order:'JC-2026-0422-005',remark:'做宫保鸡丁'},
{time:'2026-04-22 09:15',name:'土豆丝',type:'存入',before:0.0,after:12.5,op:'张厨师',order:'JC-2026-0422-001',remark:'首次入柜'}
]
},
1:{ /* A1-02 · 净胡萝卜 */
cabinet:'A区智能柜-1号',slot:'A1-02',
rows:[
{time:'2026-04-22 15:40',name:'净胡萝卜',type:'取用',before:16.8,after:15.6,op:'王厨师',order:'JC-2026-0422-015',remark:'做胡萝卜炒肉'},
{time:'2026-04-22 13:20',name:'净胡萝卜',type:'取用',before:17.5,after:16.8,op:'王厨师',order:'JC-2026-0422-015',remark:'做胡萝卜炒肉'},
{time:'2026-04-22 11:00',name:'净胡萝卜',type:'取用',before:17.8,after:17.5,op:'李主管',order:'JC-2026-0422-008',remark:'做炖菜'},
{time:'2026-04-22 09:20',name:'净胡萝卜',type:'存入',before:0.0,after:18.0,op:'李主管',order:'JC-2026-0422-002',remark:'首次入柜'}
]
},
2:{ /* A1-03 · 净香菇(临期预警) */
cabinet:'A区智能柜-1号',slot:'A1-03',
rows:[
{time:'2026-04-22 16:10',name:'净香菇',type:'取用',before:7.5,after:6.8,op:'张厨师',order:'JC-2026-0422-018',remark:'做香菇青菜'},
{time:'2026-04-22 14:00',name:'净香菇',type:'取用',before:8.2,after:7.5,op:'张厨师',order:'JC-2026-0422-018',remark:'做香菇青菜'},
{time:'2026-04-22 11:45',name:'净香菇',type:'取用',before:8.8,after:8.2,op:'王厨师',order:'JC-2026-0422-016',remark:'做香菇鸡块'},
{time:'2026-04-22 09:30',name:'净香菇',type:'存入',before:0.0,after:9.0,op:'王厨师',order:'JC-2026-0422-003',remark:'首次入柜'}
]
},
3:{ /* A1-04 · 净西兰花 */
cabinet:'A区智能柜-1号',slot:'A1-04',
rows:[
{time:'2026-04-22 15:30',name:'净西兰花',type:'取用',before:9.0,after:8.5,op:'李主管',order:'JC-2026-0422-020',remark:'做西兰花炒虾仁'},
{time:'2026-04-22 13:10',name:'净西兰花',type:'取用',before:9.5,after:9.0,op:'李主管',order:'JC-2026-0422-020',remark:'做西兰花炒虾仁'},
{time:'2026-04-22 10:50',name:'净西兰花',type:'取用',before:9.8,after:9.5,op:'张厨师',order:'JC-2026-0422-019',remark:'做凉拌西兰花'},
{time:'2026-04-22 09:40',name:'净西兰花',type:'存入',before:0.0,after:10.0,op:'张厨师',order:'JC-2026-0422-004',remark:'首次入柜'}
]
},
4:{ /* A1-05 · 宫保鸡丁(餐品包) */
cabinet:'A区智能柜-1号',slot:'A1-05',
rows:[
{time:'2026-04-22 16:30',name:'宫保鸡丁',type:'取用',before:5.7,after:5.4,op:'李主管',order:'—',remark:'加餐补料'},
{time:'2026-04-22 14:50',name:'宫保鸡丁',type:'取用',before:5.9,after:5.7,op:'李主管',order:'—',remark:'加餐补料'},
{time:'2026-04-22 12:40',name:'宫保鸡丁',type:'取用',before:5.95,after:5.9,op:'王厨师',order:'—',remark:'出餐装盘'},
{time:'2026-04-22 10:20',name:'宫保鸡丁',type:'存入',before:0.0,after:6.0,op:'李主管',order:'JC-2026-0422-005',remark:'烹制完入柜'}
]
},
5:{ /* A1-06 · 净白菜(已过期,9 条用于演示分页) */
cabinet:'A区智能柜-1号',slot:'A1-06',
rows:[
{time:'2026-04-22 11:10',name:'净白菜',type:'取用',before:18.7,after:18.0,op:'王厨师',order:'JC-2026-0422-022',remark:'做白菜炖豆腐'},
{time:'2026-04-22 10:00',name:'净白菜',type:'取用',before:20.5,after:18.7,op:'王厨师',order:'JC-2026-0422-022',remark:'做醋溜白菜'},
{time:'2026-04-21 17:30',name:'净白菜',type:'取用',before:21.2,after:20.5,op:'李主管',order:'JC-2026-0422-021',remark:'晚餐备菜'},
{time:'2026-04-21 11:50',name:'净白菜',type:'取用',before:21.8,after:21.2,op:'张厨师',order:'JC-2026-0422-021',remark:'午餐备菜'},
{time:'2026-04-20 14:00',name:'净白菜',type:'取用',before:22.0,after:21.8,op:'张厨师',order:'—',remark:'试菜取样'},
{time:'2026-04-20 09:30',name:'净白菜',type:'取用',before:22.5,after:22.0,op:'张厨师',order:'—',remark:'试菜取样'},
{time:'2026-04-19 16:20',name:'净白菜',type:'取用',before:22.8,after:22.5,op:'李主管',order:'—',remark:'冷库调整'},
{time:'2026-04-19 10:00',name:'净白菜',type:'取用',before:23.0,after:22.8,op:'李主管',order:'—',remark:'冷库调整'},
{time:'2026-04-18 14:30',name:'净白菜',type:'存入',before:0.0,after:23.0,op:'王厨师',order:'JC-2026-0422-006',remark:'首次入柜'}
]
}
};
var HISTORY_PAGE_SIZE=5;
var HISTORY_CURRENT={idx:0,page:1}; /* 当前抽屉查看的格口与页码 */
function showHistory(idx){
HISTORY_CURRENT={idx:idx,page:1};
var d=HISTORY_DATA[idx];
if(!d){showToast('暂无历史记录');return;}
var cinCount=d.rows.filter(function(r){return r.type==='存入';}).length;
var coutCount=d.rows.filter(function(r){return r.type==='取用';}).length;
/* 摘要条(不展示当前菜品,只体现柜子+格口+累计存取次数) */
var summary='<div class="history-summary">'
+'<div class="history-summary__item"><span class="label">所属柜:</span><strong>'+d.cabinet+'</strong></div>'
+'<div class="history-summary__item"><span class="label">格口:</span><strong>'+d.slot+'</strong></div>'
+'<div class="history-summary__item"><span class="label">累计存入:</span><span class="history-summary__count in">'+cinCount+' 次</span></div>'
+'<div class="history-summary__item"><span class="label">累计取用:</span><span class="history-summary__count out">'+coutCount+' 次</span></div>'
+'</div>';
HistoryModal.open('格口历史记录 — '+d.slot, summary+'<div id="historyTableWrap"></div>', '<div id="historyFooterWrap"></div>');
renderHistoryPage();
}
/* 渲染当前页的历史表 + 分页;rows 按 time 倒序展示(最新在前) */
function renderHistoryPage(){
var wrap=document.getElementById('historyTableWrap');
var footer=document.getElementById('historyFooterWrap');
if(!wrap)return;
var d=HISTORY_DATA[HISTORY_CURRENT.idx];
/* 时间倒序:复制一份再排,避免污染原数据 */
var sortedRows=d.rows.slice().sort(function(a,b){
return b.time.localeCompare(a.time);
});
var total=sortedRows.length;
var pageSize=HISTORY_PAGE_SIZE;
var totalPages=Math.max(1,Math.ceil(total/pageSize));
if(HISTORY_CURRENT.page>totalPages)HISTORY_CURRENT.page=totalPages;
if(HISTORY_CURRENT.page<1)HISTORY_CURRENT.page=1;
var start=(HISTORY_CURRENT.page-1)*pageSize;
var pageRows=sortedRows.slice(start,start+pageSize);
var html='<div style="overflow-x:auto"><table class="history-table"><thead><tr>'
+'<th>操作时间</th><th>净菜名称</th><th>操作类型</th><th>操作前(kg)</th><th>操作后(kg)</th><th>余量变化</th><th>操作人</th><th>关联订单</th><th>备注</th>'
+'</tr></thead><tbody>';
pageRows.forEach(function(r){
var delta=Math.abs(r.after-r.before).toFixed(1);
var isPos=r.type==='存入';
var deltaHtml='<span class="history-delta '+(isPos?'pos':'neg')+'">'+(isPos?'+':'-')+delta+' '+(isPos?'↑':'↓')+'</span>';
var typeHtml=r.type==='存入'?'<span class="tag tag-blue">存入</span>':'<span class="tag tag-orange">取用</span>';
var orderHtml=r.order==='—'?'—':'<a class="link" href="prod-clean-order.html">'+r.order+'</a>';
html+='<tr><td>'+r.time+'</td><td>'+r.name+'</td><td>'+typeHtml+'</td><td>'+r.before.toFixed(1)+'</td><td>'+r.after.toFixed(1)+'</td><td>'+deltaHtml+'</td><td>'+r.op+'</td><td>'+orderHtml+'</td><td>'+(r.remark||'—')+'</td></tr>';
});
html+='</tbody></table></div>';
wrap.innerHTML=html;
/* 分页条放到 footer 区,确保即使表格很长也固定可见 */
if(footer){
var pgHtml='<div class="history-pagination"><span class="history-pagination__total">共 '+total+' 条</span>';
pgHtml+='<button class="page-btn" onclick="goHistoryPage('+(HISTORY_CURRENT.page-1)+')" '+(HISTORY_CURRENT.page<=1?'disabled style="opacity:.4;cursor:not-allowed"':'')+'></button>';
for(var i=1;i<=totalPages;i++){
pgHtml+='<button class="page-btn'+(i===HISTORY_CURRENT.page?' active':'')+'" onclick="goHistoryPage('+i+')">'+i+'</button>';
}
pgHtml+='<button class="page-btn" onclick="goHistoryPage('+(HISTORY_CURRENT.page+1)+')" '+(HISTORY_CURRENT.page>=totalPages?'disabled style="opacity:.4;cursor:not-allowed"':'')+'></button>';
pgHtml+='</div>';
footer.innerHTML=pgHtml;
}
}
function goHistoryPage(p){
HISTORY_CURRENT.page=p;
renderHistoryPage();
}
</script>
</body>
</html>
+116 -5
View File
@@ -116,6 +116,27 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
.drawer-field{display:flex;flex-direction:column;gap:2px;}
.drawer-field__label{font-size:var(--font-xs);color:var(--text-placeholder);}
.drawer-field__value{font-size:var(--font-sm);color:var(--text-primary);font-weight:500;min-height:22px;}
/* 上餐重量单元格 - 蓝色可点击 */
.serve-weight{display:inline-flex;align-items:center;gap:6px;cursor:pointer;color:var(--primary);font-size:var(--font-sm);}
.serve-weight:hover{color:#40a9ff;}
/* 上餐记录弹窗 - 时间线 */
.serve-log__summary{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px;}
.serve-log__summary-item{background:#FAFBFC;border:1px solid #F0F0F0;border-radius:6px;padding:12px 16px;}
.serve-log__summary-label{font-size:var(--font-xs);color:var(--text-placeholder);margin-bottom:4px;}
.serve-log__summary-value{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);}
.serve-log__summary-value .unit{font-size:var(--font-xs);color:var(--text-secondary);font-weight:400;margin-left:2px;}
.serve-log__list{position:relative;padding-left:24px;}
.serve-log__list::before{content:'';position:absolute;left:7px;top:8px;bottom:8px;width:2px;background:#E8E8E8;}
.serve-log__item{position:relative;padding:10px 0 10px 8px;}
.serve-log__item::before{content:'';position:absolute;left:-21px;top:14px;width:12px;height:12px;border-radius:50%;background:#fff;border:2px solid var(--primary);}
.serve-log__item--first::before{border-color:var(--success);background:var(--success);}
.serve-log__item--extra::before{border-color:var(--warning);background:var(--warning);}
.serve-log__head{display:flex;align-items:center;gap:8px;margin-bottom:6px;}
.serve-log__time{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);}
.serve-log__fields{display:grid;grid-template-columns:repeat(4,1fr);gap:8px 16px;padding:8px 12px;background:#FAFBFC;border-radius:6px;}
.serve-log__field-label{font-size:var(--font-xs);color:var(--text-placeholder);}
.serve-log__field-value{font-size:var(--font-sm);color:var(--text-primary);font-weight:500;margin-top:2px;}
.serve-log__field-value--highlight{color:var(--warning);}
</style>
</head>
<body>
@@ -160,11 +181,11 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<table>
<thead><tr><th>送达餐线</th><th>餐次</th><th>送达设备(秤台)</th><th>识别方式</th><th>菜品名称</th><th>当前余量(kg)</th><th>上餐重量(kg)</th><th>出锅时间</th><th>送达餐线时间</th><th>上餐状态</th><th>烹制任务号</th><th>出锅温度(℃)</th><th>厨师</th><th>操作</th></tr></thead>
<tbody>
<tr><td>A区中餐线</td><td><span class="tag tag-blue">午餐</span></td><td>00025号秤</td><td><span class="tag tag-green">🤖 自动识别</span></td><td>宫保鸡丁</td><td>12.5</td><td>38.2</td><td>11:18</td><td>11:30</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-001</td><td>82</td><td>张厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>A区中餐线</td><td><span class="tag tag-blue">午餐</span></td><td>00025号秤</td><td><span class="tag tag-green">🤖 自动识别</span></td><td>番茄炒蛋</td><td>8.3</td><td>22.6</td><td>11:20</td><td>11:30</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-002</td><td>78</td><td>李厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>B区中餐线</td><td><span class="tag tag-blue">午餐</span></td><td>00027号秤</td><td><span class="tag tag-blue">👤 人工选择</span></td><td>红烧肉</td><td>1.2</td><td>42.6</td><td>11:22</td><td>11:38</td><td><span class="tag tag-red">余量不足</span></td><td>COOK-003</td><td>86</td><td>王厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>A区早餐线</td><td><span class="tag tag-gray">早餐</span></td><td>00022号秤</td><td><span class="tag tag-green">🤖 自动识别</span></td><td>小米粥</td><td>0</td><td>12.6</td><td>07:20</td><td>07:28</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-006</td><td>75</td><td>赵厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>B区早餐线</td><td><span class="tag tag-gray">早餐</span></td><td>00023号秤</td><td><span class="tag tag-blue">👤 人工选择</span></td><td>燕麦粥</td><td>2.1</td><td>8.4</td><td>07:22</td><td>07:35</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-007</td><td>72</td><td>陈厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>A区中餐线</td><td><span class="tag tag-blue">午餐</span></td><td>00025号秤</td><td><span class="tag tag-green">🤖 自动识别</span></td><td>宫保鸡丁</td><td>12.5</td><td><span class="serve-weight" onclick="openServeLogModal(0)">38.2</span></td><td>11:18</td><td>11:30</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-001</td><td>82</td><td>张厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>A区中餐线</td><td><span class="tag tag-blue">午餐</span></td><td>00025号秤</td><td><span class="tag tag-green">🤖 自动识别</span></td><td>番茄炒蛋</td><td>8.3</td><td><span class="serve-weight" onclick="openServeLogModal(1)">22.6</span></td><td>11:20</td><td>11:30</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-002</td><td>78</td><td>李厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>B区中餐线</td><td><span class="tag tag-blue">午餐</span></td><td>00027号秤</td><td><span class="tag tag-blue">👤 人工选择</span></td><td>红烧肉</td><td>1.2</td><td><span class="serve-weight" onclick="openServeLogModal(2)">42.6</span></td><td>11:22</td><td>11:38</td><td><span class="tag tag-red">余量不足</span></td><td>COOK-003</td><td>86</td><td>王厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>A区早餐线</td><td><span class="tag tag-gray">早餐</span></td><td>00022号秤</td><td><span class="tag tag-green">🤖 自动识别</span></td><td>小米粥</td><td>0</td><td><span class="serve-weight" onclick="openServeLogModal(3)">12.6</span></td><td>07:20</td><td>07:28</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-006</td><td>75</td><td>赵厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>B区早餐线</td><td><span class="tag tag-gray">早餐</span></td><td>00023号秤</td><td><span class="tag tag-blue">👤 人工选择</span></td><td>燕麦粥</td><td>2.1</td><td><span class="serve-weight" onclick="openServeLogModal(4)">8.4</span></td><td>07:22</td><td>07:35</td><td><span class="tag tag-green">上餐完成</span></td><td>COOK-007</td><td>72</td><td>陈厨师</td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
<tr><td>C区快餐线</td><td><span class="tag tag-blue">午餐</span></td><td>00028号秤</td><td><span class="tag tag-blue">👤 人工选择</span></td><td>蒜蓉菠菜</td><td></td><td></td><td></td><td></td><td><span class="tag tag-gray">待上餐</span></td><td>COOK-005</td><td></td><td></td><td class="td-actions"><a class="link">食材溯源链路</a> <a class="link">查看视频</a> <span class="action-more"><a class="action-more__trigger">更多 ▾</a><div class="action-more__menu"><a class="action-more__item" onclick="showDetail(this)">详情</a><a class="action-more__item">编辑</a><a class="action-more__item action-more__item--danger">删除</a></div></span></td></tr>
</tbody>
</table>
@@ -248,6 +269,38 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
</div>
</div>
<!-- 上餐记录弹窗(含上餐+加餐明细) -->
<div class="modal-overlay" id="serveLogModal" style="display:none;" onclick="if(event.target===this)closeServeLogModal()">
<div class="modal" style="width:680px;">
<div class="modal-header">
<span class="modal-title" id="serveLogTitle">上餐记录 — 宫保鸡丁</span>
<span class="modal-close" onclick="closeServeLogModal()">&times;</span>
</div>
<div class="modal-body">
<!-- 汇总卡片 -->
<div class="serve-log__summary">
<div class="serve-log__summary-item">
<div class="serve-log__summary-label">上餐次数</div>
<div class="serve-log__summary-value"><span id="serveLogCount">3</span><span class="unit"></span></div>
</div>
<div class="serve-log__summary-item">
<div class="serve-log__summary-label">累计上餐重量</div>
<div class="serve-log__summary-value"><span id="serveLogTotal">55.8</span><span class="unit">kg</span></div>
</div>
<div class="serve-log__summary-item">
<div class="serve-log__summary-label">当前余量</div>
<div class="serve-log__summary-value"><span id="serveLogRemain">12.5</span><span class="unit">kg</span></div>
</div>
</div>
<!-- 上餐+加餐时间线 -->
<div class="serve-log__list" id="serveLogList"></div>
</div>
<div class="modal-footer">
<button class="btn btn-default" onclick="closeServeLogModal()">关闭</button>
</div>
</div>
</div>
<script>
/* 日期默认选中当天 */
(function(){var d=new Date();var y=d.getFullYear();var m=String(d.getMonth()+1).padStart(2,'0');var day=String(d.getDate()).padStart(2,'0');document.getElementById('filterDate').value=y+'-'+m+'-'+day;})();
@@ -317,6 +370,64 @@ function showDetail(el){
var body='<div class="drawer-section"><div class="drawer-section__title">上餐信息</div><div class="drawer-fields">'+fields+'</div></div>';
Drawer.open('上餐详情 — '+dishName,body);
}
/* ========== 上餐记录弹窗(上餐 + 加餐明细) ========== */
// 索引与表格行顺序对齐: 0=宫保鸡丁 1=番茄炒蛋 2=红烧肉 3=小米粥 4=燕麦粥
// type: first=初次上餐(绿), extra=加餐(橙)
// remainBefore: 操作前余量(kg), weight: 本次上餐/加餐重量(kg), remainAfter: 操作后余量(kg)
var SERVE_LOGS=[
{dish:'宫保鸡丁',total:55.8,remain:12.5,logs:[
{type:'first',time:'11:30',outTime:'11:18',remainBefore:0,weight:38.2,remainAfter:38.2,operator:'张厨师',recog:'自动识别',device:'00025号秤'},
{type:'extra',time:'12:15',outTime:'12:08',remainBefore:8.6,weight:12.4,remainAfter:21.0,operator:'张厨师',recog:'自动识别',device:'00025号秤'},
{type:'extra',time:'12:48',outTime:'12:41',remainBefore:5.2,weight:5.2,remainAfter:10.4,operator:'李服务员',recog:'人工选择',device:'00025号秤'}
]},
{dish:'番茄炒蛋',total:32.4,remain:8.3,logs:[
{type:'first',time:'11:30',outTime:'11:20',remainBefore:0,weight:22.6,remainAfter:22.6,operator:'李厨师',recog:'自动识别',device:'00025号秤'},
{type:'extra',time:'12:20',outTime:'12:14',remainBefore:6.8,weight:9.8,remainAfter:16.6,operator:'李厨师',recog:'自动识别',device:'00025号秤'}
]},
{dish:'红烧肉',total:42.6,remain:1.2,logs:[
{type:'first',time:'11:38',outTime:'11:22',remainBefore:0,weight:42.6,remainAfter:42.6,operator:'王厨师',recog:'人工选择',device:'00027号秤'}
]},
{dish:'小米粥',total:12.6,remain:0,logs:[
{type:'first',time:'07:28',outTime:'07:20',remainBefore:0,weight:12.6,remainAfter:12.6,operator:'赵厨师',recog:'自动识别',device:'00022号秤'}
]},
{dish:'燕麦粥',total:8.4,remain:2.1,logs:[
{type:'first',time:'07:35',outTime:'07:22',remainBefore:0,weight:8.4,remainAfter:8.4,operator:'陈厨师',recog:'人工选择',device:'00023号秤'}
]}
];
// 打开上餐记录弹窗,渲染时间线
function openServeLogModal(idx){
var data=SERVE_LOGS[idx];
if(!data)return;
document.getElementById('serveLogTitle').textContent='上餐记录 — '+data.dish;
document.getElementById('serveLogCount').textContent=data.logs.length;
document.getElementById('serveLogTotal').textContent=data.total.toFixed(1);
document.getElementById('serveLogRemain').textContent=data.remain.toFixed(1);
var html='';
data.logs.forEach(function(log,i){
var isFirst=log.type==='first';
var itemClass=isFirst?'serve-log__item serve-log__item--first':'serve-log__item serve-log__item--extra';
var tagClass=isFirst?'tag tag-green':'tag tag-orange';
var tagText=isFirst?'初次上餐':'加餐';
html+='<div class="'+itemClass+'">';
html+='<div class="serve-log__head"><span class="'+tagClass+'">'+tagText+'</span><span class="serve-log__time">'+log.time+'</span>';
if(!isFirst)html+='<span style="font-size:var(--font-xs);color:var(--text-placeholder);">第 '+i+' 次加餐</span>';
html+='</div>';
html+='<div class="serve-log__fields">';
html+='<div><div class="serve-log__field-label">'+(isFirst?'出锅时间':'加餐出锅时间')+'</div><div class="serve-log__field-value">'+log.outTime+'</div></div>';
html+='<div><div class="serve-log__field-label">操作前余量(kg)</div><div class="serve-log__field-value'+(isFirst?'':' serve-log__field-value--highlight')+'">'+log.remainBefore.toFixed(1)+'</div></div>';
html+='<div><div class="serve-log__field-label">'+(isFirst?'上餐重量(kg)':'加餐重量(kg)')+'</div><div class="serve-log__field-value">'+log.weight.toFixed(1)+'</div></div>';
html+='<div><div class="serve-log__field-label">操作后余量(kg)</div><div class="serve-log__field-value">'+log.remainAfter.toFixed(1)+'</div></div>';
html+='<div><div class="serve-log__field-label">操作人</div><div class="serve-log__field-value">'+log.operator+'</div></div>';
html+='<div><div class="serve-log__field-label">识别方式</div><div class="serve-log__field-value">'+log.recog+'</div></div>';
html+='<div><div class="serve-log__field-label">秤台设备</div><div class="serve-log__field-value">'+log.device+'</div></div>';
html+='</div>';
html+='</div>';
});
document.getElementById('serveLogList').innerHTML=html;
document.getElementById('serveLogModal').style.display='flex';
}
function closeServeLogModal(){document.getElementById('serveLogModal').style.display='none';}
</script>
</body>
</html>
+360 -24
View File
@@ -211,7 +211,7 @@ function toggleSubgroup(header) {
<div class="table-card__content">
<table id="extOrderTable">
<thead><tr>
<th style="width:36px;"></th><th>订单号</th><th>客户名称</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总计量(kg)</th><th>订单金额</th><th>下单时间</th><th>预定配送</th><th>物流单号</th><th>订单状态</th><th>操作</th>
<th style="width:36px;"></th><th>订单号</th><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="extOrderTableBody"></tbody>
</table>
@@ -260,16 +260,92 @@ function toggleSubgroup(header) {
</div>
</div>
<!-- 发货确认弹窗 -->
<!-- 弹窗:立即发货 — 配送方式选择 -->
<div class="modal-mask" id="shipModal">
<div class="modal" style="width:480px;">
<div class="modal__header"><span class="modal__title">确认发货</span><button class="modal__close" onclick="closeModal('shipModal')">&times;</button></div>
<div class="modal" style="width:560px;">
<div class="modal__header"><span class="modal__title">立即发货 · 选择配送方式</span><button class="modal__close" onclick="closeModal('shipModal')">&times;</button></div>
<div class="modal__body">
<p style="font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:12px;" id="shipInfo"></p>
<div class="form-grid" style="grid-template-columns:1fr;">
<div class="form-group"><label class="form-label">物流公司<span class="required">*</span></label>
<select class="form-control" id="shipCompany"><option value="">请选择</option><option>顺丰速运</option><option>京东物流</option><option>德邦快递</option><option>自有车队配送</option></select></div>
<div class="form-group"><label class="form-label">物流单号<span class="required">*</span></label><input class="form-control" id="shipTrackNo" placeholder="请输入物流单号"></div>
<div style="background:var(--primary-light);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:20px;font-size:var(--font-sm);color:var(--primary);">
<span>订单号:<strong id="shipOrderNoExt"></strong></span>
<span style="margin-left:12px;">客户:<strong id="shipCustomerExt"></strong></span>
</div>
<!-- 配送方式三选一 -->
<div style="margin-bottom:16px;">
<div style="font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:10px;">配送方式 <span style="color:var(--danger);">*</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;">
<label class="ship-mode-card" data-mode="self_dispatch">
<input type="radio" name="shipMode" value="self_dispatch" onchange="switchShipMode('self_dispatch')" style="display:none;">
<div class="ship-mode-card__inner">
<div class="ship-mode-card__icon">🛵</div>
<div class="ship-mode-card__title">自营物流·派单</div>
<div class="ship-mode-card__desc">指定配送员<br>走配送 H5 派单流</div>
</div>
</label>
<label class="ship-mode-card" data-mode="self_grab">
<input type="radio" name="shipMode" value="self_grab" onchange="switchShipMode('self_grab')" style="display:none;">
<div class="ship-mode-card__inner">
<div class="ship-mode-card__icon">🔥</div>
<div class="ship-mode-card__title">自营物流·抢单</div>
<div class="ship-mode-card__desc">订单进抢单大厅<br>由配送员自取</div>
</div>
</label>
<label class="ship-mode-card" data-mode="third_party">
<input type="radio" name="shipMode" value="third_party" onchange="switchShipMode('third_party')" style="display:none;">
<div class="ship-mode-card__inner">
<div class="ship-mode-card__icon">🚚</div>
<div class="ship-mode-card__title">三方物流</div>
<div class="ship-mode-card__desc">仅录入承运方与运单号<br>不进入 H5</div>
</div>
</label>
</div>
</div>
<!-- 子面板:自营派单 -->
<div id="shipPanel-self_dispatch" class="ship-subpanel" style="display:none;">
<div class="form-grid" style="grid-template-columns:1fr 1fr;">
<div class="form-group"><label class="form-label">配送员 <span class="required">*</span></label>
<select class="form-control" id="ship-rider-sd"><option value="">请选择</option>
<option value="李配送|138****1234|RDR-1024">李配送 · RDR-1024 · 138****1234</option>
<option value="王配送|139****5678|RDR-1025">王配送 · RDR-1025 · 139****5678</option>
<option value="陈师傅|138****6688|RDR-2026">陈师傅 · RDR-2026 · 138****6688</option>
</select>
</div>
<div class="form-group"><label class="form-label">收货码(系统生成)</label>
<input class="form-control" id="ship-code-sd" readonly placeholder="提交后系统自动生成 6 位收货码">
</div>
</div>
<div style="background:#FFF7E6;border-radius:var(--radius-md);padding:10px 14px;margin-top:12px;font-size:var(--font-xs);color:#8C7B3A;line-height:1.6;">
💡 订单将作为派单任务推送给所选配送员,配送员需在 30 秒内响应接听。系统会在客户手机生成 6 位收货码,配送员送达时索取并输入后才能完成签收。
</div>
</div>
<!-- 子面板:自营抢单 -->
<div id="shipPanel-self_grab" class="ship-subpanel" style="display:none;">
<div style="background:#F9F0FF;border-radius:var(--radius-md);padding:14px;font-size:var(--font-sm);color:#722ED1;">
<div style="font-weight:600;margin-bottom:6px;">🔥 抢单模式说明</div>
<div style="font-size:var(--font-xs);line-height:1.6;">订单将发布到「抢单大厅」(配送员 H5 应用),所有在线配送员可主动抢接。首个抢接的配送员自动成为承运人。</div>
</div>
<div class="form-grid" style="grid-template-columns:1fr; margin-top:12px;">
<div class="form-group"><label class="form-label">收货码(系统生成)</label>
<input class="form-control" id="ship-code-sg" readonly placeholder="提交后系统自动生成 6 位收货码">
</div>
</div>
</div>
<!-- 子面板:三方物流 -->
<div id="shipPanel-third_party" class="ship-subpanel" style="display:none;">
<div class="form-grid" style="grid-template-columns:1fr 1fr;">
<div class="form-group"><label class="form-label">承运方 <span class="required">*</span></label>
<select class="form-control" id="ship-carrier"><option value="">请选择</option>
<option>顺丰速运</option><option>京东物流</option><option>德邦快递</option>
<option>圆通速递</option><option>自有车队</option><option>其他</option>
</select>
</div>
<div class="form-group"><label class="form-label">运单号 <span class="required">*</span></label><input class="form-control" id="ship-tracking" placeholder="请输入运单号"></div>
</div>
<div style="background:#FFF1F0;border-radius:var(--radius-md);padding:10px 14px;margin-top:12px;font-size:var(--font-xs);color:#8C3A3A;line-height:1.6;">
⚠️ 三方物流订单不进入配送 H5,客户收货码不生效,仅作后台物流登记与跟进。客户签收由承运方系统回调或后台手动确认。
</div>
</div>
</div>
<div class="modal__footer">
@@ -278,6 +354,44 @@ function toggleSubgroup(header) {
</div>
</div>
</div>
<style>
.ship-mode-card{cursor:pointer;display:block;}
.ship-mode-card__inner{border:1.5px solid var(--border);border-radius:var(--radius-md);padding:14px 10px;text-align:center;background:#fff;transition:all .2s;}
.ship-mode-card:hover .ship-mode-card__inner{border-color:var(--primary);background:#fff9f5;}
.ship-mode-card input:checked + .ship-mode-card__inner{border-color:var(--primary);background:var(--primary-light);box-shadow:0 0 0 3px rgba(24,144,255,0.1);}
.ship-mode-card__icon{font-size:28px;margin-bottom:6px;}
.ship-mode-card__title{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:4px;}
.ship-mode-card__desc{font-size:var(--font-xs);color:var(--text-secondary);line-height:1.5;}
.ship-subpanel{border-top:1px dashed var(--border);padding-top:16px;margin-top:4px;}
/* 物流徽标单元(列表悬浮交互) */
.logistics-cell{display:inline-flex;align-items:center;transition:transform .15s;}
.logistics-cell:hover{transform:scale(1.05);}
.logistics-cell .tag{cursor:pointer;}
/* 配送进度时间轴(横向 4 节点) */
.logistics-timeline{display:flex;align-items:flex-start;gap:0;padding:8px 4px 4px;}
.timeline-node{display:flex;flex-direction:column;align-items:center;flex-shrink:0;position:relative;flex:1;max-width:110px;}
.timeline-dot{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:var(--font-xs);font-weight:700;z-index:2;transition:all .3s;}
.timeline-node--done .timeline-dot{background:var(--success);color:#fff;border:2px solid var(--success);}
.timeline-node--current .timeline-dot{background:#fff;color:var(--primary);border:2px solid var(--primary);animation:timelinePulse 1.6s ease-in-out infinite;}
.timeline-node--pending .timeline-dot{background:#fff;color:var(--text-placeholder);border:2px dashed var(--border);}
.timeline-content{text-align:center;margin-top:6px;}
.timeline-label{font-size:var(--font-xs);color:var(--text-secondary);font-weight:500;}
.timeline-time{font-size:10px;color:var(--text-placeholder);margin-top:2px;line-height:1.3;white-space:nowrap;}
.timeline-line{position:absolute;top:14px;left:50%;width:100%;height:2px;background:var(--border);z-index:1;}
.timeline-line--done{background:var(--success);}
@keyframes timelinePulse{0%,100%{box-shadow:0 0 0 0 rgba(24,144,255,0.45);}50%{box-shadow:0 0 0 8px rgba(24,144,255,0);}}
/* 物流详情弹窗 */
.logistics-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);}
.logistics-modal-title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);}
.logistics-modal-body{padding:20px 24px;max-height:70vh;overflow-y:auto;}
.logistics-modal-section{margin-bottom:20px;}
.logistics-modal-section:last-child{margin-bottom:0;}
.logistics-modal-section__title{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:10px;display:flex;align-items:center;gap:6px;}
.logistics-modal-info{display:grid;grid-template-columns:1fr 1fr;gap:8px 20px;font-size:var(--font-sm);}
.logistics-modal-info__item{display:flex;gap:8px;}
.logistics-modal-info__label{color:var(--text-placeholder);flex-shrink:0;}
.logistics-modal-info__value{color:var(--text-primary);font-weight:500;}
</style>
<!-- 备货完成弹窗 -->
<div class="modal-mask" id="readyModal">
@@ -300,6 +414,17 @@ function toggleSubgroup(header) {
</div>
</div>
<!-- 物流详情弹窗 -->
<div class="modal-mask" id="modal-logistics">
<div class="modal" style="width:500px;">
<div class="logistics-modal-header">
<span class="logistics-modal-title">🚚 物流详情 — <span id="logisticsOrderNo"></span></span>
<button class="modal__close" onclick="closeModal('modal-logistics')"></button>
</div>
<div class="logistics-modal-body" id="logisticsModalBody"></div>
</div>
</div>
<script>
/* ========== 弹窗 ========== */
function openModal(id){if(id==='createOrderModal')resetExtForm();document.getElementById(id).classList.add('show');}
@@ -379,33 +504,43 @@ var dishIngsExt={
};
var extOrders=[
{orderNo:'EXT-001',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 08:00',deliverTime:'2026-04-22 16:00',logistics:'SF1234567890',
{orderNo:'EXT-001',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 08:00',deliverTime:'2026-04-22 16:00',
logistics:{type:'third_party',carrier:'顺丰速运',trackingNo:'SF1234567890',dispatchStatus:'completed',receiveCode:null,receiveStatus:'verified',
t1_ship:'2026-04-22 08:30',t2_accept:'2026-04-22 08:32',t3_pick:'2026-04-22 08:45',t4_deliver:'2026-04-22 09:05'},
lines:[
{id:'L1',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'5kg/箱',qty:10,weight:50,price:3.5,subtotal:175,status:'已完成'},
{id:'L2',ncType:'nc',ingredient:'土豆',ncName:'土豆块',spec:'5kg/箱',qty:6,weight:30,price:3.5,subtotal:105,status:'已完成'}
]},
{orderNo:'EXT-002',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 09:30',deliverTime:'2026-04-22 14:00',logistics:'SF9876543210',
{orderNo:'EXT-002',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 09:30',deliverTime:'2026-04-22 14:00',
logistics:{type:'self_dispatch',riderName:'陈师傅',riderPhone:'138****6688',riderId:'RDR-2026',dispatchStatus:'delivering',receiveCode:'347820',receiveStatus:'pending',
t1_ship:'2026-04-22 10:00',t2_accept:'2026-04-22 10:02',t3_pick:'2026-04-22 10:15',t4_deliver:null},
lines:[
{id:'L3',ncType:'nc',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:10,weight:10,price:28,subtotal:280,status:'配送中'},
{id:'L4',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:40,weight:20,price:12,subtotal:240,status:'配送中',needSeasoning:true}
]},
{orderNo:'EXT-003',ncType:'nc',customer:'学校食堂',orderTime:'2026-04-22 10:00',deliverTime:'2026-04-23 09:00',logistics:'—',
{orderNo:'EXT-003',ncType:'nc',customer:'学校食堂',orderTime:'2026-04-22 10:00',deliverTime:'2026-04-23 09:00',
logistics:null,
lines:[
{id:'L5',ncType:'nc',ingredient:'鸡肉',ncName:'鸡肉块',spec:'1kg/箱',qty:60,weight:60,price:20,subtotal:1200,status:'待配送'},
{id:'L6',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'5kg/箱',qty:8,weight:40,price:4.5,subtotal:180,status:'待配送'}
]},
{orderNo:'EXT-004',ncType:'nc',customer:'职工家属区团购',orderTime:'2026-04-22 10:30',deliverTime:'2026-04-23 16:00',logistics:'—',
{orderNo:'EXT-004',ncType:'nc',customer:'职工家属区团购',orderTime:'2026-04-22 10:30',deliverTime:'2026-04-23 16:00',
logistics:null,
lines:[
{id:'L7',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g',qty:20,weight:12,price:15,subtotal:180,status:'备货中',needSeasoning:false},
{id:'L8',ncType:'cp',foodName:'蒜蓉菠菜',spec:'标准份(400g',qty:30,weight:12,price:8,subtotal:96,status:'备货中',needSeasoning:true},
{id:'L9',ncType:'nc',ingredient:'胡萝卜',ncName:'胡萝卜条',spec:'2kg/箱',qty:15,weight:30,price:4,subtotal:120,status:'备货中'}
]},
{orderNo:'EXT-005',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 11:00',deliverTime:'2026-04-23 10:00',logistics:'—',
{orderNo:'EXT-005',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 11:00',deliverTime:'2026-04-23 10:00',
logistics:{type:'self_grab',riderName:'张师傅',riderPhone:'137****8888',riderId:'RDR-2026',dispatchStatus:'delivering',receiveCode:'915374',receiveStatus:'pending',
t1_ship:'2026-04-22 11:30',t2_accept:'2026-04-22 11:35',t3_pick:'2026-04-22 11:50',t4_deliver:null},
lines:[
{id:'L10',ncType:'nc',ingredient:'菠菜',ncName:'菠菜段',spec:'3kg/箱',qty:12,weight:36,price:5,subtotal:180,status:'已完成'},
{id:'L11',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g',qty:25,weight:7.5,price:18,subtotal:135,status:'配送中',needSeasoning:true}
]},
{orderNo:'EXT-006',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 12:00',deliverTime:'2026-04-23 12:00',logistics:'SF5566778899',
{orderNo:'EXT-006',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 12:00',deliverTime:'2026-04-23 12:00',
logistics:{type:'third_party',carrier:'京东物流',trackingNo:'JD5566778899',dispatchStatus:'completed',receiveCode:null,receiveStatus:'verified',
t1_ship:'2026-04-22 13:00',t2_accept:'2026-04-22 13:05',t3_pick:'2026-04-22 13:30',t4_deliver:'2026-04-22 14:00'},
lines:[
{id:'L12',ncType:'nc',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:15,weight:15,price:28,subtotal:420,status:'已完成'},
{id:'L13',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:50,weight:25,price:12,subtotal:300,status:'已完成',needSeasoning:true},
@@ -417,6 +552,148 @@ function calcOrderTotal(o){return o.lines.reduce(function(s,l){return s+(parseFl
/* 金额格式化:¥1,234.5 */
function formatMoney(n){var v=parseFloat(n)||0;return '¥'+v.toLocaleString('zh-CN',{minimumFractionDigits:0,maximumFractionDigits:2});}
/* ========== 物流信息共享渲染(与 internal/ prod-clean-order 一致) ========== */
function logisticsTypeTag(type){
var m={
'self_dispatch':'<span class="tag tag-blue">🛵 自营·派单</span>',
'self_grab':'<span class="tag tag-purple">🔥 自营·抢单</span>',
'third_party':'<span class="tag tag-orange">🚚 三方物流</span>'
};
return m[type]||'<span class="tag tag-gray">— 未发货</span>';
}
function dispatchStatusTag(s){
var m={
'pending':'<span class="tag tag-gray">⏳ 待响应</span>',
'accepted':'<span class="tag tag-blue">✓ 已接单</span>',
'picking':'<span class="tag tag-orange">📦 取货中</span>',
'delivering':'<span class="tag tag-purple">🛵 配送中</span>',
'completed':'<span class="tag tag-green">✓ 已送达</span>',
'rejected':'<span class="tag tag-red">✗ 已拒单</span>',
'timeout':'<span class="tag tag-gray">⏰ 超时未响应</span>'
};
return m[s]||'';
}
function receiveStatusTag(s){
var m={
'pending':'<span class="tag tag-gray">未送达</span>',
'verifying':'<span class="tag tag-orange">待验证</span>',
'verified':'<span class="tag tag-green">已签收</span>'
};
return m[s]||'<span class="tag tag-gray">未送达</span>';
}
/* 列表用「物流信息」单元 — 只显示配送方式徽标,点击弹出物流详情 */
function renderLogistics(logistics,orderNo){
if(!logistics){
return '<span style="color:var(--text-placeholder);"></span>';
}
return '<span class="logistics-cell" onclick="event.stopPropagation();showLogisticsModal(\''+orderNo+'\')" style="cursor:pointer;">'
+logisticsTypeTag(logistics.type)
+'</span>';
}
/* 物流详情时间轴:发货 → 接单 → 取货 → 送达 */
function renderLogisticsTimeline(logistics){
var nodes=[
{key:'t1_ship',label:'发货',icon:'📦'},
{key:'t2_accept',label:'接单',icon:'✓'},
{key:'t3_pick',label:'取货',icon:'🛵'},
{key:'t4_deliver',label:'送达',icon:'🏁'}
];
/* 找到第一个时间戳为 null 的节点索引(即当前进行中的节点) */
var currentIdx=-1;
for(var i=0;i<nodes.length;i++){
if(!logistics[nodes[i].key]){
currentIdx=i;break;
}
}
/* 如果全部都有值,则全部完成 */
if(currentIdx===-1)currentIdx=nodes.length;
var h='<div class="logistics-timeline">';
nodes.forEach(function(n,i){
var state='done';
if(i===currentIdx)state='current';
else if(i>currentIdx)state='pending';
h+='<div class="timeline-node timeline-node--'+state+'">';
h+='<div class="timeline-dot">'+(state==='done'?'✓':(state==='current'?'●':n.icon))+'</div>';
h+='<div class="timeline-content">';
h+='<div class="timeline-label">'+n.label+'</div>';
h+='<div class="timeline-time">'+(logistics[n.key]||'—')+'</div>';
h+='</div>';
if(i<nodes.length-1){
var lineState=(i<currentIdx)?'done':'pending';
h+='<div class="timeline-line timeline-line--'+lineState+'"></div>';
}
h+='</div>';
});
h+='</div>';
return h;
}
/* 详情抽屉用「物流信息」单元 — 后台视角不显示收货码 */
function renderLogisticsDetail(logistics){
if(!logistics){
return '<div style="color:var(--text-placeholder);font-size:var(--font-sm);">该订单尚未发起配送,请点击「立即发货」选择配送方式</div>';
}
var html='<div style="display:flex;flex-direction:column;gap:14px;">';
/* 配送方式徽标 */
html+='<div>'+logisticsTypeTag(logistics.type)+'</div>';
/* 承运信息 grid */
if(logistics.type==='self_dispatch'||logistics.type==='self_grab'){
html+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;font-size:var(--font-sm);">';
html+='<div><span style="color:var(--text-placeholder);">配送员:</span> <b>'+logistics.riderName+'</b></div>';
html+='<div><span style="color:var(--text-placeholder);">联系电话:</span> '+logistics.riderPhone+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送员工号:</span> '+logistics.riderId+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送状态:</span> '+dispatchStatusTag(logistics.dispatchStatus)+'</div>';
html+='</div>';
}else if(logistics.type==='third_party'){
html+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;font-size:var(--font-sm);">';
html+='<div><span style="color:var(--text-placeholder);">承运方:</span> <b>'+logistics.carrier+'</b></div>';
html+='<div><span style="color:var(--text-placeholder);">运单号:</span> '+logistics.trackingNo+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送状态:</span> '+dispatchStatusTag(logistics.dispatchStatus)+'</div>';
html+='</div>';
}
/* 配送进度时间轴 */
html+='<div style="margin-top:4px;"><div style="font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:8px;font-weight:600;">📊 配送进度</div>'+renderLogisticsTimeline(logistics)+'</div>';
html+='</div>';
return html;
}
/* 物流详情弹窗:点击列表徽标触发,后台视角不展示收货码 */
function showLogisticsModal(orderNo){
var o=extOrders.find(function(x){return x.orderNo===orderNo;});
if(!o||!o.logistics){alert('该订单尚未发起配送');return;}
var lg=o.logistics;
document.getElementById('logisticsOrderNo').textContent=orderNo;
var h='';
/* Section 1: 配送方式徽标 */
h+='<div class="logistics-modal-section">';
h+='<div class="logistics-modal-section__title">🚚 配送方式</div>';
h+='<div>'+logisticsTypeTag(lg.type)+'</div>';
h+='</div>';
/* Section 2: 承运信息 */
h+='<div class="logistics-modal-section">';
h+='<div class="logistics-modal-section__title">📋 承运信息</div>';
if(lg.type==='self_dispatch'||lg.type==='self_grab'){
h+='<div class="logistics-modal-info">';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">配送员:</span><span class="logistics-modal-info__value">'+lg.riderName+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">联系电话:</span><span class="logistics-modal-info__value">'+lg.riderPhone+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">工号:</span><span class="logistics-modal-info__value">'+lg.riderId+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">状态:</span><span class="logistics-modal-info__value">'+dispatchStatusTag(lg.dispatchStatus)+'</span></div>';
h+='</div>';
}else if(lg.type==='third_party'){
h+='<div class="logistics-modal-info">';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">承运方:</span><span class="logistics-modal-info__value">'+lg.carrier+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">运单号:</span><span class="logistics-modal-info__value">'+lg.trackingNo+'</span></div>';
h+='<div class="logistics-modal-info__item"><span class="logistics-modal-info__label">状态:</span><span class="logistics-modal-info__value">'+dispatchStatusTag(lg.dispatchStatus)+'</span></div>';
h+='</div>';
}
h+='</div>';
/* Section 3: 配送进度时间轴 */
h+='<div class="logistics-modal-section">';
h+='<div class="logistics-modal-section__title">📊 配送进度</div>';
h+=renderLogisticsTimeline(lg);
h+='</div>';
document.getElementById('logisticsModalBody').innerHTML=h;
openModal('modal-logistics');
}
function aggregateStatus(lines){var ss=lines.map(function(l){return l.status;});
if(ss.every(function(s){return s==='已完成';}))return '已完成';
if(ss.some(function(s){return s==='配送中';})&&!ss.some(function(s){return s==='备货中'||s==='待配送';}))return '配送中';
@@ -441,7 +718,7 @@ function renderExtTable(){
h+='<tr class="master-row" data-ext-idx="'+oi+'" onclick="toggleExpandExt(this,'+oi+')">';
h+='<td><span class="expand-arrow" id="earrow'+oi+'"></span></td>';
h+='<td><b>'+o.orderNo+'</b></td><td>'+o.customer+'</td><td>'+ncTypeTag(o.ncType)+'</td><td>'+o.lines.length+'</td><td>'+tQ+'</td><td>'+tW+'</td><td><b>'+formatMoney(calcOrderTotal(o))+'</b></td>';
h+='<td>'+o.orderTime+'</td><td>'+o.deliverTime+'</td><td>'+o.logistics+'</td><td>'+statusTag(agg)+'</td>';
h+='<td>'+o.orderTime+'</td><td>'+statusTag(agg)+'</td><td>'+(o.deliverTime||'—')+'</td><td>'+renderLogistics(o.logistics,o.orderNo)+'</td>';
/* 主行操作按钮:根据聚合状态显示 */
var abtns='';
if(agg==='备货中')abtns='<span class="link" style="color:var(--success);" onclick="event.stopPropagation();orderAction(\''+o.orderNo+'\',\'ready\')">备货完成</span> ';
@@ -480,6 +757,7 @@ function deleteExtOrder(no){if(!confirm('确认删除订单 '+no+' '))return;
/* ========== 订单级操作 ========== */
var _extOp={};
var _shipMode=null;
function orderAction(no,action){
var o=extOrders.find(function(x){return x.orderNo===no;});if(!o)return;
if(action==='ready'){
@@ -490,13 +768,68 @@ function orderAction(no,action){
}
else if(action==='ship'){
_extOp={orderNo:no};
document.getElementById('shipInfo').textContent='订单号:'+no;
document.getElementById('shipCompany').value='';
document.getElementById('shipTrackNo').value='';
_shipMode=null;
document.getElementById('shipOrderNoExt').textContent=no;
document.getElementById('shipCustomerExt').textContent=o.customer||'—';
/* 重置表单 */
document.querySelectorAll('input[name="shipMode"]').forEach(function(r){r.checked=false;});
document.querySelectorAll('.ship-subpanel').forEach(function(p){p.style.display='none';});
var sdSel=document.getElementById('ship-rider-sd');if(sdSel)sdSel.value='';
var tpCar=document.getElementById('ship-carrier');if(tpCar)tpCar.value='';
var tpTrk=document.getElementById('ship-tracking');if(tpTrk)tpTrk.value='';
openModal('shipModal');
}else if(action==='receive'){o.lines.forEach(function(l){l.status='已完成';});renderExtTable();}
}else if(action==='receive'){
if(!confirm('确认对订单 '+no+' 中所有配送中菜品进行收货?'))return;
o.lines.forEach(function(l){l.status='已完成';});
/* 同步物流状态为已签收 */
if(o.logistics){o.logistics.dispatchStatus='completed';o.logistics.receiveStatus='verified';}
renderExtTable();
}
}
/* 切换发货配送方式子面板 */
function switchShipMode(mode){
_shipMode=mode;
document.querySelectorAll('.ship-subpanel').forEach(function(p){p.style.display='none';});
var panel=document.getElementById('shipPanel-'+mode);
if(panel)panel.style.display='block';
}
/* 生成 6 位收货码 */
function genReceiveCodeExt(){
var code='';
for(var i=0;i<6;i++){code+=Math.floor(Math.random()*10);}
return code;
}
function confirmShip(){
if(!_shipMode){alert('请选择配送方式');return;}
var o=extOrders.find(function(x){return x.orderNo===_extOp.orderNo;});
if(!o)return;
var code=genReceiveCodeExt();
/* 当前时间作为发货时间戳 */
var now=new Date();
var pad=function(n){return String(n).padStart(2,'0');};
var nowStr=now.getFullYear()+'-'+pad(now.getMonth()+1)+'-'+pad(now.getDate())+' '+pad(now.getHours())+':'+pad(now.getMinutes());
if(_shipMode==='self_dispatch'){
var riderVal=document.getElementById('ship-rider-sd').value;
if(!riderVal){alert('请选择配送员');return;}
var parts=riderVal.split('|');
o.logistics={type:'self_dispatch',riderName:parts[0],riderPhone:parts[1],riderId:parts[2],dispatchStatus:'pending',receiveCode:code,receiveStatus:'pending',
t1_ship:nowStr,t2_accept:null,t3_pick:null,t4_deliver:null};
}else if(_shipMode==='self_grab'){
o.logistics={type:'self_grab',riderName:'— 待抢单',riderPhone:'—',riderId:'—',dispatchStatus:'pending',receiveCode:code,receiveStatus:'pending',
t1_ship:nowStr,t2_accept:null,t3_pick:null,t4_deliver:null};
}else if(_shipMode==='third_party'){
var carrier=document.getElementById('ship-carrier').value.trim();
var tracking=document.getElementById('ship-tracking').value.trim();
if(!carrier){alert('请选择承运方');return;}
if(!tracking){alert('请输入运单号');return;}
/* 三方物流发货即视为承运方已接单,跳过 pending 态 */
o.logistics={type:'third_party',carrier:carrier,trackingNo:tracking,dispatchStatus:'delivering',receiveCode:null,receiveStatus:'pending',
t1_ship:nowStr,t2_accept:nowStr,t3_pick:null,t4_deliver:null};
}
o.lines.forEach(function(l){if(l.status==='待配送')l.status='配送中';});
_extOp={};_shipMode=null;
closeModal('shipModal');renderExtTable();
}
function confirmShip(){var c=document.getElementById('shipCompany').value;var t=document.getElementById('shipTrackNo').value.trim();if(!c){alert('请选择物流公司');return;}if(!t){alert('请输入物流单号');return;}var o=extOrders.find(function(x){return x.orderNo===_extOp.orderNo;});if(o){o.lines.forEach(function(l){l.status='配送中';});o.logistics=t;}_extOp={};closeModal('shipModal');renderExtTable();}
function doReadyExt(){var b=document.getElementById('readyBatchSelectExt').value;if(!b){alert('请选择包装批次号');return;}var o=extOrders.find(function(x){return x.orderNo===_extOp.orderNo;});if(o){o.lines.forEach(function(l){if(l.status==='备货中')l.status='待配送';});}_extOp={};closeModal('readyModal');renderExtTable();}
/* ========== 新建订单 ========== */
var newExtOrderType='nc';
@@ -623,7 +956,7 @@ function submitExtOrder(){
}
return {id:'L'+(Date.now()+i),ncType:'cp',foodName:l.dish,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,price:l.price||0,subtotal:l.subtotal||0,status:'备货中',needSeasoning:!!l.needSeasoning};
});
extOrders.unshift({orderNo:no,customer:cust,orderTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:dt,logistics:'—',
extOrders.unshift({orderNo:no,customer:cust,orderTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:dt,logistics:null,
ncType:newExtOrderType,lines:newLines});
closeModal('createOrderModal');renderExtTable();
alert('外售订单 '+no+' 创建成功,包含 '+vl.length+' 道菜品');
@@ -644,9 +977,12 @@ function showDetailExt(no){
h+='<div class="drawer-field"><div class="drawer-field__label">总计量(kg)</div><div class="drawer-field__value">'+tW+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">订单总金额</div><div class="drawer-field__value" style="color:var(--danger);font-weight:700;">'+formatMoney(calcOrderTotal(o))+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">下单时间</div><div class="drawer-field__value">'+o.orderTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">预定配送</div><div class="drawer-field__value">'+o.deliverTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">物流单号</div><div class="drawer-field__value">'+o.logistics+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">预定配送</div><div class="drawer-field__value">'+(o.deliverTime||'—')+'</div></div>';
h+='</div></div>';
/* 物流方式独立分区(完整呈现 + 收货码) */
h+='<div class="drawer-section"><div class="drawer-section__title">配送方式 · 物流信息</div>';
h+=renderLogisticsDetail(o.logistics);
h+='</div>';
h+='<div class="drawer-section"><div class="drawer-section__title">订单明细('+o.lines.length+' 道)</div>';
h+='<table class="ing-table"><thead><tr><th>净菜类型</th><th>菜品/食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>内配调料</th><th>状态</th></tr></thead><tbody>';
o.lines.forEach(function(l){
+382 -30
View File
@@ -158,6 +158,44 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
.seasoning-radio-group{display:inline-flex;align-items:center;gap:12px;white-space:nowrap;}
.seasoning-radio-group label{display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:var(--font-sm);color:var(--text-primary);}
.seasoning-radio-group input[type="radio"]{width:auto;height:auto;margin:0;cursor:pointer;accent-color:var(--primary);}
/* 列表物流单元 hover 提示 */
.logistics-cell{display:inline-flex;align-items:center;border-radius:10px;transition:all .2s;}
.logistics-cell:hover{background:var(--primary-light);transform:translateX(1px);}
/* 配送状态时间轴(物流小弹窗/详情抽屉共用,横向布局) */
.logistics-timeline{display:flex;align-items:flex-start;gap:0;padding:8px 4px 4px;}
.timeline-node{display:flex;flex-direction:column;align-items:center;flex-shrink:0;position:relative;flex:1;max-width:140px;}
.timeline-dot{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;background:#fff;border:2px solid var(--border);color:var(--text-placeholder);position:relative;z-index:2;transition:all .3s;}
.timeline-text{margin-top:6px;text-align:center;width:100%;}
.timeline-label{font-size:var(--font-xs);font-weight:600;color:var(--text-secondary);}
.timeline-time{font-size:10px;color:var(--text-placeholder);margin-top:2px;line-height:1.3;white-space:nowrap;font-family:Consolas,Monaco,monospace;}
.timeline-desc{font-size:10px;color:var(--text-placeholder);margin-top:2px;line-height:1.3;}
/* 节点状态:已完成 / 当前进行 / 未到达 */
.timeline-node--done .timeline-dot{background:var(--success);border-color:var(--success);color:#fff;}
.timeline-node--done .timeline-label{color:var(--success);}
.timeline-node--done .timeline-time{color:var(--text-primary);}
.timeline-node--current .timeline-dot{background:#fff;border-color:var(--primary);color:var(--primary);animation:timelinePulse 1.6s ease-in-out infinite;}
.timeline-node--current .timeline-label{color:var(--primary);}
@keyframes timelinePulse{0%,100%{box-shadow:0 0 0 0 rgba(24,144,255,0.4);}50%{box-shadow:0 0 0 8px rgba(24,144,255,0);}}
.timeline-node--pending .timeline-dot{background:#fff;border-color:var(--border);border-style:dashed;color:var(--text-placeholder);}
.timeline-node--pending .timeline-label{color:var(--text-placeholder);}
.timeline-node--pending .timeline-time{color:var(--text-placeholder);}
/* 连接线(水平,在 dot 之间) */
.timeline-line{position:absolute;top:14px;left:50%;width:100%;height:2px;background:var(--border);z-index:1;}
.timeline-line--done{background:var(--success);}
/* 物流小弹窗样式(复用 modal,宽度 500px) */
.logistics-modal-header{padding:16px 20px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.logistics-modal-title{font-size:var(--font-base);font-weight:600;}
.logistics-modal-body{padding:18px 20px 20px;}
.logistics-modal-section{margin-bottom:16px;}
.logistics-modal-section__title{font-size:var(--font-xs);color:var(--text-placeholder);margin-bottom:8px;font-weight:500;}
.logistics-modal-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;font-size:var(--font-sm);}
.logistics-modal-grid .label{color:var(--text-placeholder);}
.logistics-modal-grid .value{color:var(--text-primary);font-weight:500;}
</style>
</head>
<body>
@@ -216,7 +254,7 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<div class="table-card__content">
<table id="claimTable">
<thead><tr>
<th style="width:36px;"></th><th>申领单号</th><th>申领场所</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总数量(kg)</th><th>订单金额</th><th>申领单状态</th><th>申领时间</th><th>预定配送时间</th><th>配送人</th><th>操作</th>
<th style="width:36px;"></th><th>申领单号</th><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="claimTableBody"></tbody>
</table>
@@ -249,14 +287,14 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<div class="form-item__label">预定配送时间</div>
<input type="datetime-local" id="claimDeliveryTime">
</div>
<div class="form-item">
<div class="form-item__label">配送人</div>
<input placeholder="请输入配送人" id="claimDeliverPerson">
</div>
<div class="form-item">
<div class="form-item__label">包装批次号(选填)</div>
<select id="claimBatchNo"><option value="">请选择</option><option>BATCH-DH-20260415-01</option><option>BATCH-DH-20260415-02</option><option>BATCH-DH-20260415-03</option><option>BATCH-DH-20260415-04</option></select>
</div>
<div class="form-item">
<div class="form-item__label" style="color:var(--text-placeholder);">配送方式</div>
<input value="配送环节统一在「立即配送」按钮选择" readonly style="font-size:var(--font-xs);color:var(--text-placeholder);">
</div>
</div>
<div style="margin-top:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
@@ -277,25 +315,122 @@ 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-logistics">
<div class="modal" style="width:500px;">
<div class="logistics-modal-header">
<span class="logistics-modal-title">🚚 物流详情 — <span id="logisticsOrderNo"></span></span>
<button class="modal__close" onclick="closeModal('modal-logistics')"></button>
</div>
<div class="logistics-modal-body" id="logisticsModalBody"></div>
</div>
</div>
<!-- 弹窗:立即配送 — 配送方式选择 -->
<div class="modal-mask" id="modal-deliver">
<div class="modal" style="width:480px;">
<div class="modal__header"><div class="modal__title">立即配送</div><button class="modal__close" onclick="closeModal('modal-deliver')"></button></div>
<div class="modal" style="width:560px;">
<div class="modal__header"><div class="modal__title">立即配送 · 选择配送方式</div><button class="modal__close" onclick="closeModal('modal-deliver')"></button></div>
<div class="modal__body">
<div style="background:var(--primary-light);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:20px;font-size:var(--font-sm);color:var(--primary);">
<span>申领单号:<strong id="deliverOrderNo"></strong></span>
</div>
<div class="form-grid" style="grid-template-columns:1fr;">
<div class="form-item"><div class="form-item__label">配送人<span class="required">*</span></div><input id="deliverPerson" placeholder="请输入配送人姓名"></div>
<div class="form-item"><div class="form-item__label">备注</div><textarea id="deliverRemark" style="height:80px;padding:8px 12px;resize:vertical;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--font-sm);outline:none;" placeholder="请输入配送备注"></textarea></div>
<!-- 配送方式三选一 -->
<div style="margin-bottom:16px;">
<div style="font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:10px;">配送方式 <span style="color:var(--danger);">*</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;">
<label class="deliver-mode-card" data-mode="self_dispatch">
<input type="radio" name="deliverMode" value="self_dispatch" onchange="switchDeliverMode('self_dispatch')" style="display:none;">
<div class="deliver-mode-card__inner">
<div class="deliver-mode-card__icon">🛵</div>
<div class="deliver-mode-card__title">自营物流·派单</div>
<div class="deliver-mode-card__desc">指定配送员<br>走配送 H5 派单流</div>
</div>
</label>
<label class="deliver-mode-card" data-mode="self_grab">
<input type="radio" name="deliverMode" value="self_grab" onchange="switchDeliverMode('self_grab')" style="display:none;">
<div class="deliver-mode-card__inner">
<div class="deliver-mode-card__icon">🔥</div>
<div class="deliver-mode-card__title">自营物流·抢单</div>
<div class="deliver-mode-card__desc">订单进抢单大厅<br>由配送员自取</div>
</div>
</label>
<label class="deliver-mode-card" data-mode="third_party">
<input type="radio" name="deliverMode" value="third_party" onchange="switchDeliverMode('third_party')" style="display:none;">
<div class="deliver-mode-card__inner">
<div class="deliver-mode-card__icon">🚚</div>
<div class="deliver-mode-card__title">三方物流</div>
<div class="deliver-mode-card__desc">仅录入承运方与运单号<br>不进入 H5</div>
</div>
</label>
</div>
</div>
<!-- 子面板:自营派单 -->
<div id="deliverPanel-self_dispatch" class="deliver-subpanel" style="display:none;">
<div class="form-grid" style="grid-template-columns:1fr 1fr;">
<div class="form-item"><div class="form-item__label">配送员 <span class="required">*</span></div>
<select id="deliver-rider-sd"><option value="">请选择</option>
<option value="李配送|138****1234|RDR-1024">李配送 · RDR-1024 · 138****1234</option>
<option value="王配送|139****5678|RDR-1025">王配送 · RDR-1025 · 139****5678</option>
<option value="陈师傅|138****6688|RDR-2026">陈师傅 · RDR-2026 · 138****6688</option>
</select>
</div>
<div class="form-item"><div class="form-item__label">收货码(系统生成)</div>
<input id="deliver-code-sd" readonly placeholder="提交后系统自动生成 6 位收货码">
</div>
</div>
<div style="background:#FFF7E6;border-radius:var(--radius-md);padding:10px 14px;margin-top:12px;font-size:var(--font-xs);color:#8C7B3A;line-height:1.6;">
💡 订单将作为派单任务推送给所选配送员,配送员需在 30 秒内响应接听。系统会在客户手机生成 6 位收货码,配送员送达时索取并输入后才能完成签收。
</div>
</div>
<!-- 子面板:自营抢单 -->
<div id="deliverPanel-self_grab" class="deliver-subpanel" style="display:none;">
<div style="background:#F9F0FF;border-radius:var(--radius-md);padding:14px;font-size:var(--font-sm);color:#722ED1;">
<div style="font-weight:600;margin-bottom:6px;">🔥 抢单模式说明</div>
<div style="font-size:var(--font-xs);line-height:1.6;">订单将发布到「抢单大厅」(配送员 H5 应用),所有在线配送员可主动抢接。首个抢接的配送员自动成为承运人。</div>
</div>
<div class="form-grid" style="grid-template-columns:1fr; margin-top:12px;">
<div class="form-item"><div class="form-item__label">收货码(系统生成)</div>
<input id="deliver-code-sg" readonly placeholder="提交后系统自动生成 6 位收货码">
</div>
</div>
</div>
<!-- 子面板:三方物流 -->
<div id="deliverPanel-third_party" class="deliver-subpanel" style="display:none;">
<div class="form-grid" style="grid-template-columns:1fr 1fr;">
<div class="form-item"><div class="form-item__label">承运方 <span class="required">*</span></div>
<select id="deliver-carrier"><option value="">请选择</option>
<option>顺丰速运</option><option>京东物流</option><option>德邦快递</option>
<option>圆通速递</option><option>自有车队</option><option>其他</option>
</select>
</div>
<div class="form-item"><div class="form-item__label">运单号 <span class="required">*</span></div>
<input id="deliver-tracking" placeholder="请输入运单号">
</div>
</div>
<div style="background:#FFF1F0;border-radius:var(--radius-md);padding:10px 14px;margin-top:12px;font-size:var(--font-xs);color:#8C3A3A;line-height:1.6;">
⚠️ 三方物流订单不进入配送 H5,客户收货码不生效,仅作后台物流登记与跟进。客户签收由承运方系统回调或后台手动确认。
</div>
</div>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeModal('modal-deliver')">取消</button>
<button class="btn btn-primary" onclick="doDeliver()">确认配送</button>
<button class="btn btn-primary" onclick="doDeliver()">确认发起配送</button>
</div>
</div>
</div>
<style>
.deliver-mode-card{cursor:pointer;display:block;}
.deliver-mode-card__inner{border:1.5px solid var(--border);border-radius:var(--radius-md);padding:14px 10px;text-align:center;background:#fff;transition:all .2s;}
.deliver-mode-card:hover .deliver-mode-card__inner{border-color:var(--primary);background:#fff9f5;}
.deliver-mode-card input:checked + .deliver-mode-card__inner{border-color:var(--primary);background:var(--primary-light);box-shadow:0 0 0 3px rgba(24,144,255,0.1);}
.deliver-mode-card__icon{font-size:28px;margin-bottom:6px;}
.deliver-mode-card__title{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:4px;}
.deliver-mode-card__desc{font-size:var(--font-xs);color:var(--text-secondary);line-height:1.5;}
.deliver-subpanel{border-top:1px dashed var(--border);padding-top:16px;margin-top:4px;}
</style>
<!-- 弹窗:备货完成 -->
<div class="modal-mask" id="modal-ready">
@@ -401,30 +536,47 @@ var dishIngredients={
'炒胡萝卜':[{trace:'NC-PROC-HLB-20260415-01',name:'净胡萝卜',cleanType:'胡萝卜丝',type:'主材',amount:'260g'},{trace:'NC-PROC-CJG-20260415-01',name:'葱姜',cleanType:'姜丝',type:'调料',amount:'适量'}]
};
/* 申领单主子表数据 */
/* 申领单主子表数据
* logistics 字段(共享对象):
* type: 'self_dispatch'(自营派单) | 'self_grab'(自营抢单) | 'third_party'(三方物流)
* riderName / riderPhone / riderId: 自营物流配送员信息(self_dispatch 必填)
* dispatchStatus: 'pending'(待响应) | 'accepted'(已接单) | 'picking'(取货中) | 'delivering'(配送中) | 'completed'(已送达) | 'rejected'(已拒单) | 'timeout'(超时)
* carrier / trackingNo: 三方物流承运方 + 运单号(third_party 必填)
* receiveCode: 6 位收货验证码(自营物流必填,客户向配送员口述,后台不展示)
* receiveStatus: 'pending'(未送达) | 'verifying'(待验证) | 'verified'(已签收)
* t1_ship / t2_accept / t3_pick / t4_deliver: 配送时间轴四节点(已发生的为字符串,未发生为 null)
*/
var claims=[
{claimNo:'INT-001',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 07:30',deliverTime:'2026-04-22 09:00',deliverPerson:'李配送',
{claimNo:'INT-001',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 07:30',deliverTime:'2026-04-22 09:00',
logistics:{type:'self_dispatch',riderName:'李配送',riderPhone:'138****1234',riderId:'RDR-1024',dispatchStatus:'completed',receiveCode:'582491',receiveStatus:'verified',
t1_ship:'2026-04-22 08:30',t2_accept:'2026-04-22 08:32',t3_pick:'2026-04-22 08:45',t4_deliver:'2026-04-22 09:05'},
lines:[
{id:'L1',ncType:'nc',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,price:5,subtotal:125,status:'已完成'},
{id:'L2',ncType:'nc',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,price:6,subtotal:120,status:'已完成'}
]},
{claimNo:'INT-002',canteen:'二号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:00',deliverPerson:'王配送',
{claimNo:'INT-002',canteen:'二号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:00',
logistics:{type:'self_dispatch',riderName:'王配送',riderPhone:'139****5678',riderId:'RDR-1025',dispatchStatus:'delivering',receiveCode:'347820',receiveStatus:'pending',
t1_ship:'2026-04-22 09:00',t2_accept:'2026-04-22 09:01',t3_pick:'2026-04-22 09:15',t4_deliver:null},
lines:[
{id:'L3',ncType:'nc',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,price:25,subtotal:1000,status:'配送中'},
{id:'L4',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:30,weight:15,price:3.5,subtotal:52.5,status:'配送中'}
]},
{claimNo:'INT-003',canteen:'三号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:30',deliverTime:'2026-04-22 10:30',deliverPerson:'—',
{claimNo:'INT-003',canteen:'三号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:30',deliverTime:'2026-04-22 10:30',
logistics:null,
lines:[
{id:'L5',ncType:'nc',ingredient:'草鱼',ncName:'鱼片',spec:'2kg/盘',qty:20,weight:40,price:18,subtotal:720,status:'待配送'},
{id:'L6',ncType:'nc',ingredient:'鸡胸肉',ncName:'鸡胸肉丝',spec:'500g/袋',qty:15,weight:7.5,price:20,subtotal:150,status:'待配送'}
]},
{claimNo:'INT-004',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:30',deliverPerson:'—',
{claimNo:'INT-004',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:30',
logistics:null,
lines:[
{id:'L7',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,price:3.5,subtotal:70,status:'备货中'},
{id:'L8',ncType:'nc',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:25,weight:12.5,price:5,subtotal:62.5,status:'备货中'},
{id:'L9',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:6,subtotal:90,status:'备货中'}
]},
{claimNo:'INT-005',canteen:'二号食堂厨房',ncType:'cp',claimTime:'2026-04-22 09:30',deliverTime:'2026-04-22 12:00',deliverPerson:'—',
{claimNo:'INT-005',canteen:'二号食堂厨房',ncType:'cp',claimTime:'2026-04-22 09:30',deliverTime:'2026-04-22 12:00',
logistics:{type:'self_grab',riderName:'张师傅',riderPhone:'137****8888',riderId:'RDR-2026',dispatchStatus:'delivering',receiveCode:'915374',receiveStatus:'pending',
t1_ship:'2026-04-22 10:30',t2_accept:'2026-04-22 10:35',t3_pick:'2026-04-22 10:50',t4_deliver:null},
lines:[
{id:'L10',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:20,weight:10,price:12,subtotal:120,status:'已完成',needSeasoning:true},
{id:'L11',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g',qty:15,weight:4.5,price:18,subtotal:81,status:'配送中',needSeasoning:false}
@@ -435,6 +587,121 @@ function calcOrderTotal(o){return o.lines.reduce(function(s,l){return s+(parseFl
/* 金额格式化:¥1,234.5 */
function formatMoney(n){var v=parseFloat(n)||0;return '¥'+v.toLocaleString('zh-CN',{minimumFractionDigits:0,maximumFractionDigits:2});}
/* ========== 物流信息共享渲染(跨 sup-clean-internal / sup-clean-external / prod-clean-order 一致) ========== */
/* 配送方式徽标:self_dispatch=自营派单 / self_grab=自营抢单 / third_party=三方物流 */
function logisticsTypeTag(type){
var m={
'self_dispatch':'<span class="tag tag-blue">🛵 自营·派单</span>',
'self_grab':'<span class="tag tag-purple">🔥 自营·抢单</span>',
'third_party':'<span class="tag tag-orange">🚚 三方物流</span>'
};
return m[type]||'<span class="tag tag-gray">— 未配送</span>';
}
/* 配送过程状态徽标(对应 H5 七态) */
function dispatchStatusTag(s){
var m={
'pending':'<span class="tag tag-gray">⏳ 待响应</span>',
'accepted':'<span class="tag tag-blue">✓ 已接单</span>',
'picking':'<span class="tag tag-orange">📦 取货中</span>',
'delivering':'<span class="tag tag-purple">🛵 配送中</span>',
'completed':'<span class="tag tag-green">✓ 已送达</span>',
'rejected':'<span class="tag tag-red">✗ 已拒单</span>',
'timeout':'<span class="tag tag-gray">⏰ 超时未响应</span>'
};
return m[s]||'';
}
/* 收货验证状态徽标 */
function receiveStatusTag(s){
var m={
'pending':'<span class="tag tag-gray">未送达</span>',
'verifying':'<span class="tag tag-orange">待验证</span>',
'verified':'<span class="tag tag-green">已签收</span>'
};
return m[s]||'<span class="tag tag-gray">未送达</span>';
}
/* 列表「物流信息」单元:仅显示配送方式徽标(可点击查看详情) */
function renderLogistics(logistics,orderNo){
if(!logistics){
return '<span style="color:var(--text-placeholder);"></span>';
}
/* 用 wrapper 包裹徽标,hover 时变蓝可点击 */
return '<span class="logistics-cell" onclick="event.stopPropagation();showLogisticsModal(\''+orderNo+'\')" style="cursor:pointer;">'
+logisticsTypeTag(logistics.type)
+'</span>';
}
/* 配送状态时间轴(4 节点:发货/接单/取货/送达)
* 已完成:绿色实心圆 + 实线
* 当前节点(下一未完成的前一个):蓝色脉冲
* 未到达:灰色空心圆 + 灰色虚线
*/
function renderLogisticsTimeline(logistics){
if(!logistics)return '';
var nodes=[
{key:'t1_ship',label:'发货',icon:'📦',time:logistics.t1_ship||null,desc:'后台点击立即配送'},
{key:'t2_accept',label:'接单',icon:'✓',time:logistics.t2_accept||null,desc:logistics.type==='third_party'?'承运方接单':'配送员响应接单'},
{key:'t3_pick',label:'取货',icon:'🛵',time:logistics.t3_pick||null,desc:logistics.type==='third_party'?'承运方取件':'配送员到店取货'},
{key:'t4_deliver',label:'送达',icon:'📍',time:logistics.t4_deliver||null,desc:'客户签收'}
];
/* 找到第一个未完成节点的索引(即当前进行中的节点) */
var currentIdx=-1;
for(var i=0;i<nodes.length;i++){
if(!nodes[i].time){currentIdx=i;break;}
}
/* 全部完成时 currentIdx 置为节点数,后续判断 i<currentIdx 时所有线都是 done */
if(currentIdx===-1)currentIdx=nodes.length;
var html='<div class="logistics-timeline">';
nodes.forEach(function(n,i){
var state='done';
if(i===currentIdx)state='current';
else if(i>currentIdx)state='pending';
html+='<div class="timeline-node timeline-node--'+state+'">';
/* 圆点 */
html+='<div class="timeline-dot">'+(state==='done'?'✓':(state==='current'?'●':n.icon))+'</div>';
/* 文本 */
html+='<div class="timeline-text">';
html+='<div class="timeline-label">'+n.label+'</div>';
html+='<div class="timeline-time">'+(n.time||'—')+'</div>';
html+='<div class="timeline-desc">'+n.desc+'</div>';
html+='</div>';
/* 连接线(非最后一个节点,水平延伸到下个节点 dot 中心) */
if(i<nodes.length-1){
var lineState=(i<currentIdx)?'done':'pending';
html+='<div class="timeline-line timeline-line--'+lineState+'"></div>';
}
html+='</div>';
});
html+='</div>';
return html;
}
/* 详情抽屉用「物流信息」单元:展示完整配送员信息 + 时间轴(后台视角不展示收货码) */
function renderLogisticsDetail(logistics){
if(!logistics){
return '<div style="color:var(--text-placeholder);font-size:var(--font-sm);">该订单尚未发起配送,请点击「立即配送」选择配送方式</div>';
}
var html='<div style="display:flex;flex-direction:column;gap:14px;">';
/* 配送方式徽标 */
html+='<div>'+logisticsTypeTag(logistics.type)+'</div>';
/* 承运信息 grid */
if(logistics.type==='self_dispatch'||logistics.type==='self_grab'){
html+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;font-size:var(--font-sm);">';
html+='<div><span style="color:var(--text-placeholder);">配送员:</span> <b>'+logistics.riderName+'</b></div>';
html+='<div><span style="color:var(--text-placeholder);">联系电话:</span> '+logistics.riderPhone+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送员工号:</span> '+logistics.riderId+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送状态:</span> '+dispatchStatusTag(logistics.dispatchStatus)+'</div>';
html+='</div>';
}else if(logistics.type==='third_party'){
html+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;font-size:var(--font-sm);">';
html+='<div><span style="color:var(--text-placeholder);">承运方:</span> <b>'+logistics.carrier+'</b></div>';
html+='<div><span style="color:var(--text-placeholder);">运单号:</span> '+logistics.trackingNo+'</div>';
html+='<div><span style="color:var(--text-placeholder);">配送状态:</span> '+dispatchStatusTag(logistics.dispatchStatus)+'</div>';
html+='</div>';
}
/* 配送进度时间轴 */
html+='<div style="margin-top:4px;"><div style="font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:8px;font-weight:600;">📊 配送进度</div>'+renderLogisticsTimeline(logistics)+'</div>';
html+='</div>';
return html;
}
function aggregateStatus(lines){
var statuses=lines.map(function(l){return l.status;});
if(statuses.every(function(s){return s==='已完成';}))return '已完成';
@@ -463,7 +730,7 @@ function renderClaimTable(){
html+='<tr class="master-row" data-claim-idx="'+ci+'" onclick="toggleExpand(this,'+ci+')">';
html+='<td><span class="expand-arrow" id="carrow'+ci+'"></span></td>';
html+='<td><b>'+c.claimNo+'</b></td><td>'+c.canteen+'</td><td>'+ncTypeTag(c.ncType)+'</td><td>'+c.lines.length+'</td><td>'+tQty+'</td><td>'+tW+'</td><td><b>'+formatMoney(calcOrderTotal(c))+'</b></td><td>'+statusTag(agg)+'</td>';
html+='<td>'+c.claimTime+'</td><td>'+c.deliverTime+'</td><td>'+c.deliverPerson+'</td>';
html+='<td>'+c.claimTime+'</td><td>'+(c.deliverTime||'—')+'</td><td>'+renderLogistics(c.logistics,c.claimNo)+'</td>';
html+='<td class="td-actions">';
if(agg==='备货中')html+='<span class="link" style="color:var(--success);" onclick="event.stopPropagation();orderAction(\''+c.claimNo+'\',\'ready\')">备货完成</span> ';
if(agg==='待配送')html+='<span class="link" style="color:var(--primary);" onclick="event.stopPropagation();orderAction(\''+c.claimNo+'\',\'deliver\')">立即配送</span> ';
@@ -478,7 +745,7 @@ function renderClaimTable(){
}else{
html+='<td colspan="2">菜品/食材</td>';
}
html+='<td>包装规格</td><td>份数</td><td>数量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td><td></td></tr>';
html+='<td>包装规格</td><td>份数</td><td>数量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td>内配调料</td><td></td><td></td><td></td><td></td></tr>';
c.lines.forEach(function(line){
html+='<tr class="detail-row" id="cdetail-'+ci+'" style="display:none;">';
html+='<td></td>';
@@ -489,7 +756,7 @@ function renderClaimTable(){
}
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td><td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td>';
html+='<td>'+seasoningTagInt(line)+'</td>';
html+='<td></td><td></td><td></td><td></td><td class="td-actions"></td></tr>';
html+='<td></td><td></td><td></td><td class="td-actions"></td></tr>';
});
});
tbody.innerHTML=html;
@@ -515,6 +782,7 @@ function deleteClaim(no){
/* ========== 订单级操作 ========== */
var _deliverClaimNo=null;
var _readyClaimNo=null;
var _deliverMode=null; /* 当前选中的配送方式 */
function orderAction(claimNo,action){
var c=claims.find(function(x){return x.claimNo===claimNo;});
@@ -526,25 +794,70 @@ function orderAction(claimNo,action){
openModal('modal-ready');
}else if(action==='deliver'){
_deliverClaimNo=claimNo;
_deliverMode=null;
document.getElementById('deliverOrderNo').textContent=claimNo;
document.getElementById('deliverPerson').value=c.deliverPerson!=='—'?c.deliverPerson:'';
document.getElementById('deliverRemark').value='';
/* 重置表单 */
document.querySelectorAll('input[name="deliverMode"]').forEach(function(r){r.checked=false;});
document.querySelectorAll('.deliver-subpanel').forEach(function(p){p.style.display='none';});
var sdSel=document.getElementById('deliver-rider-sd');if(sdSel)sdSel.value='';
var sgCode=document.getElementById('deliver-code-sg');if(sgCode)sgCode.value='';
var tpCar=document.getElementById('deliver-carrier');if(tpCar)tpCar.value='';
var tpTrk=document.getElementById('deliver-tracking');if(tpTrk)tpTrk.value='';
openModal('modal-deliver');
}else if(action==='receive'){
if(!confirm('确认对申领单 '+claimNo+' 中的所有配送中菜品进行收货?'))return;
c.lines.forEach(function(l){if(l.status==='配送中')l.status='已完成';});
/* 同步物流状态为已签收 */
if(c.logistics){c.logistics.dispatchStatus='completed';c.logistics.receiveStatus='verified';}
renderClaimTable();
}
}
/* 切换配送方式子面板 */
function switchDeliverMode(mode){
_deliverMode=mode;
document.querySelectorAll('.deliver-subpanel').forEach(function(p){p.style.display='none';});
var panel=document.getElementById('deliverPanel-'+mode);
if(panel)panel.style.display='block';
}
/* 生成 6 位收货码 */
function genReceiveCode(){
var code='';
for(var i=0;i<6;i++){code+=Math.floor(Math.random()*10);}
return code;
}
/* 提交发起配送 */
function doDeliver(){
var p=document.getElementById('deliverPerson').value.trim();
if(!p){alert('请输入配送人姓名');return;}
if(!_deliverMode){alert('请选择配送方式');return;}
var c=claims.find(function(x){return x.claimNo===_deliverClaimNo;});
if(!c)return;
var code=genReceiveCode();
var now=new Date();
var pad=function(n){return String(n).padStart(2,'0');};
var nowStr=now.getFullYear()+'-'+pad(now.getMonth()+1)+'-'+pad(now.getDate())+' '+pad(now.getHours())+':'+pad(now.getMinutes());
/* 三方物流直接进入配送中,自营物流初始为待响应 */
if(_deliverMode==='self_dispatch'){
var riderVal=document.getElementById('deliver-rider-sd').value;
if(!riderVal){alert('请选择配送员');return;}
var parts=riderVal.split('|');
c.logistics={type:'self_dispatch',riderName:parts[0],riderPhone:parts[1],riderId:parts[2],dispatchStatus:'pending',receiveCode:code,receiveStatus:'pending',
t1_ship:nowStr,t2_accept:null,t3_pick:null,t4_deliver:null};
}else if(_deliverMode==='self_grab'){
c.logistics={type:'self_grab',riderName:'— 待抢单',riderPhone:'—',riderId:'—',dispatchStatus:'pending',receiveCode:code,receiveStatus:'pending',
t1_ship:nowStr,t2_accept:null,t3_pick:null,t4_deliver:null};
}else if(_deliverMode==='third_party'){
var carrier=document.getElementById('deliver-carrier').value.trim();
var tracking=document.getElementById('deliver-tracking').value.trim();
if(!carrier){alert('请选择承运方');return;}
if(!tracking){alert('请输入运单号');return;}
c.logistics={type:'third_party',carrier:carrier,trackingNo:tracking,dispatchStatus:'delivering',receiveCode:null,receiveStatus:'pending',
t1_ship:nowStr,t2_accept:nowStr,t3_pick:null,t4_deliver:null};
}
c.lines.forEach(function(l){if(l.status==='待配送')l.status='配送中';});
c.deliverPerson=p;
_deliverClaimNo=null;
_deliverMode=null;
closeModal('modal-deliver');
renderClaimTable();
}
@@ -569,7 +882,6 @@ function resetClaimForm(){
document.getElementById('claimCanteen').value='';
document.getElementById('claimNcType').value='nc';
document.getElementById('claimDeliveryTime').value='';
document.getElementById('claimDeliverPerson').value='';
document.getElementById('claimBatchNo').value='';
document.getElementById('claimLinesContainer').innerHTML='';
addClaimLine();
@@ -705,7 +1017,7 @@ function submitClaim(){
}
if(vl.length===0){alert('请至少完善一条菜品明细');return;}
var no='INT-'+String(claims.length+1).padStart(3,'0');
claims.unshift({claimNo:no,canteen:canteen,ncType:orderNcType,claimTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:document.getElementById('claimDeliveryTime').value||'—',deliverPerson:document.getElementById('claimDeliverPerson').value||'—',
claims.unshift({claimNo:no,canteen:canteen,ncType:orderNcType,claimTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:document.getElementById('claimDeliveryTime').value||'—',logistics:null,
lines:vl.map(function(l,i){
var specLbl=formatSpecLabelInt(l);
if(orderNcType==='nc'){
@@ -732,9 +1044,12 @@ function showDetail(claimNo){
h+='<div class="drawer-field"><div class="drawer-field__label">总数量(kg)</div><div class="drawer-field__value">'+tW+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">订单总金额</div><div class="drawer-field__value"><b style="color:var(--danger);">'+formatMoney(calcOrderTotal(c))+'</b></div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">申领时间</div><div class="drawer-field__value">'+c.claimTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">预定配送时间</div><div class="drawer-field__value">'+c.deliverTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">配送人</div><div class="drawer-field__value">'+c.deliverPerson+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">预定配送时间</div><div class="drawer-field__value">'+(c.deliverTime||'—')+'</div></div>';
h+='</div></div>';
/* 物流方式独立分区(完整呈现 + 收货码) */
h+='<div class="drawer-section"><div class="drawer-section__title">配送方式 · 物流信息</div>';
h+=renderLogisticsDetail(c.logistics);
h+='</div>';
h+='<div class="drawer-section"><div class="drawer-section__title">申领明细('+c.lines.length+' 道)</div>';
h+='<table class="ing-table"><thead><tr>';
if(c.ncType==='nc'){
@@ -759,6 +1074,43 @@ function showDetail(claimNo){
}
function closeDrawer(){var o=document.querySelector('.drawer-overlay');if(!o)return;o.classList.add('closing');setTimeout(function(){o.remove();},200);}
/* ========== 物流详情小弹窗(列表徽标点击触发) ========== */
function showLogisticsModal(claimNo){
var c=claims.find(function(x){return x.claimNo===claimNo;});
if(!c||!c.logistics)return;
var lg=c.logistics;
document.getElementById('logisticsOrderNo').textContent=claimNo;
var html='';
/* 配送方式徽标 */
html+='<div class="logistics-modal-section">';
html+='<div class="logistics-modal-section__title">配送方式</div>';
html+='<div style="margin-bottom:6px;">'+logisticsTypeTag(lg.type)+'</div>';
html+='</div>';
/* 核心字段 */
html+='<div class="logistics-modal-section">';
html+='<div class="logistics-modal-section__title">承运信息</div>';
html+='<div class="logistics-modal-grid">';
if(lg.type==='self_dispatch'||lg.type==='self_grab'){
html+='<div><span class="label">配送员:</span> <span class="value">'+lg.riderName+'</span></div>';
html+='<div><span class="label">联系电话:</span> <span class="value">'+lg.riderPhone+'</span></div>';
html+='<div><span class="label">配送员工号:</span> <span class="value">'+lg.riderId+'</span></div>';
html+='<div><span class="label">配送状态:</span> '+dispatchStatusTag(lg.dispatchStatus)+'</div>';
}else if(lg.type==='third_party'){
html+='<div><span class="label">承运方:</span> <span class="value">'+lg.carrier+'</span></div>';
html+='<div><span class="label">运单号:</span> <span class="value" style="font-family:Consolas,Monaco,monospace;">'+lg.trackingNo+'</span></div>';
html+='<div><span class="label">配送状态:</span> '+dispatchStatusTag(lg.dispatchStatus)+'</div>';
}
html+='</div>';
html+='</div>';
/* 配送状态时间轴 */
html+='<div class="logistics-modal-section">';
html+='<div class="logistics-modal-section__title">配送进度</div>';
html+=renderLogisticsTimeline(lg);
html+='</div>';
document.getElementById('logisticsModalBody').innerHTML=html;
openModal('modal-logistics');
}
renderClaimTable();
</script>
</body>
+428 -1
View File
@@ -127,6 +127,63 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
.drawer-field{display:flex;flex-direction:column;gap:2px;}
.drawer-field__label{font-size:var(--font-xs);color:var(--text-placeholder);}
.drawer-field__value{font-size:var(--font-sm);color:var(--text-primary);font-weight:500;min-height:22px;}
/* ============ 智能货柜整行卡片 ============ */
.cabinet-list{display:flex;flex-direction:column;gap:16px;}
.cabinet-card{background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-sm);overflow:hidden;}
.cabinet-card__header{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--border);flex-wrap:wrap;}
.cabinet-card__name{font-size:var(--font-base);font-weight:600;color:var(--text-primary);}
.cabinet-card__code{font-family:monospace;color:var(--text-secondary);font-size:var(--font-xs);}
.cabinet-card__sep{color:var(--border);}
.cabinet-card__meta{font-size:var(--font-xs);color:var(--text-secondary);}
.cabinet-card__tags{display:flex;align-items:center;gap:6px;margin-left:auto;}
.cabinet-card__stat{display:inline-flex;align-items:center;gap:4px;font-size:var(--font-xs);color:var(--text-secondary);}
.cabinet-card__stat strong{color:var(--text-primary);font-weight:600;margin-right:2px;}
.cabinet-card__stat-dot{width:10px;height:10px;border-radius:2px;border:1.5px solid;display:inline-block;}
.cabinet-card__stat-dot.idle{background:#FAFAFA;border-color:var(--border);border-style:dashed;}
.cabinet-card__stat-dot.storing{background:#FAFFF5;border-color:#B7EB8F;}
.cabinet-card__body{padding:16px 20px;display:flex;align-items:stretch;gap:16px;}
.cabinet-card__slots{flex:1;display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;}
.cabinet-cell{flex:0 0 auto;width:96px;border:1.5px solid var(--border);border-radius:var(--radius-sm);padding:8px 6px;min-height:64px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;text-align:center;position:relative;cursor:pointer;transition:all .15s;background:#fff;}
.cabinet-cell:hover{border-color:var(--primary);box-shadow:0 2px 8px rgba(24,144,255,0.18);}
.cabinet-cell__id{font-size:10px;color:var(--text-placeholder);font-weight:500;}
.cabinet-cell__name{font-size:11px;color:var(--text-primary);font-weight:500;line-height:1.2;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.cabinet-cell__qty{font-size:10px;color:var(--primary);font-weight:600;}
.cabinet-cell.idle{background:#FAFAFA;border-style:dashed;}
.cabinet-cell.idle .cabinet-cell__name{color:var(--text-placeholder);font-weight:400;}
.cabinet-cell.occupied{background:#FAFFF5;border-color:#B7EB8F;}
.cabinet-cell.offline{background:#F5F5F5;border-color:#D9D9D9;opacity:.7;cursor:not-allowed;}
.cabinet-cell.offline:hover{border-color:#D9D9D9;box-shadow:none;}
.cabinet-cell.offline .cabinet-cell__name{color:var(--text-placeholder);}
.cabinet-cell.offline .cabinet-cell__qty{color:var(--text-placeholder);}
/* ============ 格口历史居中弹窗(1100×80vh ============ */
.history-modal-mask{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:1500;display:flex;align-items:center;justify-content:center;animation:modalFadeIn .2s ease;}
.history-modal-mask.closing{animation:modalFadeOut .2s ease forwards;}
.history-modal{width:1100px;max-width:94vw;height:80vh;max-height:90vh;background:#fff;border-radius:var(--radius-md);box-shadow:0 12px 40px rgba(0,0,0,.18);display:flex;flex-direction:column;overflow:hidden;animation:historyModalIn .22s ease;}
.history-modal-mask.closing .history-modal{animation:historyModalOut .18s ease forwards;}
@keyframes historyModalIn{from{transform:translateY(16px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
@keyframes historyModalOut{from{transform:translateY(0) scale(1);opacity:1}to{transform:translateY(16px) scale(.98);opacity:0}}
.history-modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);flex-shrink:0;}
.history-modal__title{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);}
.history-modal__close{width:32px;height:32px;border:none;background:none;font-size:20px;cursor:pointer;border-radius:var(--radius-sm);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;}
.history-modal__close:hover{background:#f5f5f5;color:var(--text-primary);}
.history-modal__body{padding:20px 24px;overflow-y:auto;flex:1;}
.history-modal__footer{padding:12px 24px;border-top:1px solid var(--border);flex-shrink:0;background:#FAFBFC;}
.history-summary{display:flex;align-items:center;gap:16px;flex-wrap:wrap;background:#F8FBFF;border:1px solid var(--border);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:16px;}
.history-summary__item{display:flex;align-items:center;gap:6px;font-size:var(--font-sm);color:var(--text-secondary);}
.history-summary__item strong{color:var(--text-primary);font-weight:600;}
.history-summary__item .label{color:var(--text-placeholder);font-size:var(--font-xs);}
.history-summary__count{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:var(--font-xs);}
.history-summary__count.in{background:#F6FFED;color:var(--success);}
.history-summary__count.out{background:#FFF7E6;color:var(--warning);}
.history-table{width:100%;border-collapse:collapse;}
.history-table thead th{background:#FAFAFA;padding:10px 12px;text-align:left;font-size:var(--font-xs);color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap;}
.history-table tbody td{padding:10px 12px;font-size:var(--font-sm);color:var(--text-primary);border-bottom:1px solid var(--border);white-space:nowrap;}
.history-table tbody tr:last-child td{border-bottom:none;}
.history-delta{display:inline-flex;align-items:center;gap:2px;font-weight:600;}
.history-delta.pos{color:var(--success);}
.history-delta.neg{color:var(--danger);}
.history-pagination{display:flex;align-items:center;justify-content:flex-end;padding:14px 0 0;gap:6px;border-top:1px solid var(--border);margin-top:12px;}
.history-pagination__total{font-size:var(--font-sm);color:var(--text-secondary);margin-right:auto;}
</style>
</head>
<body>
@@ -202,6 +259,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<div class="page-tab active" onclick="switchTab(0)">🥬 在库库存</div>
<div class="page-tab" onclick="switchTab(1)">🏭 仓库档案</div>
<div class="page-tab" onclick="switchTab(2)">📦 区域管理</div>
<div class="page-tab" onclick="switchTab(3)">🗄️ 智能货柜</div>
</div>
<!-- Tab1: 在库库存 -->
@@ -349,6 +407,22 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
</div>
</div>
</div>
<!-- Tab4: 智能货柜(只读展示设备管理中录入的智能货柜) -->
<div class="tab-panel">
<div class="filter-bar">
<div class="filter-item"><label>设备编号/名称</label><input class="filter-input" placeholder="请输入设备编号或名称"></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>在线</option><option>离线</option><option>故障</option></select>
</div>
<button class="btn btn-primary">查询</button>
<button class="btn btn-default">重置</button>
</div>
<div class="cabinet-list" id="cabinetList"></div>
</div>
</main>
<!-- 仓库档案弹窗 -->
@@ -496,7 +570,32 @@ function showWhDetail(el){
Drawer.open('仓库详情 — '+g(1),html);
}
/* 区域管理详情 */
/* 区域放置物品 Mock 数据:按区域编号索引,同一区域可同时存放多种净菜/毛菜 */
var ZONE_ITEMS={
'WH-A-01':[
{itemType:'毛菜',itemName:'菠菜',inOrder:'IN-20260415-001',inTime:'2026-04-15 10:30',op:'张仓管'},
{itemType:'毛菜',itemName:'油麦菜',inOrder:'IN-20260415-012',inTime:'2026-04-15 11:10',op:'张仓管'}
],
'WH-A-02':[
{itemType:'毛菜',itemName:'小白菜',inOrder:'IN-20260416-008',inTime:'2026-04-16 08:20',op:'张仓管'}
],
'WH-A-03':[
{itemType:'毛菜',itemName:'胡萝卜',inOrder:'IN-20260416-005',inTime:'2026-04-16 11:00',op:'张仓管'},
{itemType:'毛菜',itemName:'白萝卜',inOrder:'IN-20260416-006',inTime:'2026-04-16 11:20',op:'张仓管'}
],
'WH-B-01':[
{itemType:'净菜',itemName:'净猪肉丁',inOrder:'IN-20260415-021',inTime:'2026-04-15 14:00',op:'王仓管'},
{itemType:'毛菜',itemName:'五花肉',inOrder:'IN-20260415-020',inTime:'2026-04-15 13:30',op:'王仓管'}
],
'WH-B-02':[
{itemType:'净菜',itemName:'净鸡腿肉',inOrder:'IN-20260415-025',inTime:'2026-04-15 15:00',op:'王仓管'}
],
'WH-C-01':[
{itemType:'毛菜',itemName:'草鱼',inOrder:'IN-20260415-004',inTime:'2026-04-15 09:45',op:'李仓管'}
]
};
/* 区域管理详情:使用中状态追加「当前放置物品」分区(净菜/毛菜可共存) */
function showZoneDetail(el){
var row=el.closest('tr');
var cells=row.querySelectorAll('td');
@@ -510,8 +609,336 @@ function showZoneDetail(el){
+'<div class="drawer-field"><span class="drawer-field__label">容量(㎥)</span><span class="drawer-field__value">'+g(4)+'</span></div>'
+'<div class="drawer-field"><span class="drawer-field__label">状态</span><span class="drawer-field__value">'+h(5)+'</span></div>'
+'</div></div>';
/* 仅当状态为「使用中」时,展示当前放置物品列表 */
var statusText=g(5);
if(statusText.indexOf('使用中')!==-1){
var zoneCode=g(0);
var items=ZONE_ITEMS[zoneCode]||[];
html+='<div class="drawer-section"><div class="drawer-section__title">当前放置物品<span style="margin-left:8px;font-size:var(--font-xs);color:var(--text-placeholder);font-weight:400;">共 '+items.length+' 种</span></div>';
if(items.length===0){
html+='<div style="padding:16px;text-align:center;color:var(--text-placeholder);font-size:var(--font-sm);background:#FAFAFA;border-radius:var(--radius-sm);">暂无物品数据</div>';
}else{
html+='<div style="overflow-x:auto"><table style="width:100%;min-width:440px;border-collapse:collapse;font-size:var(--font-sm);">'
+'<thead><tr>'
+'<th style="background:#FAFAFA;padding:10px 12px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--border);">物品类型</th>'
+'<th style="background:#FAFAFA;padding:10px 12px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--border);">物品名称</th>'
+'<th style="background:#FAFAFA;padding:10px 12px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--border);">入库单号</th>'
+'<th style="background:#FAFAFA;padding:10px 12px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--border);">入库时间</th>'
+'<th style="background:#FAFAFA;padding:10px 12px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--border);">操作人</th>'
+'</tr></thead><tbody>';
items.forEach(function(it){
var typeTag=it.itemType==='净菜'?'<span class="tag tag-blue">净菜</span>':'<span class="tag tag-green">毛菜</span>';
html+='<tr>'
+'<td style="padding:10px 12px;border-bottom:1px solid var(--border);">'+typeTag+'</td>'
+'<td style="padding:10px 12px;border-bottom:1px solid var(--border);">'+it.itemName+'</td>'
+'<td style="padding:10px 12px;border-bottom:1px solid var(--border);color:var(--primary);">'+it.inOrder+'</td>'
+'<td style="padding:10px 12px;border-bottom:1px solid var(--border);">'+it.inTime+'</td>'
+'<td style="padding:10px 12px;border-bottom:1px solid var(--border);">'+it.op+'</td>'
+'</tr>';
});
html+='</tbody></table></div>';
}
}
Drawer.open('区域详情 — '+g(1),html);
}
/* ====================== 智能货柜(Tab4====================== */
/* 智能货柜 Mock 数据:来自「设备管理」模块录入,此处只读展示
格口状态归并为:occupied(储物中) / idle(空闲) / offline(设备离线) */
var CABINETS=[
{
code:'CAB-NC-001',name:'3号净菜柜',cabinetType:'净菜柜',status:'在线',totalSlots:10,
slots:[
{id:'NC3-01',status:'occupied',itemName:'净土豆丝',itemType:'净菜',qty:10.2,op:'张厨师',inTime:'2026-04-22 09:15'},
{id:'NC3-02',status:'occupied',itemName:'净胡萝卜',itemType:'净菜',qty:15.6,op:'李主管',inTime:'2026-04-22 09:20'},
{id:'NC3-03',status:'occupied',itemName:'净香菇',itemType:'净菜',qty:6.8,op:'王厨师',inTime:'2026-04-22 09:30'},
{id:'NC3-04',status:'occupied',itemName:'净西兰花',itemType:'净菜',qty:8.5,op:'张厨师',inTime:'2026-04-22 09:40'},
{id:'NC3-05',status:'occupied',itemName:'净白菜',itemType:'净菜',qty:18.0,op:'王厨师',inTime:'2026-04-22 08:50'},
{id:'NC3-06',status:'idle'},
{id:'NC3-07',status:'idle'},
{id:'NC3-08',status:'idle'},
{id:'NC3-09',status:'idle'},
{id:'NC3-10',status:'idle'}
]
},
{
code:'CAB-NC-002',name:'5号净菜柜',cabinetType:'净菜柜',status:'在线',totalSlots:10,
slots:[
{id:'NC5-01',status:'occupied',itemName:'净黄瓜片',itemType:'净菜',qty:7.4,op:'李主管',inTime:'2026-04-22 10:05'},
{id:'NC5-02',status:'occupied',itemName:'净西红柿',itemType:'净菜',qty:12.0,op:'张厨师',inTime:'2026-04-22 10:15'},
{id:'NC5-03',status:'occupied',itemName:'净洋葱',itemType:'净菜',qty:3.2,op:'王厨师',inTime:'2026-04-22 08:40'},
{id:'NC5-04',status:'occupied',itemName:'净青椒丝',itemType:'净菜',qty:6.6,op:'张厨师',inTime:'2026-04-22 11:00'},
{id:'NC5-05',status:'occupied',itemName:'净豆角',itemType:'净菜',qty:9.0,op:'李主管',inTime:'2026-04-22 11:10'},
{id:'NC5-06',status:'occupied',itemName:'净藕片',itemType:'净菜',qty:4.8,op:'王厨师',inTime:'2026-04-22 11:20'},
{id:'NC5-07',status:'idle'},
{id:'NC5-08',status:'idle'},
{id:'NC5-09',status:'idle'},
{id:'NC5-10',status:'idle'}
]
},
{
code:'CAB-RW-008',name:'8号毛菜柜',cabinetType:'毛菜柜',status:'在线',totalSlots:8,
slots:[
{id:'RW8-A1',status:'occupied',itemName:'菠菜',itemType:'毛菜',qty:225.0,op:'张仓管',inTime:'2026-04-15 10:30'},
{id:'RW8-A2',status:'occupied',itemName:'油麦菜',itemType:'毛菜',qty:60.0,op:'张仓管',inTime:'2026-04-15 11:10'},
{id:'RW8-B1',status:'occupied',itemName:'小白菜',itemType:'毛菜',qty:80.0,op:'张仓管',inTime:'2026-04-16 08:20'},
{id:'RW8-B2',status:'occupied',itemName:'生菜',itemType:'毛菜',qty:42.5,op:'张仓管',inTime:'2026-04-16 09:00'},
{id:'RW8-B3',status:'occupied',itemName:'空心菜',itemType:'毛菜',qty:30.0,op:'张仓管',inTime:'2026-04-15 08:30'},
{id:'RW8-C1',status:'occupied',itemName:'芹菜',itemType:'毛菜',qty:12.0,op:'张仓管',inTime:'2026-04-14 16:00'},
{id:'RW8-C2',status:'idle'},
{id:'RW8-C3',status:'idle'}
]
},
{
code:'CAB-RW-009',name:'9号毛菜柜',cabinetType:'毛菜柜',status:'离线',totalSlots:8,
slots:[
{id:'RW9-A1',status:'offline',itemName:'蒜薹',itemType:'毛菜',qty:55.0,op:'张仓管',inTime:'2026-04-15 14:00'},
{id:'RW9-A2',status:'offline',itemName:'韭菜',itemType:'毛菜',qty:38.0,op:'张仓管',inTime:'2026-04-15 14:20'},
{id:'RW9-B1',status:'offline'},
{id:'RW9-B2',status:'offline'},
{id:'RW9-C1',status:'offline'},
{id:'RW9-C2',status:'offline'},
{id:'RW9-C3',status:'offline'},
{id:'RW9-D1',status:'offline'}
]
}
];
/* 格口状态 → 文案/样式映射 */
function slotStatusText(s){
return {occupied:'储物中',idle:'空闲',offline:'离线'}[s]||s;
}
function slotStatusTag(s){
var map={occupied:'tag-green',idle:'tag-gray',offline:'tag-gray'};
return '<span class="tag '+(map[s]||'tag-gray')+'">'+slotStatusText(s)+'</span>';
}
/* 渲染智能货柜整行卡片列表 */
function renderCabinets(){
var wrap=document.getElementById('cabinetList');
if(!wrap)return;
var html='';
CABINETS.forEach(function(cab,idx){
var statusTag=cab.status==='在线'?'<span class="tag tag-green">● 在线</span>'
:cab.status==='离线'?'<span class="tag tag-gray">● 离线</span>'
:'<span class="tag tag-red">● 故障</span>';
var typeTag=cab.cabinetType==='净菜柜'?'<span class="tag tag-blue">净菜柜</span>':'<span class="tag tag-green">毛菜柜</span>';
/* 统计:空闲 / 储物中 / 离线 格口数 */
var idleCount=cab.slots.filter(function(s){return s.status==='idle';}).length;
var occupiedCount=cab.slots.filter(function(s){return s.status==='occupied';}).length;
var offlineCount=cab.slots.filter(function(s){return s.status==='offline';}).length;
/* 格口横向一行 */
var cellsHtml='';
cab.slots.forEach(function(s,sidx){
var cls='cabinet-cell '+s.status;
var inner='';
if(s.status==='idle'){
inner='<div class="cabinet-cell__id">'+s.id+'</div><div class="cabinet-cell__name">空闲</div>';
}else if(s.status==='offline'){
inner='<div class="cabinet-cell__id">'+s.id+'</div><div class="cabinet-cell__name">'+(s.itemName||'—')+'</div><div class="cabinet-cell__qty" style="color:var(--text-placeholder);">离线</div>';
}else{
inner='<div class="cabinet-cell__id">'+s.id+'</div>'
+'<div class="cabinet-cell__name">'+s.itemName+'</div>'
+'<div class="cabinet-cell__qty">'+s.qty+'kg</div>';
}
cellsHtml+='<div class="'+cls+'" onclick="showSlotHistory('+idx+','+sidx+')">'+inner+'</div>';
});
html+='<div class="cabinet-card">'
+'<div class="cabinet-card__header">'
+'<span class="cabinet-card__name">'+cab.name+'</span>'
+'<span class="cabinet-card__code">'+cab.code+'</span>'
+'<span class="cabinet-card__sep">·</span>'
+'<span class="cabinet-card__meta">共 '+cab.totalSlots+' 格口</span>'
+'<span class="cabinet-card__sep">·</span>'
+'<span class="cabinet-card__stat"><span class="cabinet-card__stat-dot idle"></span>'+idleCount+' 空闲</span>'
+'<span class="cabinet-card__stat"><span class="cabinet-card__stat-dot storing"></span>'+occupiedCount+' 储物中</span>'
+(offlineCount>0?'<span class="cabinet-card__sep">·</span><span class="cabinet-card__meta" style="color:var(--text-placeholder);">'+offlineCount+' 离线</span>':'')
+'<div class="cabinet-card__tags">'+typeTag+statusTag+'</div>'
+'</div>'
+'<div class="cabinet-card__body">'
+'<div class="cabinet-card__slots">'+cellsHtml+'</div>'
+'</div>'
+'</div>';
});
wrap.innerHTML=html;
}
renderCabinets();
/* 轻量 toast(不依赖其他组件) */
function cabToast(msg){
var old=document.getElementById('cabToast');
if(old)old.remove();
var t=document.createElement('div');
t.id='cabToast';
t.textContent=msg;
t.style.cssText='position:fixed;top:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.78);color:#fff;padding:10px 20px;border-radius:6px;font-size:14px;z-index:3000;box-shadow:0 4px 12px rgba(0,0,0,.2);';
document.body.appendChild(t);
setTimeout(function(){if(t)t.remove();},2200);
}
/* 格口历史记录 Mock:键格式「柜编号|格口号」 */
var SLOT_HISTORY={
'CAB-NC-001|NC3-01':[
{time:'2026-04-22 14:20',itemType:'净菜',itemName:'净土豆丝',type:'取出',before:11.0,after:10.2,op:'王厨师',inOrder:'IN-20260415-001',remark:'做土豆丝炒肉'},
{time:'2026-04-22 11:30',itemType:'净菜',itemName:'净土豆丝',type:'取出',before:11.8,after:11.0,op:'李主管',inOrder:'IN-20260415-001',remark:'做宫保鸡丁'},
{time:'2026-04-22 09:15',itemType:'净菜',itemName:'净土豆丝',type:'存入',before:0.0,after:12.5,op:'张厨师',inOrder:'IN-20260415-001',remark:'首次入柜'}
],
'CAB-NC-001|NC3-02':[
{time:'2026-04-22 15:40',itemType:'净菜',itemName:'净胡萝卜',type:'取出',before:16.8,after:15.6,op:'王厨师',inOrder:'IN-20260415-002',remark:'做胡萝卜炒肉'},
{time:'2026-04-22 09:20',itemType:'净菜',itemName:'净胡萝卜',type:'存入',before:0.0,after:18.0,op:'李主管',inOrder:'IN-20260415-002',remark:'首次入柜'}
],
'CAB-NC-001|NC3-03':[
{time:'2026-04-22 16:10',itemType:'净菜',itemName:'净香菇',type:'取出',before:7.5,after:6.8,op:'张厨师',inOrder:'IN-20260415-018',remark:'做香菇青菜'},
{time:'2026-04-22 14:00',itemType:'净菜',itemName:'净香菇',type:'取出',before:8.2,after:7.5,op:'张厨师',inOrder:'IN-20260415-018',remark:'做香菇青菜'},
{time:'2026-04-22 09:30',itemType:'净菜',itemName:'净香菇',type:'存入',before:0.0,after:9.0,op:'王厨师',inOrder:'IN-20260415-018',remark:'首次入柜'}
],
'CAB-NC-001|NC3-04':[
{time:'2026-04-22 15:30',itemType:'净菜',itemName:'净西兰花',type:'取出',before:9.0,after:8.5,op:'李主管',inOrder:'IN-20260415-022',remark:'做西兰花炒虾仁'},
{time:'2026-04-22 09:40',itemType:'净菜',itemName:'净西兰花',type:'存入',before:0.0,after:10.0,op:'张厨师',inOrder:'IN-20260415-022',remark:'首次入柜'}
],
'CAB-NC-001|NC3-05':[
{time:'2026-04-22 11:10',itemType:'净菜',itemName:'净白菜',type:'取出',before:18.7,after:18.0,op:'王厨师',inOrder:'IN-20260415-006',remark:'取样检测'},
{time:'2026-04-21 17:30',itemType:'净菜',itemName:'净白菜',type:'取出',before:20.5,after:18.7,op:'李主管',inOrder:'IN-20260415-006',remark:'晚餐备菜'},
{time:'2026-04-20 09:30',itemType:'净菜',itemName:'净白菜',type:'取出',before:22.5,after:20.5,op:'张厨师',inOrder:'IN-20260415-006',remark:'午餐备菜'},
{time:'2026-04-18 14:30',itemType:'净菜',itemName:'净白菜',type:'存入',before:0.0,after:23.0,op:'王厨师',inOrder:'IN-20260415-006',remark:'首次入柜'}
],
'CAB-NC-002|NC5-01':[
{time:'2026-04-22 13:20',itemType:'净菜',itemName:'净黄瓜片',type:'取出',before:8.0,after:7.4,op:'李主管',inOrder:'IN-20260416-031',remark:'凉拌黄瓜'},
{time:'2026-04-22 10:05',itemType:'净菜',itemName:'净黄瓜片',type:'存入',before:0.0,after:9.5,op:'李主管',inOrder:'IN-20260416-031',remark:'首次入柜'}
],
'CAB-NC-002|NC5-02':[
{time:'2026-04-22 12:00',itemType:'净菜',itemName:'净西红柿',type:'取出',before:13.5,after:12.0,op:'张厨师',inOrder:'IN-20260416-032',remark:'西红柿炒蛋'},
{time:'2026-04-22 10:15',itemType:'净菜',itemName:'净西红柿',type:'存入',before:0.0,after:15.0,op:'张厨师',inOrder:'IN-20260416-032',remark:'首次入柜'}
],
'CAB-NC-002|NC5-03':[
{time:'2026-04-22 15:00',itemType:'净菜',itemName:'净洋葱',type:'取出',before:3.8,after:3.2,op:'王厨师',inOrder:'IN-20260416-033',remark:'洋葱炒蛋'},
{time:'2026-04-22 08:40',itemType:'净菜',itemName:'净洋葱',type:'存入',before:0.0,after:5.0,op:'王厨师',inOrder:'IN-20260416-033',remark:'首次入柜'}
],
'CAB-RW-008|RW8-A1':[
{time:'2026-04-16 09:00',itemType:'毛菜',itemName:'菠菜',type:'取出',before:248.0,after:225.0,op:'张仓管',inOrder:'IN-20260415-001',remark:'净菜加工领用'},
{time:'2026-04-15 10:30',itemType:'毛菜',itemName:'菠菜',type:'存入',before:0.0,after:286.0,op:'张仓管',inOrder:'IN-20260415-001',remark:'毛菜入库'}
],
'CAB-RW-008|RW8-C1':[
{time:'2026-04-16 08:00',itemType:'毛菜',itemName:'芹菜',type:'取出',before:18.0,after:12.0,op:'张仓管',inOrder:'IN-20260414-009',remark:'领用加工'}
]
};
/* 格口历史居中大弹窗组件(1100×80vh) */
var HistoryModal={
open:function(title,bodyHtml,footerHtml){
this.close();
var mask=document.createElement('div');
mask.className='history-modal-mask';
var html='<div class="history-modal">'
+'<div class="history-modal__header"><div class="history-modal__title">'+title+'</div>'
+'<button class="history-modal__close" onclick="HistoryModal.close()">&times;</button></div>'
+'<div class="history-modal__body">'+bodyHtml+'</div>';
if(footerHtml){html+='<div class="history-modal__footer">'+footerHtml+'</div>';}
html+='</div>';
mask.innerHTML=html;
mask.addEventListener('click',function(e){if(e.target===mask)HistoryModal.close();});
document.body.appendChild(mask);
this._escHandler=function(e){if(e.key==='Escape')HistoryModal.close();};
document.addEventListener('keydown',this._escHandler);
},
close:function(){
var m=document.querySelector('.history-modal-mask');
if(!m)return;
m.classList.add('closing');
setTimeout(function(){m.remove();},200);
if(this._escHandler){document.removeEventListener('keydown',this._escHandler);this._escHandler=null;}
}
};
/* 点击格口:弹出居中历史弹窗(上半区格口信息+存储详情,下半区历史取用记录) */
var SLOT_HIST_CURRENT={cabIdx:0,slotIdx:0,key:'',page:1};
var SLOT_HIST_PAGE_SIZE=6;
function showSlotHistory(cabIdx,slotIdx){
var cab=CABINETS[cabIdx];
if(!cab)return;
var slot=cab.slots[slotIdx];
if(!slot)return;
/* 离线格口不响应 */
if(slot.status==='offline'){
cabToast('「'+cab.name+' · '+slot.id+'」设备离线,暂不可查看');
return;
}
SLOT_HIST_CURRENT={cabIdx:cabIdx,slotIdx:slotIdx,key:cab.code+'|'+slot.id,page:1};
/* 上半区:格口信息 + 当前存储详情 */
var typeTag=cab.cabinetType==='净菜柜'?'<span class="tag tag-blue">净菜柜</span>':'<span class="tag tag-green">毛菜柜</span>';
var statusTag=cab.status==='在线'?'<span class="tag tag-green">在线</span>':cab.status==='离线'?'<span class="tag tag-gray">离线</span>':'<span class="tag tag-red">故障</span>';
var slotTypeTag=slot.itemType?(slot.itemType==='净菜'?'<span class="tag tag-blue">净菜</span>':'<span class="tag tag-green">毛菜</span>'):'—';
var infoHtml='<div class="history-summary" style="grid-template-columns:none;">'
+'<div class="history-summary__item"><span class="label">所属柜:</span><strong>'+cab.name+'</strong></div>'
+'<div class="history-summary__item"><span class="label">设备编号:</span><strong>'+cab.code+'</strong></div>'
+'<div class="history-summary__item"><span class="label">货柜类型:</span><strong>'+typeTag+'</strong></div>'
+'<div class="history-summary__item"><span class="label">设备状态:</span><strong>'+statusTag+'</strong></div>'
+'<div class="history-summary__item"><span class="label">格口号:</span><strong style="font-family:monospace;">'+slot.id+'</strong></div>'
+'<div class="history-summary__item"><span class="label">格口状态:</span><strong>'+slotStatusTag(slot.status)+'</strong></div>'
+'</div>';
/* 当前存储详情卡片(仅占用格口有数据) */
var storeHtml='';
if(slot.status==='occupied'){
storeHtml='<div style="background:#FAFFF5;border:1px solid #B7EB8F;border-radius:var(--radius-md);padding:14px 16px;margin-bottom:16px;">'
+'<div style="font-size:var(--font-sm);font-weight:600;color:var(--success);margin-bottom:10px;display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:3px;height:14px;background:var(--success);border-radius:2px;"></span>当前存储详情</div>'
+'<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:10px 16px;">'
+'<div class="drawer-field"><span class="drawer-field__label">物品类型</span><span class="drawer-field__value">'+slotTypeTag+'</span></div>'
+'<div class="drawer-field"><span class="drawer-field__label">物品名称</span><span class="drawer-field__value">'+slot.itemName+'</span></div>'
+'<div class="drawer-field"><span class="drawer-field__label">余量(kg)</span><span class="drawer-field__value" style="color:var(--primary);font-weight:600;">'+slot.qty+'</span></div>'
+'<div class="drawer-field"><span class="drawer-field__label">操作人</span><span class="drawer-field__value">'+slot.op+'</span></div>'
+'<div class="drawer-field"><span class="drawer-field__label">存入时间</span><span class="drawer-field__value">'+slot.inTime+'</span></div>'
+'</div></div>';
}else{
storeHtml='<div style="background:#FAFAFA;border:1px dashed var(--border);border-radius:var(--radius-md);padding:14px 16px;margin-bottom:16px;color:var(--text-placeholder);font-size:var(--font-sm);text-align:center;">该格口当前为空闲状态,暂无存储物品</div>';
}
HistoryModal.open('格口历史记录 — '+slot.id, infoHtml+storeHtml+'<div id="slotHistTableWrap"></div>', '<div id="slotHistFooterWrap"></div>');
renderSlotHistoryPage();
}
function renderSlotHistoryPage(){
var wrap=document.getElementById('slotHistTableWrap');
var footer=document.getElementById('slotHistFooterWrap');
if(!wrap)return;
var rows=(SLOT_HISTORY[SLOT_HIST_CURRENT.key]||[]).slice().sort(function(a,b){return b.time.localeCompare(a.time);});
var total=rows.length;
var totalPages=Math.max(1,Math.ceil(total/SLOT_HIST_PAGE_SIZE));
if(SLOT_HIST_CURRENT.page>totalPages)SLOT_HIST_CURRENT.page=totalPages;
if(SLOT_HIST_CURRENT.page<1)SLOT_HIST_CURRENT.page=1;
var start=(SLOT_HIST_CURRENT.page-1)*SLOT_HIST_PAGE_SIZE;
var pageRows=rows.slice(start,start+SLOT_HIST_PAGE_SIZE);
var html='<div style="font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:8px;display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:3px;height:14px;background:var(--primary);border-radius:2px;"></span>历史取用记录</div>';
html+='<div style="overflow-x:auto"><table class="history-table"><thead><tr>'
+'<th>操作时间</th><th>物品类型</th><th>物品名称</th><th>操作类型</th><th>操作前(kg)</th><th>操作后(kg)</th><th>余量变化</th><th>操作人</th><th>入库单号</th><th>备注</th>'
+'</tr></thead><tbody>';
if(pageRows.length===0){
html+='<tr><td colspan="10" style="text-align:center;color:var(--text-placeholder);padding:24px;">该格口暂无历史记录</td></tr>';
}else{
pageRows.forEach(function(r){
var delta=Math.abs(r.after-r.before).toFixed(1);
var isPos=r.type==='存入';
var deltaHtml='<span class="history-delta '+(isPos?'pos':'neg')+'">'+(isPos?'+':'-')+delta+' '+(isPos?'↑':'↓')+'</span>';
var typeHtml=r.type==='存入'?'<span class="tag tag-blue">存入</span>':'<span class="tag tag-orange">取出</span>';
var itemTypeHtml=r.itemType==='净菜'?'<span class="tag tag-blue">净菜</span>':'<span class="tag tag-green">毛菜</span>';
html+='<tr><td>'+r.time+'</td><td>'+itemTypeHtml+'</td><td>'+r.itemName+'</td><td>'+typeHtml+'</td><td>'+r.before.toFixed(1)+'</td><td>'+r.after.toFixed(1)+'</td><td>'+deltaHtml+'</td><td>'+r.op+'</td><td style="color:var(--primary);">'+r.inOrder+'</td><td>'+(r.remark||'—')+'</td></tr>';
});
}
html+='</tbody></table></div>';
wrap.innerHTML=html;
if(footer){
var pgHtml='<div class="history-pagination"><span class="history-pagination__total">共 '+total+' 条</span>';
pgHtml+='<button class="page-btn" onclick="goSlotHistPage('+(SLOT_HIST_CURRENT.page-1)+')" '+(SLOT_HIST_CURRENT.page<=1?'disabled style="opacity:.4;cursor:not-allowed"':'')+'></button>';
for(var i=1;i<=totalPages;i++){
pgHtml+='<button class="page-btn'+(i===SLOT_HIST_CURRENT.page?' active':'')+'" onclick="goSlotHistPage('+i+')">'+i+'</button>';
}
pgHtml+='<button class="page-btn" onclick="goSlotHistPage('+(SLOT_HIST_CURRENT.page+1)+')" '+(SLOT_HIST_CURRENT.page>=totalPages?'disabled style="opacity:.4;cursor:not-allowed"':'')+'></button>';
pgHtml+='</div>';
footer.innerHTML=pgHtml;
}
}
function goSlotHistPage(p){
SLOT_HIST_CURRENT.page=p;
renderSlotHistoryPage();
}
</script>
</body>