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
+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>