feat: 订餐终端设备概览弹窗 + 会员权益/采购库存抵扣/追溯多锅次分支 + 设备端统计与验收改造
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b4dfe31b25
commit
792567ac03
@@ -146,6 +146,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<option>营养值超范围</option>
|
||||
<option>算法超时</option>
|
||||
<option>模型输出异常</option>
|
||||
<option>设备告警</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
@@ -162,7 +163,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-primary"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 10v2h10v-2M7 2v7M4.5 6.5L7 9l2.5-2.5" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>导出列表数据</button>
|
||||
<button class="btn btn-default"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4"/><path d="M4 5h6M4 7.5h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>查看导出任务</button>
|
||||
<div class="action-bar__right">共 <strong>89</strong> 条异常记录</div>
|
||||
<div class="action-bar__right">共 <strong>91</strong> 条异常记录</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
@@ -282,13 +283,33 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<td>2026-05-11 08:40</td>
|
||||
<td class="td-actions"><a class="link">详情</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ERR-2026-011</td>
|
||||
<td><span class="tag tag-orange">设备告警</span></td>
|
||||
<td>人脸识别</td>
|
||||
<td>档口秤 FCD-B007(2 号档口)于 2026-05-10 11:30~11:55 期间人脸识别连续失败 12 次,触发设备告警</td>
|
||||
<td><span class="tag tag-orange">中</span></td>
|
||||
<td>v1.5.0</td>
|
||||
<td>2026-05-10 11:55</td>
|
||||
<td class="td-actions"><a class="link">详情</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ERR-2026-012</td>
|
||||
<td><span class="tag tag-orange">设备告警</span></td>
|
||||
<td>人脸识别</td>
|
||||
<td>餐线秤 FCD-A012(3 号窗口)于 2026-05-09 17:20~18:10 期间人脸识别累计失败 28 次(阈值 20 次),触发设备告警</td>
|
||||
<td><span class="tag tag-red">高</span></td>
|
||||
<td>v1.5.0</td>
|
||||
<td>2026-05-09 18:10</td>
|
||||
<td class="td-actions"><a class="link">详情</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<div class="pagination__left">
|
||||
<span>共 89 条</span>
|
||||
<span>共 91 条</span>
|
||||
<span>每页</span>
|
||||
<select class="filter-select" style="min-width:70px;">
|
||||
<option>10</option>
|
||||
|
||||
@@ -171,6 +171,32 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
|
||||
.adv-range{display:flex;align-items:center;gap:8px;}
|
||||
.adv-range .form-input{flex:1;min-width:0;}
|
||||
.adv-range__sep{color:var(--text-placeholder);font-size:var(--font-sm);flex-shrink:0;}
|
||||
/* 食材选择器(可搜索下拉) */
|
||||
.ingredient-picker{position:relative;}
|
||||
.ingredient-picker__trigger{display:flex;align-items:center;gap:6px;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 12px;background:#fff;cursor:pointer;font-size:var(--font-sm);color:var(--text-primary);position:relative;transition:border-color .15s;}
|
||||
.ingredient-picker__trigger:hover{border-color:var(--primary);}
|
||||
.ingredient-picker__trigger.open{border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.12);}
|
||||
.ingredient-picker__trigger-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.ingredient-picker__trigger-text.is-empty{color:var(--text-placeholder);}
|
||||
.ingredient-picker__trigger-code{font-size:var(--font-xs);color:var(--primary);background:var(--primary-light);padding:1px 6px;border-radius:8px;flex-shrink:0;}
|
||||
.ingredient-picker__trigger-sep{color:var(--text-placeholder);flex-shrink:0;}
|
||||
.ingredient-picker__trigger-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.ingredient-picker__trigger-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:18px;height:18px;display:flex;align-items:center;justify-content:center;border-radius:50%;color:var(--text-placeholder);font-size:12px;cursor:pointer;background:#fff;border:1px solid var(--border);}
|
||||
.ingredient-picker__trigger-clear:hover{color:var(--danger);border-color:var(--danger);}
|
||||
.ingredient-picker__trigger-arrow{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:10px;height:6px;pointer-events:none;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat center;}
|
||||
.ingredient-picker__trigger.open .ingredient-picker__trigger-arrow{transform:translateY(-50%) rotate(180deg);}
|
||||
.ingredient-picker__panel{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-md);z-index:50;overflow:hidden;}
|
||||
.ingredient-picker.open .ingredient-picker__panel{display:block;}
|
||||
.ingredient-picker__search{padding:8px;border-bottom:1px solid var(--border);}
|
||||
.ingredient-picker__search input{width:100%;height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 10px;font-size:var(--font-sm);outline:none;}
|
||||
.ingredient-picker__search input:focus{border-color:var(--primary);}
|
||||
.ingredient-picker__list{max-height:220px;overflow-y:auto;padding:4px 0;}
|
||||
.ingredient-picker__item{display:flex;align-items:center;gap:8px;padding:7px 12px;cursor:pointer;font-size:var(--font-sm);color:var(--text-primary);transition:background .12s;}
|
||||
.ingredient-picker__item:hover{background:var(--primary-light);}
|
||||
.ingredient-picker__item.selected{background:var(--primary-light);color:var(--primary);font-weight:500;}
|
||||
.ingredient-picker__item-code{font-size:var(--font-xs);color:var(--primary);background:var(--primary-light);padding:1px 6px;border-radius:8px;flex-shrink:0;font-family:Consolas,Monaco,monospace;}
|
||||
.ingredient-picker__item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.ingredient-picker__empty{padding:16px 12px;text-align:center;color:var(--text-placeholder);font-size:var(--font-xs);}
|
||||
/* 操作列固定右侧 */
|
||||
thead th:last-child,tbody td.td-actions{position:sticky;right:0;background:#fff;box-shadow:-4px 0 8px rgba(0,0,0,0.04);z-index:2;white-space:nowrap;}
|
||||
thead th:last-child{background:#FAFAFA;}
|
||||
@@ -470,6 +496,20 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<option>低脂</option><option>高蛋白</option><option>素食</option><option>低卡</option><option>高纤维</option><option>低GI</option><option>高糖</option><option>无麸质</option><option>主食</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group ingredient-picker" id="advIngredientPicker">
|
||||
<label>食材</label>
|
||||
<div class="ingredient-picker__trigger" onclick="toggleIngredientPicker(event)">
|
||||
<span class="ingredient-picker__trigger-text is-empty" id="advIngredientText">请选择食材</span>
|
||||
<span class="ingredient-picker__trigger-clear" id="advIngredientClear" style="display:none;" onclick="clearIngredient(event)" title="清除">✕</span>
|
||||
<span class="ingredient-picker__trigger-arrow"></span>
|
||||
</div>
|
||||
<div class="ingredient-picker__panel">
|
||||
<div class="ingredient-picker__search">
|
||||
<input type="text" id="advIngredientSearch" placeholder="输入编码或名称搜索" oninput="filterIngredients()">
|
||||
</div>
|
||||
<div class="ingredient-picker__list" id="advIngredientList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label>上架状态</label>
|
||||
<select class="form-select" id="advStatus">
|
||||
<option value="">全部</option>
|
||||
@@ -593,6 +633,23 @@ let currentTab = 0;
|
||||
let filteredDishes = [];
|
||||
/* 高级搜索已应用条件(用于汇总条显示) */
|
||||
let appliedAdvSearch = null;
|
||||
/* 食材候选列表:从所有餐品的 composition 中提取 {code,name} 并按 code 去重 */
|
||||
var INGREDIENT_OPTIONS = (function(){
|
||||
var map = {};
|
||||
DISHES.forEach(function(d){
|
||||
(d.composition||[]).forEach(function(c){
|
||||
if(c && c.code && c.name && !map[c.code]){
|
||||
map[c.code] = {code:c.code, name:c.name};
|
||||
}
|
||||
});
|
||||
});
|
||||
return Object.keys(map).map(function(k){return map[k];}).sort(function(a,b){
|
||||
var ca = a.code, cb = b.code;
|
||||
return ca < cb ? -1 : (ca > cb ? 1 : 0);
|
||||
});
|
||||
})();
|
||||
/* 高级搜索弹窗中食材选择器的当前选中食材编码(null 表示未选) */
|
||||
var advIngredientSelected = null;
|
||||
|
||||
function renderDishList(){
|
||||
document.getElementById('dishList').innerHTML = filteredDishes.map((entry)=>{const d=entry.dish;const i=entry.idx;return `
|
||||
@@ -1012,6 +1069,7 @@ function matchAdvanced(d, q){
|
||||
if(q.category && d.category!==q.category) return false;
|
||||
if(q.meal && (d.meal||'')!==q.meal) return false;
|
||||
if(q.tag && !(d.tags||[]).some(function(t){return t===q.tag;})) return false;
|
||||
if(q.ingredient && !(d.composition||[]).some(function(c){return c.code===q.ingredient;})) return false;
|
||||
if(q.status && d.status!==q.status) return false;
|
||||
/* 食材成本区间 */
|
||||
var cost = (d.pricing && d.pricing.ingredientCost!==undefined) ? d.pricing.ingredientCost : null;
|
||||
@@ -1040,10 +1098,17 @@ function updateFilterSummary(){
|
||||
Object.keys(appliedAdvSearch).forEach(function(k){
|
||||
var v = appliedAdvSearch[k];
|
||||
if(v===null || v===undefined || v==='') return;
|
||||
var labelMap = {name:'名称',code:'编号',category:'分类',meal:'餐次',tag:'标签',status:'状态',
|
||||
var labelMap = {name:'名称',code:'编号',category:'分类',meal:'餐次',tag:'标签',status:'状态',ingredient:'食材',
|
||||
costMin:'成本≥',costMax:'成本≤',priceMin:'售价≥',priceMax:'售价≤',
|
||||
updatedStart:'更新≥',updatedEnd:'更新≤',createdStart:'创建≥',createdEnd:'创建≤'};
|
||||
var label = labelMap[k] || k;
|
||||
/* 食材字段:v 为食材编码,需展示为 "[编码] 名称" 格式 */
|
||||
if(k==='ingredient'){
|
||||
var found = INGREDIENT_OPTIONS.filter(function(it){return it.code===v;})[0];
|
||||
var dispName = found ? found.name : v;
|
||||
bits.push(label + ' ['+v+'] ' + dispName);
|
||||
return;
|
||||
}
|
||||
var suffix = (k.indexOf('Min')>=0 || k.indexOf('Max')>=0) ? (k.indexOf('cost')>=0?'元':(k.indexOf('price')>=0?'元':'')) : '';
|
||||
bits.push(label + v + suffix);
|
||||
});
|
||||
@@ -1080,6 +1145,15 @@ function openAdvancedSearchDialog(){
|
||||
setVal('advUpdatedEnd', q.updatedEnd||'');
|
||||
setVal('advCreatedStart', q.createdStart||'');
|
||||
setVal('advCreatedEnd', q.createdEnd||'');
|
||||
/* 回填食材选择器 */
|
||||
advIngredientSelected = q.ingredient || null;
|
||||
updateIngredientTrigger();
|
||||
/* 清空食材搜索框 */
|
||||
var ingSearch = document.getElementById('advIngredientSearch');
|
||||
if(ingSearch) ingSearch.value = '';
|
||||
/* 关闭食材面板(避免上次状态残留) */
|
||||
var picker = document.getElementById('advIngredientPicker');
|
||||
if(picker) picker.classList.remove('open');
|
||||
document.getElementById('advancedSearchDialog').classList.add('open');
|
||||
}
|
||||
function closeAdvancedSearchDialog(){
|
||||
@@ -1087,6 +1161,11 @@ function closeAdvancedSearchDialog(){
|
||||
}
|
||||
function resetAdvancedSearch(){
|
||||
['advName','advCode','advCategory','advMeal','advTag','advStatus','advCostMin','advCostMax','advPriceMin','advPriceMax','advUpdatedStart','advUpdatedEnd','advCreatedStart','advCreatedEnd'].forEach(function(id){ setVal(id,''); });
|
||||
/* 重置食材选择器 */
|
||||
advIngredientSelected = null;
|
||||
updateIngredientTrigger();
|
||||
var ingSearch = document.getElementById('advIngredientSearch');
|
||||
if(ingSearch) ingSearch.value = '';
|
||||
}
|
||||
function applyAdvancedSearch(){
|
||||
appliedAdvSearch = {
|
||||
@@ -1096,6 +1175,7 @@ function applyAdvancedSearch(){
|
||||
meal: getVal('advMeal'),
|
||||
tag: getVal('advTag'),
|
||||
status: getVal('advStatus'),
|
||||
ingredient: advIngredientSelected || '',
|
||||
costMin: getNum('advCostMin'),
|
||||
costMax: getNum('advCostMax'),
|
||||
priceMin: getNum('advPriceMin'),
|
||||
@@ -1123,6 +1203,105 @@ function setVal(id, v){ var el = document.getElementById(id); if(el) el.value =
|
||||
function getVal(id){ var el = document.getElementById(id); return el ? el.value.trim() : ''; }
|
||||
function getNum(id){ var v = getVal(id); if(v==='') return null; var n = parseFloat(v); return isNaN(n) ? null : n; }
|
||||
|
||||
/* ========== 食材选择器(可搜索下拉) ========== */
|
||||
/* 切换食材下拉面板的展开/收起状态 */
|
||||
function toggleIngredientPicker(event){
|
||||
if(event){ event.stopPropagation(); }
|
||||
var picker = document.getElementById('advIngredientPicker');
|
||||
if(!picker) return;
|
||||
var isOpen = picker.classList.contains('open');
|
||||
if(isOpen){
|
||||
picker.classList.remove('open');
|
||||
return;
|
||||
}
|
||||
picker.classList.add('open');
|
||||
/* 展开时聚焦搜索框并重新渲染候选列表 */
|
||||
var searchInput = document.getElementById('advIngredientSearch');
|
||||
if(searchInput){
|
||||
setTimeout(function(){ searchInput.focus(); }, 0);
|
||||
}
|
||||
renderIngredientList('');
|
||||
}
|
||||
|
||||
/* 根据搜索关键词过滤并渲染候选列表 */
|
||||
function filterIngredients(){
|
||||
var kw = (document.getElementById('advIngredientSearch').value || '').trim().toLowerCase();
|
||||
renderIngredientList(kw);
|
||||
}
|
||||
|
||||
/* 渲染食材候选列表(传入已过滤关键词) */
|
||||
function renderIngredientList(kw){
|
||||
var listEl = document.getElementById('advIngredientList');
|
||||
if(!listEl) return;
|
||||
var filtered = INGREDIENT_OPTIONS.filter(function(it){
|
||||
if(!kw) return true;
|
||||
return (it.code||'').toLowerCase().indexOf(kw)>=0 || (it.name||'').toLowerCase().indexOf(kw)>=0;
|
||||
});
|
||||
if(filtered.length===0){
|
||||
listEl.innerHTML = '<div class="ingredient-picker__empty">未找到匹配的食材</div>';
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = filtered.map(function(it){
|
||||
var sel = advIngredientSelected && advIngredientSelected===it.code ? ' selected' : '';
|
||||
return '<div class="ingredient-picker__item'+sel+'" onclick="selectIngredient(\''+it.code+'\')">'
|
||||
+ '<span class="ingredient-picker__item-code">'+it.code+'</span>'
|
||||
+ '<span class="ingredient-picker__item-name">'+it.name+'</span>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* 选中某条食材 */
|
||||
function selectIngredient(code){
|
||||
advIngredientSelected = code;
|
||||
updateIngredientTrigger();
|
||||
/* 关闭面板 */
|
||||
var picker = document.getElementById('advIngredientPicker');
|
||||
if(picker) picker.classList.remove('open');
|
||||
/* 清空搜索框,便于下次重新展开 */
|
||||
var searchInput = document.getElementById('advIngredientSearch');
|
||||
if(searchInput) searchInput.value = '';
|
||||
}
|
||||
|
||||
/* 更新触发器显示(空态/已选) */
|
||||
function updateIngredientTrigger(){
|
||||
var textEl = document.getElementById('advIngredientText');
|
||||
var clearEl = document.getElementById('advIngredientClear');
|
||||
var trigger = textEl ? textEl.parentElement : null;
|
||||
if(!textEl || !clearEl) return;
|
||||
if(!advIngredientSelected){
|
||||
textEl.textContent = '请选择食材';
|
||||
textEl.className = 'ingredient-picker__trigger-text is-empty';
|
||||
clearEl.style.display = 'none';
|
||||
if(trigger) trigger.classList.remove('has-value');
|
||||
return;
|
||||
}
|
||||
var found = INGREDIENT_OPTIONS.filter(function(it){return it.code===advIngredientSelected;})[0];
|
||||
var name = found ? found.name : advIngredientSelected;
|
||||
textEl.innerHTML = '<span class="ingredient-picker__trigger-code">'+advIngredientSelected+'</span>'
|
||||
+ '<span class="ingredient-picker__trigger-sep">/</span>'
|
||||
+ '<span class="ingredient-picker__trigger-name">'+name+'</span>';
|
||||
textEl.className = 'ingredient-picker__trigger-text';
|
||||
clearEl.style.display = 'flex';
|
||||
if(trigger) trigger.classList.add('has-value');
|
||||
}
|
||||
|
||||
/* 清除已选食材 */
|
||||
function clearIngredient(event){
|
||||
if(event){ event.stopPropagation(); }
|
||||
advIngredientSelected = null;
|
||||
updateIngredientTrigger();
|
||||
}
|
||||
|
||||
/* 点击食材下拉面板外部时关闭面板(挂在 document 上,只在弹窗打开期间生效) */
|
||||
document.addEventListener('click', function(e){
|
||||
var picker = document.getElementById('advIngredientPicker');
|
||||
if(!picker) return;
|
||||
if(!picker.classList.contains('open')) return;
|
||||
/* 点击在面板内则保留 */
|
||||
if(picker.contains(e.target)) return;
|
||||
picker.classList.remove('open');
|
||||
});
|
||||
|
||||
/* ========== 餐品分类管理 ========== */
|
||||
var nextDishCatId = 100;
|
||||
var dishCategories = [
|
||||
|
||||
@@ -300,6 +300,8 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<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><option>工作人员</option><option>外部人员</option></select></div>
|
||||
<div class="filter-item"><label>日期范围</label><input class="filter-input" type="date" style="min-width:130px;"> <span style="margin:0 4px;color:#999;">至</span> <input class="filter-input" type="date" style="min-width:130px;"></div>
|
||||
<!-- 交互注释:重量偏差范围筛选,单位克,支持负值(如 -20 ~ 20),按"实际重量-结算重量"的偏差值过滤记录;未复秤记录不参与该筛选 -->
|
||||
<div class="filter-item"><label>重量偏差</label><input class="filter-input" type="number" placeholder="最小值(克)" style="min-width:100px;"> <span style="margin:0 4px;color:#999;">至</span> <input class="filter-input" type="number" placeholder="最大值(克)" style="min-width:100px;"></div>
|
||||
<button class="btn btn-primary">查询</button>
|
||||
<button class="btn btn-default">重置</button>
|
||||
</div>
|
||||
@@ -318,18 +320,19 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="table-card__content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>就餐记录号</th><th>姓名</th><th>员工编号</th><th>单位</th><th>部门</th><th>餐次</th><th>取餐方式</th><th>取餐类型</th><th>人员类别</th><th>取餐开始时间</th><th>取餐结束时间</th><th>取餐总数量</th><th>总金额</th><th>营养评分</th><th>操作</th></tr>
|
||||
<!-- 交互注释:结算重量=终端结算时电子秤计重;实际重量=结算后复秤电子秤上传的实际重量;重量偏差=实际重量-结算重量,偏差超过±10克(含)显示橙色预警、超过±15克显示红色异常,未复秤显示"—" -->
|
||||
<tr><th>就餐记录号</th><th>姓名</th><th>员工编号</th><th>单位</th><th>部门</th><th>餐次</th><th>取餐方式</th><th>取餐类型</th><th>人员类别</th><th>取餐开始时间</th><th>取餐结束时间</th><th>取餐总数量</th><th>结算重量(克)</th><th>实际重量(克)</th><th>重量偏差(克)</th><th>总金额</th><th>营养评分</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>M-20260428-001</td><td>张伟</td><td>EMP001</td><td>CQ能源</td><td>研发部</td><td>午餐</td><td><span class="tag tag-blue">点餐取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:02</td><td>11:08</td><td><a class="link">3件</a></td><td>¥22.00</td><td><span class="tag tag-green">88分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-002</td><td>李娜</td><td>EMP002</td><td>CQ能源</td><td>市场部</td><td>午餐</td><td><span class="tag tag-blue">点餐取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:08</td><td>11:14</td><td><a class="link">3件</a></td><td>¥18.00</td><td><span class="tag tag-green">92分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-003</td><td>王磊</td><td>EMP003</td><td>CQ能源</td><td>运营部</td><td>午餐</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-gray">非会员</span></td><td>11:15</td><td>11:20</td><td><a class="link">2件</a></td><td>¥28.00</td><td><span class="tag tag-orange">68分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-004</td><td>赵敏</td><td>EMP004</td><td>CQ能源</td><td>财务部</td><td>午餐</td><td><span class="tag tag-green">档口+餐线</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:22</td><td>11:30</td><td><a class="link">3件</a></td><td>¥25.50</td><td><span class="tag tag-green">94分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-005</td><td>陈刚</td><td>EMP005</td><td>CQ能源</td><td>人事部</td><td>早餐</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-gray">非会员</span></td><td>07:48</td><td>07:55</td><td><a class="link">3件</a></td><td>¥12.00</td><td><span class="tag tag-green">85分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-006</td><td>刘芳</td><td>EMP006</td><td>CQ能源</td><td>市场部</td><td>午餐</td><td><span class="tag tag-blue">外卖/外带</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:45</td><td>11:48</td><td><a class="link">2件</a></td><td>¥15.80</td><td><span class="tag tag-green">90分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>SP-001-001</td><td>张运维</td><td>EMP101</td><td>CQ能源</td><td>运维部</td><td>午餐</td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-blue">员工</span></td><td>14:05</td><td>14:06</td><td><a class="link">1件</a></td><td>¥5.00</td><td>—</td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>SP-001-003</td><td>王保安</td><td>—</td><td>CQ能源</td><td>安保部</td><td>午餐</td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-blue">工作人员</span></td><td>14:10</td><td>14:11</td><td><a class="link">1件</a></td><td>¥5.00</td><td>—</td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-007</td><td>林教授</td><td>—</td><td>外部单位</td><td>外部访客</td><td>午餐</td><td><span class="tag tag-blue">点餐取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-purple">外部人员</span></td><td>12:10</td><td>12:18</td><td><a class="link">3件</a></td><td>¥26.50</td><td><span class="tag tag-green">86分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-001</td><td>张伟</td><td>EMP001</td><td>CQ能源</td><td>研发部</td><td>午餐</td><td><span class="tag tag-blue">点餐取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:02</td><td>11:08</td><td><a class="link">3件</a></td><td>326.0</td><td>338.5</td><td><span class="tag tag-orange">+12.5</span></td><td>¥22.00</td><td><span class="tag tag-green">88分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-002</td><td>李娜</td><td>EMP002</td><td>CQ能源</td><td>市场部</td><td>午餐</td><td><span class="tag tag-blue">点餐取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:08</td><td>11:14</td><td><a class="link">3件</a></td><td>365.0</td><td>363.0</td><td><span class="tag tag-green">-2.0</span></td><td>¥18.00</td><td><span class="tag tag-green">92分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-003</td><td>王磊</td><td>EMP003</td><td>CQ能源</td><td>运营部</td><td>午餐</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-gray">非会员</span></td><td>11:15</td><td>11:20</td><td><a class="link">2件</a></td><td>285.0</td><td>285.5</td><td><span class="tag tag-green">+0.5</span></td><td>¥28.00</td><td><span class="tag tag-orange">68分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-004</td><td>赵敏</td><td>EMP004</td><td>CQ能源</td><td>财务部</td><td>午餐</td><td><span class="tag tag-green">档口+餐线</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:22</td><td>11:30</td><td><a class="link">3件</a></td><td>420.0</td><td>447.0</td><td><span class="tag tag-red">+27.0</span></td><td>¥25.50</td><td><span class="tag tag-green">94分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-005</td><td>陈刚</td><td>EMP005</td><td>CQ能源</td><td>人事部</td><td>早餐</td><td><span class="tag tag-gray">餐线取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-gray">非会员</span></td><td>07:48</td><td>07:55</td><td><a class="link">3件</a></td><td>390.0</td><td>—</td><td><span class="tag tag-gray">未复秤</span></td><td>¥12.00</td><td><span class="tag tag-green">85分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-006</td><td>刘芳</td><td>EMP006</td><td>CQ能源</td><td>市场部</td><td>午餐</td><td><span class="tag tag-blue">外卖/外带</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-orange">会员</span></td><td>11:45</td><td>11:48</td><td><a class="link">2件</a></td><td>380.0</td><td>362.0</td><td><span class="tag tag-red">-18.0</span></td><td>¥15.80</td><td><span class="tag tag-green">90分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>SP-001-001</td><td>张运维</td><td>EMP101</td><td>CQ能源</td><td>运维部</td><td>午餐</td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-blue">员工</span></td><td>14:05</td><td>14:06</td><td><a class="link">1件</a></td><td>220.0</td><td>—</td><td><span class="tag tag-gray">未复秤</span></td><td>¥5.00</td><td>—</td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>SP-001-003</td><td>王保安</td><td>—</td><td>CQ能源</td><td>安保部</td><td>午餐</td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-orange">余量取餐</span></td><td><span class="tag tag-blue">工作人员</span></td><td>14:10</td><td>14:11</td><td><a class="link">1件</a></td><td>195.0</td><td>198.0</td><td><span class="tag tag-green">+3.0</span></td><td>¥5.00</td><td>—</td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
<tr><td>M-20260428-007</td><td>林教授</td><td>—</td><td>外部单位</td><td>外部访客</td><td>午餐</td><td><span class="tag tag-blue">点餐取餐</span></td><td><span class="tag tag-green">正常取餐</span></td><td><span class="tag tag-purple">外部人员</span></td><td>12:10</td><td>12:18</td><td><a class="link">3件</a></td><td>475.0</td><td>478.0</td><td><span class="tag tag-green">+3.0</span></td><td>¥26.50</td><td><span class="tag tag-green">86分</span></td><td class="td-actions"><a class="link">查看用户信息</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -144,6 +144,167 @@ tbody tr:hover{background:#F8FBFF;}
|
||||
.confirm-icon svg{width:48px;height:48px;}
|
||||
.confirm-msg{text-align:center;font-size:var(--font-base);color:var(--text-primary);margin-bottom:8px;font-weight:500;}
|
||||
.confirm-sub{text-align:center;font-size:var(--font-sm);color:var(--text-secondary);}
|
||||
/* ===== 设备概览大弹窗 ===== */
|
||||
.ov-modal-mask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1020;display:flex;align-items:center;justify-content:center;animation:ovFadeIn .2s ease;}
|
||||
.ov-modal-mask.closing{animation:ovFadeOut .2s ease forwards;}
|
||||
@keyframes ovFadeIn{from{opacity:0}to{opacity:1}}
|
||||
@keyframes ovFadeOut{from{opacity:1}to{opacity:0}}
|
||||
.ov-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:ovModalIn .22s ease;}
|
||||
.ov-modal-mask.closing .ov-modal{animation:ovModalOut .18s ease forwards;}
|
||||
@keyframes ovModalIn{from{transform:translateY(16px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
|
||||
@keyframes ovModalOut{from{transform:translateY(0) scale(1);opacity:1}to{transform:translateY(16px) scale(.98);opacity:0}}
|
||||
.ov-modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
||||
.ov-modal__title{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);}
|
||||
.ov-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;}
|
||||
.ov-modal__close:hover{background:#f5f5f5;color:var(--text-primary);}
|
||||
/* 原型演示提示条 */
|
||||
.ov-demo-tip{display:flex;align-items:center;gap:8px;background:#FFF7E6;border:1px solid #FFE58F;border-radius:var(--radius-sm);padding:8px 14px;margin:16px 24px 0;font-size:var(--font-sm);color:#8C6D1F;}
|
||||
.ov-demo-tip .tip-icon{flex-shrink:0;}
|
||||
/* Tab 栏 */
|
||||
.ov-tabs{display:flex;gap:4px;padding:12px 24px 0;border-bottom:1px solid var(--border);flex-shrink:0;overflow-x:auto;}
|
||||
.ov-tab{padding:8px 18px;font-size:var(--font-sm);color:var(--text-secondary);cursor:pointer;border:1px solid transparent;border-bottom:none;border-radius:var(--radius-sm) var(--radius-sm) 0 0;white-space:nowrap;transition:all .15s;user-select:none;}
|
||||
.ov-tab:hover{color:var(--primary);}
|
||||
.ov-tab.active{color:var(--primary);font-weight:600;background:var(--primary-light);border-color:var(--border);position:relative;top:1px;}
|
||||
.ov-modal__body{padding:20px 24px;overflow-y:auto;flex:1;}
|
||||
/* 占位 Tab */
|
||||
.ov-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 0;color:var(--text-placeholder);gap:12px;}
|
||||
.ov-placeholder__icon{font-size:48px;opacity:.6;}
|
||||
.ov-placeholder__text{font-size:var(--font-base);}
|
||||
.ov-placeholder__sub{font-size:var(--font-sm);}
|
||||
/* 智能货柜可视化(参考 prod-proc-cabinet 左侧柜子样式) */
|
||||
.ov-cabinet-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px;}
|
||||
.ov-cabinet-header select{height:34px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 10px;font-size:var(--font-sm);font-weight:600;outline:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 8px center;appearance:none;cursor:pointer;}
|
||||
.ov-cabinet-header select:focus{border-color:var(--primary);}
|
||||
.ov-cabinet-status{font-size:var(--font-sm);color:var(--text-secondary);}
|
||||
.ov-cabinet-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;}
|
||||
.ov-slot{border:2px solid var(--border);border-radius:var(--radius-md);padding:14px 10px;cursor:pointer;transition:all .2s;display:flex;flex-direction:column;align-items:center;gap:4px;min-height:96px;justify-content:center;position:relative;}
|
||||
.ov-slot:hover{border-color:var(--primary);box-shadow:0 2px 8px rgba(24,144,255,0.15);}
|
||||
.ov-slot.occupied{background:#FAFFF5;border-color:#B7EB8F;}
|
||||
.ov-slot.warning{background:#FFFBE6;border-color:#FFD666;}
|
||||
.ov-slot.alert{background:#FFF1F0;border-color:#FFA39E;}
|
||||
.ov-slot--empty{background:#FAFAFA;border-style:dashed;}
|
||||
.ov-slot--empty .ov-slot__name{color:var(--text-placeholder);font-weight:400;}
|
||||
.ov-slot__badge{position:absolute;top:5px;right:5px;width:8px;height:8px;border-radius:50%;}
|
||||
.ov-slot__badge.green{background:var(--success);}
|
||||
.ov-slot__badge.orange{background:var(--warning);}
|
||||
.ov-slot__badge.red{background:var(--danger);}
|
||||
.ov-slot__id{font-size:var(--font-xs);color:var(--text-placeholder);font-weight:500;}
|
||||
.ov-slot__name{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);text-align:center;line-height:1.3;}
|
||||
.ov-slot__remain{font-size:var(--font-sm);font-weight:700;color:var(--primary);}
|
||||
.ov-slot__expire{font-size:10px;color:var(--text-placeholder);}
|
||||
.ov-cabinet-legend{display:flex;gap:16px;padding:14px 4px 0;flex-wrap:wrap;}
|
||||
.ov-legend__item{display:flex;align-items:center;gap:4px;font-size:var(--font-xs);color:var(--text-secondary);}
|
||||
.ov-legend__dot{width:10px;height:10px;border-radius:2px;border:1.5px solid;}
|
||||
.ov-legend__dot.idle{background:#FAFAFA;border-color:var(--border);border-style:dashed;}
|
||||
.ov-legend__dot.storing{background:#FAFFF5;border-color:#B7EB8F;}
|
||||
.ov-legend__dot.warn{background:#FFFBE6;border-color:#FFD666;}
|
||||
.ov-legend__dot.err{background:#FFF1F0;border-color:#FFA39E;}
|
||||
/* 格口历史记录弹窗(叠加在设备概览之上,参考净菜进柜历史弹窗) */
|
||||
.slot-his-mask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1500;display:flex;align-items:center;justify-content:center;animation:ovFadeIn .2s ease;}
|
||||
.slot-his-mask.closing{animation:ovFadeOut .2s ease forwards;}
|
||||
.slot-his-modal{width:1000px;max-width:92vw;max-height:78vh;background:#fff;border-radius:var(--radius-md);box-shadow:0 12px 40px rgba(0,0,0,.22);display:flex;flex-direction:column;overflow:hidden;animation:ovModalIn .22s ease;}
|
||||
.slot-his-mask.closing .slot-his-modal{animation:ovModalOut .18s ease forwards;}
|
||||
.slot-his-modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
||||
.slot-his-modal__title{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);}
|
||||
.slot-his-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;}
|
||||
.slot-his-modal__close:hover{background:#f5f5f5;color:var(--text-primary);}
|
||||
.slot-his-modal__body{padding:20px 24px;overflow-y:auto;flex:1;}
|
||||
.slot-his-modal__footer{padding:12px 24px;border-top:1px solid var(--border);flex-shrink:0;background:#FAFBFC;}
|
||||
.slot-his-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;}
|
||||
.slot-his-summary__item{display:flex;align-items:center;gap:6px;font-size:var(--font-sm);color:var(--text-secondary);}
|
||||
.slot-his-summary__item strong{color:var(--text-primary);font-weight:600;}
|
||||
.slot-his-summary__item .label{color:var(--text-placeholder);font-size:var(--font-xs);}
|
||||
.slot-his-count{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:var(--font-xs);}
|
||||
.slot-his-count.in{background:#F6FFED;color:var(--success);}
|
||||
.slot-his-count.out{background:#FFF7E6;color:var(--warning);}
|
||||
.slot-his-table{width:100%;border-collapse:collapse;}
|
||||
.slot-his-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;}
|
||||
.slot-his-table tbody td{padding:10px 12px;font-size:var(--font-sm);color:var(--text-primary);border-bottom:1px solid var(--border);white-space:nowrap;}
|
||||
.slot-his-table tbody tr:last-child td{border-bottom:none;}
|
||||
.slot-his-delta{display:inline-flex;align-items:center;gap:2px;font-weight:600;}
|
||||
.slot-his-delta.pos{color:var(--success);}
|
||||
.slot-his-delta.neg{color:var(--danger);}
|
||||
.slot-his-pagination{display:flex;align-items:center;justify-content:flex-end;gap:6px;}
|
||||
.slot-his-pagination__total{font-size:var(--font-sm);color:var(--text-secondary);margin-right:auto;}
|
||||
/* ===== 餐盘柜格口扩展状态 ===== */
|
||||
.ov-slot.using{background:#F0F8FF;border-color:#91D5FF;}
|
||||
.ov-slot.disabled{background:#F5F5F5;border-color:#D9D9D9;cursor:not-allowed;opacity:.75;}
|
||||
.ov-slot__badge.blue{background:var(--primary);}
|
||||
.ov-slot__badge.gray{background:#999;}
|
||||
.ov-slot__level{font-size:10px;padding:1px 6px;border-radius:8px;background:rgba(251,191,36,.15);color:#D48806;border:1px solid rgba(251,191,36,.5);}
|
||||
.ov-slot__level.vip{background:rgba(96,165,250,.15);color:#1D6FD1;border-color:rgba(96,165,250,.5);}
|
||||
.ov-legend__dot.using{background:#F0F8FF;border-color:#91D5FF;}
|
||||
.ov-legend__dot.disabled{background:#F5F5F5;border-color:#D9D9D9;}
|
||||
/* 餐盘柜格口详情弹窗 */
|
||||
.plate-detail__grid{display:grid;grid-template-columns:1fr 1fr;gap:12px 24px;}
|
||||
.plate-detail__section{margin-bottom:20px;}
|
||||
.plate-detail__section-title{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:12px;display:flex;align-items:center;gap:6px;}
|
||||
.plate-detail__section-title::before{content:'';display:inline-block;width:3px;height:14px;background:var(--primary);border-radius:2px;}
|
||||
.plate-detail__field{display:flex;flex-direction:column;gap:2px;}
|
||||
.plate-detail__label{font-size:var(--font-xs);color:var(--text-placeholder);}
|
||||
.plate-detail__value{font-size:var(--font-sm);color:var(--text-primary);font-weight:500;min-height:22px;}
|
||||
/* 会员选择层(格口分配) */
|
||||
.member-picker-mask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1600;display:flex;align-items:center;justify-content:center;animation:ovFadeIn .2s ease;}
|
||||
.member-picker-mask.closing{animation:ovFadeOut .2s ease forwards;}
|
||||
.member-picker{width:560px;max-width:92vw;max-height:70vh;background:#fff;border-radius:var(--radius-md);box-shadow:0 12px 40px rgba(0,0,0,.22);display:flex;flex-direction:column;overflow:hidden;animation:ovModalIn .22s ease;}
|
||||
.member-picker__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
||||
.member-picker__title{font-size:var(--font-lg);font-weight:600;}
|
||||
.member-picker__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;}
|
||||
.member-picker__close:hover{background:#f5f5f5;color:var(--text-primary);}
|
||||
.member-picker__search{padding:14px 24px 0;flex-shrink:0;}
|
||||
.member-picker__search input{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);outline:none;}
|
||||
.member-picker__search input:focus{border-color:var(--primary);}
|
||||
.member-picker__body{padding:12px 24px 20px;overflow-y:auto;flex:1;}
|
||||
.member-picker__item{display:flex;align-items:center;gap:12px;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:8px;cursor:pointer;transition:all .15s;}
|
||||
.member-picker__item:hover{border-color:var(--primary);background:#F8FBFF;}
|
||||
.member-picker__item.selected{border-color:var(--primary);background:var(--primary-light);}
|
||||
.member-picker__avatar{width:36px;height:36px;border-radius:50%;background:var(--primary-light);color:var(--primary);display:flex;align-items:center;justify-content:center;font-size:var(--font-sm);font-weight:600;flex-shrink:0;}
|
||||
.member-picker__info{flex:1;min-width:0;}
|
||||
.member-picker__name{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);}
|
||||
.member-picker__meta{font-size:var(--font-xs);color:var(--text-placeholder);}
|
||||
.member-picker__empty{text-align:center;padding:30px 0;color:var(--text-placeholder);font-size:var(--font-sm);}
|
||||
.member-picker__footer{display:flex;justify-content:flex-end;gap:12px;padding:14px 24px;border-top:1px solid var(--border);flex-shrink:0;}
|
||||
/* 确认弹窗(解绑二次确认) */
|
||||
.ov-confirm-mask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1600;display:flex;align-items:center;justify-content:center;animation:ovFadeIn .2s ease;}
|
||||
.ov-confirm-mask.closing{animation:ovFadeOut .2s ease forwards;}
|
||||
.ov-confirm{width:420px;background:#fff;border-radius:var(--radius-lg);padding:24px;animation:ovModalIn .22s ease;}
|
||||
.ov-confirm__msg{text-align:center;font-size:var(--font-base);font-weight:500;margin:12px 0 4px;}
|
||||
.ov-confirm__sub{text-align:center;font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:20px;}
|
||||
.ov-confirm__icon{text-align:center;font-size:40px;}
|
||||
/* ===== 净菜/毛菜货柜格口离线态 ===== */
|
||||
.ov-slot.offline{background:#F5F5F5;border-color:#D9D9D9;}
|
||||
.ov-slot.offline .ov-slot__name{color:var(--text-placeholder);}
|
||||
.ov-legend__dot.offline{background:#F5F5F5;border-color:#D9D9D9;}
|
||||
/* ===== 水电气表卡片(参考 prod-qc-daily energy-card,重布局) ===== */
|
||||
.ov-energy-card{position:relative;border:1px solid var(--border);border-radius:var(--radius-md);padding:20px 24px;margin-bottom:20px;background:linear-gradient(135deg,#FBFDFF,#F6FAFF);}
|
||||
.ov-energy-card__entry{position:absolute;top:16px;right:16px;font-size:var(--font-xs);color:var(--primary);cursor:pointer;padding:4px 10px;border:1px solid var(--primary);border-radius:12px;transition:all .15s;background:#fff;}
|
||||
.ov-energy-card__entry:hover{background:var(--primary);color:#fff;}
|
||||
.ov-energy-card__header{display:flex;align-items:center;gap:10px;margin-bottom:14px;}
|
||||
.ov-energy-card__icon{font-size:26px;}
|
||||
.ov-energy-card__title{font-size:var(--font-lg);font-weight:700;}
|
||||
.ov-energy-card__meter{font-size:var(--font-xs);color:var(--text-placeholder);font-family:monospace;}
|
||||
.ov-energy-card__value{font-size:34px;font-weight:700;line-height:1.2;margin-bottom:14px;}
|
||||
.ov-energy-card__value span{font-size:var(--font-sm);font-weight:400;color:var(--text-secondary);}
|
||||
.ov-energy-card__rows{display:grid;grid-template-columns:repeat(4,1fr);gap:10px 24px;}
|
||||
.ov-energy-card__row{display:flex;flex-direction:column;gap:2px;}
|
||||
.ov-energy-card__row-label{font-size:var(--font-xs);color:var(--text-placeholder);}
|
||||
.ov-energy-card__row-value{font-size:var(--font-sm);font-weight:500;color:var(--text-primary);}
|
||||
.ov-energy-card__hint{margin-top:14px;padding-top:12px;border-top:1px dashed var(--border);font-size:var(--font-xs);color:var(--text-secondary);}
|
||||
.ov-energy-edit{margin-top:14px;padding-top:14px;border-top:1px dashed var(--border);display:grid;grid-template-columns:repeat(3,1fr);gap:12px 24px;align-items:end;}
|
||||
.ov-energy-edit .plate-detail__field input{height:34px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 10px;font-size:var(--font-sm);outline:none;}
|
||||
.ov-energy-edit .plate-detail__field input:focus{border-color:var(--primary);}
|
||||
.ov-energy-edit__actions{display:flex;gap:8px;justify-content:flex-end;grid-column:1/-1;}
|
||||
/* 日使用量统计表格(Tab 内嵌) */
|
||||
.ov-daily-table{width:100%;border-collapse:collapse;}
|
||||
.ov-daily-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;}
|
||||
.ov-daily-table tbody td{padding:10px 12px;font-size:var(--font-sm);color:var(--text-primary);border-bottom:1px solid var(--border);white-space:nowrap;}
|
||||
.ov-daily-table tbody tr:last-child td{border-bottom:none;}
|
||||
.ov-daily-table tbody tr:hover{background:#F8FBFF;}
|
||||
.ov-daily-pagination{display:flex;align-items:center;justify-content:flex-end;gap:6px;padding:14px 0 0;}
|
||||
.ov-daily-pagination__total{font-size:var(--font-sm);color:var(--text-secondary);margin-right:auto;}
|
||||
/* Tab 内嵌区块标题 */
|
||||
.ov-section-title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);margin:0 0 12px;display:flex;align-items:center;gap:6px;}
|
||||
.ov-section-title::before{content:'';display:inline-block;width:3px;height:15px;background:var(--primary);border-radius:2px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -282,12 +443,12 @@ tbody tr:hover{background:#F8FBFF;}
|
||||
<table>
|
||||
<thead><tr><th>设备编号</th><th>设备名称</th><th>设备类型</th><th>所属餐线</th><th>厂家品牌</th><th>责任人</th><th>网络类型</th><th>IP地址/设备ID</th><th>固件版本</th><th>安装日期</th><th>在线状态</th><th>设备健康评分</th><th>今日使用次数</th><th>最近心跳时间</th><th>运行天数</th><th>累计时长(分钟)</th><th>最近维护时间</th><th>故障记录数</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>DEV-001</td><td>1号点餐屏</td><td>点餐屏</td><td>—</td><td>海康威视</td><td>张运维</td><td>WiFi</td><td>192.168.1.101</td><td>v3.2.1</td><td>2024-08-15</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">98分</span></td><td>486</td><td>2026-04-22 11:30:22</td><td>615</td><td>362,400</td><td>2026-03-20</td><td>2</td><td class="td-actions"><a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-002</td><td>智能取餐柜-01</td><td>智能取餐柜</td><td>1号餐线</td><td>海尔智家</td><td>李运维</td><td>有线</td><td>192.168.1.102</td><td>v2.8.0</td><td>2024-08-15</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">100分</span></td><td>312</td><td>2026-04-22 11:30:18</td><td>615</td><td>498,720</td><td>2026-04-01</td><td>0</td><td class="td-actions"><a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-003</td><td>2号点餐屏</td><td>点餐屏</td><td>—</td><td>海康威视</td><td>张运维</td><td>WiFi</td><td>192.168.1.103</td><td>v3.2.1</td><td>2024-10-01</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">92分</span></td><td>398</td><td>2026-04-22 11:30:25</td><td>568</td><td>298,800</td><td>2026-04-10</td><td>1</td><td class="td-actions"><a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-004</td><td>智能取餐柜-02</td><td>智能取餐柜</td><td>2号餐线</td><td>海尔智家</td><td>李运维</td><td>有线</td><td>192.168.1.104</td><td>v2.7.3</td><td>2024-10-01</td><td><span class="tag tag-red">离线</span></td><td><span class="tag tag-red">0分</span></td><td>0</td><td>2026-04-22 09:18:44</td><td>568</td><td>423,600</td><td>2026-04-15</td><td>3</td><td class="td-actions"><a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-005</td><td>餐线秤台-00025</td><td>餐线秤台</td><td>1号餐线</td><td>大华技术</td><td>王运维</td><td>有线</td><td>DEV-00025</td><td>v1.5.2</td><td>2022-06-10</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">99分</span></td><td>1,280</td><td>2026-04-22 11:30:20</td><td>1,412</td><td>1,016,640</td><td>2026-03-01</td><td>0</td><td class="td-actions"><a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-006</td><td>体脂秤-01</td><td>体脂秤</td><td>—</td><td>华为智选</td><td>张运维</td><td>WiFi</td><td>192.168.1.106</td><td>v2.1.0</td><td>2023-12-01</td><td><span class="tag tag-orange">故障</span></td><td><span class="tag tag-red">45分</span></td><td>0</td><td>2026-04-22 10:55:08</td><td>873</td><td>78,540</td><td>2026-02-28</td><td>1</td><td class="td-actions"><a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-001</td><td>1号点餐屏</td><td>点餐屏</td><td>—</td><td>海康威视</td><td>张运维</td><td>WiFi</td><td>192.168.1.101</td><td>v3.2.1</td><td>2024-08-15</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">98分</span></td><td>486</td><td>2026-04-22 11:30:22</td><td>615</td><td>362,400</td><td>2026-03-20</td><td>2</td><td class="td-actions"><a class="link">设备概览</a> <a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-002</td><td>智能取餐柜-01</td><td>智能取餐柜</td><td>1号餐线</td><td>海尔智家</td><td>李运维</td><td>有线</td><td>192.168.1.102</td><td>v2.8.0</td><td>2024-08-15</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">100分</span></td><td>312</td><td>2026-04-22 11:30:18</td><td>615</td><td>498,720</td><td>2026-04-01</td><td>0</td><td class="td-actions"><a class="link">设备概览</a> <a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-003</td><td>2号点餐屏</td><td>点餐屏</td><td>—</td><td>海康威视</td><td>张运维</td><td>WiFi</td><td>192.168.1.103</td><td>v3.2.1</td><td>2024-10-01</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">92分</span></td><td>398</td><td>2026-04-22 11:30:25</td><td>568</td><td>298,800</td><td>2026-04-10</td><td>1</td><td class="td-actions"><a class="link">设备概览</a> <a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-004</td><td>智能取餐柜-02</td><td>智能取餐柜</td><td>2号餐线</td><td>海尔智家</td><td>李运维</td><td>有线</td><td>192.168.1.104</td><td>v2.7.3</td><td>2024-10-01</td><td><span class="tag tag-red">离线</span></td><td><span class="tag tag-red">0分</span></td><td>0</td><td>2026-04-22 09:18:44</td><td>568</td><td>423,600</td><td>2026-04-15</td><td>3</td><td class="td-actions"><a class="link">设备概览</a> <a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-005</td><td>餐线秤台-00025</td><td>餐线秤台</td><td>1号餐线</td><td>大华技术</td><td>王运维</td><td>有线</td><td>DEV-00025</td><td>v1.5.2</td><td>2022-06-10</td><td><span class="tag tag-green">在线</span></td><td><span class="tag tag-green">99分</span></td><td>1,280</td><td>2026-04-22 11:30:20</td><td>1,412</td><td>1,016,640</td><td>2026-03-01</td><td>0</td><td class="td-actions"><a class="link">设备概览</a> <a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
<tr><td>DEV-006</td><td>体脂秤-01</td><td>体脂秤</td><td>—</td><td>华为智选</td><td>张运维</td><td>WiFi</td><td>192.168.1.106</td><td>v2.1.0</td><td>2023-12-01</td><td><span class="tag tag-orange">故障</span></td><td><span class="tag tag-red">45分</span></td><td>0</td><td>2026-04-22 10:55:08</td><td>873</td><td>78,540</td><td>2026-02-28</td><td>1</td><td class="td-actions"><a class="link">设备概览</a> <a class="link">故障记录</a> <a class="link">远程重启</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -482,8 +643,726 @@ document.querySelectorAll('tbody tr').forEach(row=>{
|
||||
if(link.textContent.includes('远程重启')){
|
||||
link.addEventListener('click',()=>showRebootConfirm(row));
|
||||
}
|
||||
if(link.textContent.includes('设备概览')){
|
||||
link.addEventListener('click',()=>showDeviceOverview(row));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ===== 设备概览大弹窗(原型演示:Tab 集中演示各类设备,实际交付按设备类型显示对应概览) ===== */
|
||||
/* Tab 定义:仅「智能货柜」已实现内容,其余为占位 */
|
||||
var OV_TABS=[
|
||||
{key:'plate',name:'餐盘柜',icon:'🍽️'},
|
||||
{key:'smart',name:'智能货柜',icon:'🗄️'},
|
||||
{key:'raw',name:'毛菜货柜',icon:'🥬'},
|
||||
{key:'clean',name:'净菜货柜',icon:'🧺'},
|
||||
{key:'meter-e',name:'智能电表',icon:'⚡'},
|
||||
{key:'meter-w',name:'智能水表',icon:'💧'},
|
||||
{key:'meter-g',name:'智能气表',icon:'🔥'}
|
||||
];
|
||||
var OV_CURRENT_TAB='smart';
|
||||
|
||||
function showDeviceOverview(row){
|
||||
const cells=row.querySelectorAll('td');
|
||||
const devCode=cells[0].textContent.trim();
|
||||
const devName=cells[1].textContent.trim();
|
||||
const devType=cells[2].textContent.trim();
|
||||
|
||||
const tabsHtml=OV_TABS.map(t=>`<div class="ov-tab${t.key===OV_CURRENT_TAB?' active':''}" data-key="${t.key}" onclick="switchOvTab('${t.key}')">${t.icon} ${t.name}</div>`).join('');
|
||||
|
||||
const overlay=document.createElement('div');
|
||||
overlay.className='ov-modal-mask';
|
||||
overlay.id='ovModalMask';
|
||||
overlay.innerHTML=`
|
||||
<div class="ov-modal">
|
||||
<div class="ov-modal__header">
|
||||
<div class="ov-modal__title">设备概览 — ${devName}(${devCode} · ${devType})</div>
|
||||
<button class="ov-modal__close" onclick="closeDeviceOverview()">×</button>
|
||||
</div>
|
||||
<div class="ov-demo-tip">
|
||||
<span class="tip-icon">⚠️</span>
|
||||
原型演示说明:以下 Tab 仅用于集中演示各设备类型的概览效果,实际交付时根据设备类型自动显示对应的概览内容。
|
||||
</div>
|
||||
<div class="ov-tabs">${tabsHtml}</div>
|
||||
<div class="ov-modal__body" id="ovTabBody">${renderOvTabBody(OV_CURRENT_TAB)}</div>
|
||||
</div>`;
|
||||
overlay.addEventListener('click',e=>{if(e.target===overlay)closeDeviceOverview();});
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
function closeDeviceOverview(){
|
||||
const m=document.getElementById('ovModalMask');
|
||||
if(!m)return;
|
||||
closeSlotHistory();
|
||||
m.classList.add('closing');
|
||||
setTimeout(()=>m.remove(),200);
|
||||
}
|
||||
function switchOvTab(key){
|
||||
OV_CURRENT_TAB=key;
|
||||
document.querySelectorAll('#ovModalMask .ov-tab').forEach(t=>t.classList.toggle('active',t.dataset.key===key));
|
||||
document.getElementById('ovTabBody').innerHTML=renderOvTabBody(key);
|
||||
/* 水电气表 Tab 内容含分页列表,需在 DOM 插入后再渲染 */
|
||||
if(key==='meter-e')renderEnergyDaily('elec');
|
||||
if(key==='meter-w')renderEnergyDaily('water');
|
||||
if(key==='meter-g')renderEnergyDaily('gas');
|
||||
}
|
||||
|
||||
/* 按 Tab 渲染内容:占位 / 智能货柜可视化 */
|
||||
function renderOvTabBody(key){
|
||||
if(key==='smart')return renderSmartCabinet();
|
||||
if(key==='plate')return renderPlateCabinet();
|
||||
if(key==='clean')return renderStockCabinet('clean');
|
||||
if(key==='raw')return renderStockCabinet('raw');
|
||||
if(key==='meter-e')return renderEnergyTab('elec');
|
||||
if(key==='meter-w')return renderEnergyTab('water');
|
||||
if(key==='meter-g')return renderEnergyTab('gas');
|
||||
const tab=OV_TABS.find(t=>t.key===key);
|
||||
return `<div class="ov-placeholder">
|
||||
<div class="ov-placeholder__icon">${tab?tab.icon:'📺'}</div>
|
||||
<div class="ov-placeholder__text">${tab?tab.name:''}概览待设计</div>
|
||||
<div class="ov-placeholder__sub">原型演示阶段暂未展开,实际交付时根据设备类型展示对应概览效果</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ===== 智能货柜:柜子轮廓布局 + 储物状态(参考 prod-proc-cabinet.html 左侧柜子样式) ===== */
|
||||
/* 格口 Mock:状态 occupied 储物中 / warning 临期预警 / alert 过期预警 / empty 空闲 */
|
||||
var OV_SLOTS=[
|
||||
{id:'B1-01',name:'宫保鸡丁餐品包',remain:'2.4kg',expire:'08-19 到期',status:'occupied',badge:'green'},
|
||||
{id:'B1-02',name:'红烧肉餐品包',remain:'3.1kg',expire:'08-19 到期',status:'occupied',badge:'green'},
|
||||
{id:'B1-03',name:'清蒸鱼餐品包',remain:'1.8kg',expire:'08-18 即将到期',status:'warning',badge:'orange',expireColor:'var(--warning)'},
|
||||
{id:'B1-04',name:'净西兰花',remain:'4.5kg',expire:'08-20 到期',status:'occupied',badge:'green'},
|
||||
{id:'B1-05',name:'净胡萝卜',remain:'5.2kg',expire:'08-20 到期',status:'occupied',badge:'green'},
|
||||
{id:'B1-06',name:'净白菜',remain:'6.0kg',expire:'08-10 已过期',status:'alert',badge:'red',expireColor:'var(--danger)'},
|
||||
{id:'B1-07',name:'净香菇',remain:'2.2kg',expire:'08-19 到期',status:'occupied',badge:'green'},
|
||||
{id:'B1-08',status:'empty'},
|
||||
{id:'B1-09',status:'empty'},
|
||||
{id:'B1-10',status:'empty'}
|
||||
];
|
||||
|
||||
function renderSmartCabinet(){
|
||||
const slotsHtml=OV_SLOTS.map((s,i)=>{
|
||||
const cls=s.status==='empty'?'ov-slot ov-slot--empty':'ov-slot '+s.status;
|
||||
if(s.status==='empty'){
|
||||
return `<div class="${cls}" onclick="showSlotHistory(${i})"><div class="ov-slot__id">${s.id}</div><div class="ov-slot__name">空闲</div></div>`;
|
||||
}
|
||||
return `<div class="${cls}" onclick="showSlotHistory(${i})">
|
||||
<div class="ov-slot__badge ${s.badge}"></div>
|
||||
<div class="ov-slot__id">${s.id}</div>
|
||||
<div class="ov-slot__name">${s.name}</div>
|
||||
<div class="ov-slot__remain">${s.remain}</div>
|
||||
<div class="ov-slot__expire"${s.expireColor?` style="color:${s.expireColor}"`:''}>${s.expire}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="ov-cabinet-header">
|
||||
<div style="font-size:var(--font-base);font-weight:600;color:var(--text-primary)">1号智能货柜</div>
|
||||
<div class="ov-cabinet-status">4.1℃ · <span style="color:var(--success)">在线</span> · 已占用 7/10 格口</div>
|
||||
</div>
|
||||
<div class="ov-cabinet-grid">${slotsHtml}</div>
|
||||
<div class="ov-cabinet-legend">
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot idle"></div>空闲</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot storing"></div>储物中</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot warn"></div>临期预警</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot err"></div>过期预警</div>
|
||||
<div class="ov-legend__item" style="margin-left:auto;color:var(--text-placeholder)">点击格口可查看历史记录</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ===== 餐盘柜:格口 + 会员绑定状态 ===== */
|
||||
/* 格口 Mock:状态 bound 已绑定 / using 使用中(盘子被取出) / free 未绑定 / disabled 异常停用 */
|
||||
var PLATE_SLOTS=[
|
||||
{id:'P1-01',status:'bound',member:{name:'张*明',level:'黄金会员',levelCls:'',phone:'138****5621',code:'U20250311',dept:'生产部·一车间'},bindTime:'2026-06-01 09:30',lastUse:'2026-08-18 12:05 · 已归还',useCount:128},
|
||||
{id:'P1-02',status:'using',member:{name:'李*华',level:'铂金会员',levelCls:'vip',phone:'139****3387',code:'U20240827',dept:'技术部·平台组'},bindTime:'2026-05-12 10:15',lastUse:'2026-08-18 12:20 · 使用中(已取出)',useCount:203},
|
||||
{id:'P1-03',status:'bound',member:{name:'王*强',level:'普通会员',levelCls:'',phone:'137****8845',code:'U20260702',dept:'后勤部·餐饮科'},bindTime:'2026-07-20 14:00',lastUse:'2026-08-18 11:50 · 已归还',useCount:45},
|
||||
{id:'P1-04',status:'bound',member:{name:'赵*敏',level:'黄金会员',levelCls:'',phone:'136****2210',code:'U20230915',dept:'财务部·核算组'},bindTime:'2026-04-08 09:00',lastUse:'2026-08-18 12:10 · 已归还',useCount:312},
|
||||
{id:'P1-05',status:'using',member:{name:'陈*芳',level:'VIP会员',levelCls:'vip',phone:'135****7732',code:'U20250422',dept:'人事部·招聘组'},bindTime:'2026-06-25 11:20',lastUse:'2026-08-18 12:30 · 使用中(已取出)',useCount:97},
|
||||
{id:'P1-06',status:'bound',member:{name:'刘*洋',level:'普通会员',levelCls:'',phone:'188****4406',code:'U20260801',dept:'生产部·二车间'},bindTime:'2026-08-01 08:40',lastUse:'2026-08-18 11:45 · 已归还',useCount:23},
|
||||
{id:'P1-07',status:'disabled',member:null,bindTime:'—',lastUse:'2026-08-15 09:12 · 格口门锁故障',useCount:0},
|
||||
{id:'P1-08',status:'free',member:null,bindTime:'—',lastUse:'—',useCount:0},
|
||||
{id:'P1-09',status:'free',member:null,bindTime:'—',lastUse:'—',useCount:0},
|
||||
{id:'P1-10',status:'free',member:null,bindTime:'—',lastUse:'—',useCount:0}
|
||||
];
|
||||
|
||||
/* 可分配会员 Mock(原型演示) */
|
||||
var PLATE_MEMBERS=[
|
||||
{name:'孙*丽',level:'黄金会员',levelCls:'',phone:'133****9012',code:'U20260418',dept:'生产部·三车间'},
|
||||
{name:'周*军',level:'普通会员',levelCls:'',phone:'132****5566',code:'U20251130',dept:'技术部·运维组'},
|
||||
{name:'吴*霞',level:'铂金会员',levelCls:'vip',phone:'131****7789',code:'U20240612',dept:'质检部·化验组'},
|
||||
{name:'郑*涛',level:'普通会员',levelCls:'',phone:'130****3344',code:'U20260220',dept:'后勤部·保卫科'},
|
||||
{name:'何*静',level:'黄金会员',levelCls:'',phone:'159****8823',code:'U20250805',dept:'财务部·出纳组'}
|
||||
];
|
||||
|
||||
var PLATE_STATUS_META={
|
||||
bound:{text:'已绑定',cls:'occupied',badge:'green'},
|
||||
using:{text:'使用中',cls:'using',badge:'blue'},
|
||||
free:{text:'未绑定',cls:'ov-slot--empty',badge:null},
|
||||
disabled:{text:'异常停用',cls:'disabled',badge:'gray'}
|
||||
};
|
||||
|
||||
function renderPlateCabinet(){
|
||||
const boundCount=PLATE_SLOTS.filter(s=>s.status==='bound'||s.status==='using').length;
|
||||
const totalCount=PLATE_SLOTS.length;
|
||||
const slotsHtml=PLATE_SLOTS.map((s,i)=>{
|
||||
const meta=PLATE_STATUS_META[s.status];
|
||||
if(s.status==='free'){
|
||||
return `<div class="ov-slot ov-slot--empty" onclick="showPlateSlot(${i})"><div class="ov-slot__id">${s.id}</div><div class="ov-slot__name">未绑定</div></div>`;
|
||||
}
|
||||
const memberHtml=s.member
|
||||
?`<div class="ov-slot__name">${s.member.name}</div><div class="ov-slot__level ${s.member.levelCls}">${s.member.level}</div>`
|
||||
:`<div class="ov-slot__name" style="color:var(--text-placeholder)">停用</div>`;
|
||||
return `<div class="ov-slot ${meta.cls}" onclick="showPlateSlot(${i})">
|
||||
${meta.badge?`<div class="ov-slot__badge ${meta.badge}"></div>`:''}
|
||||
<div class="ov-slot__id">${s.id}</div>
|
||||
${memberHtml}
|
||||
<div class="ov-slot__expire">${meta.text}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="ov-cabinet-header">
|
||||
<div style="font-size:var(--font-base);font-weight:600;color:var(--text-primary)">1号餐盘柜 · 每格口配专属消毒餐盘供固定会员使用</div>
|
||||
<div class="ov-cabinet-status">36.8℃(消毒保温)· <span style="color:var(--success)">在线</span> · 已绑定 ${boundCount}/${totalCount} 格口</div>
|
||||
</div>
|
||||
<div class="ov-cabinet-grid">${slotsHtml}</div>
|
||||
<div class="ov-cabinet-legend">
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot storing"></div>已绑定</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot using"></div>使用中(盘子取出)</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot idle"></div>未绑定</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot disabled"></div>异常停用</div>
|
||||
<div class="ov-legend__item" style="margin-left:auto;color:var(--text-placeholder)">点击格口可查看绑定详情并分配/解绑</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ===== 餐盘柜格口详情弹窗(叠加在设备概览之上) ===== */
|
||||
function showPlateSlot(idx){
|
||||
const s=PLATE_SLOTS[idx];
|
||||
const meta=PLATE_STATUS_META[s.status];
|
||||
const m=s.member;
|
||||
const statusTag=s.status==='bound'?'<span class="tag tag-green">已绑定</span>':s.status==='using'?'<span class="tag tag-blue">使用中</span>':s.status==='free'?'<span class="tag tag-gray">未绑定</span>':'<span class="tag tag-red">异常停用</span>';
|
||||
let html=`<div class="plate-detail__section"><div class="plate-detail__section-title">格口信息</div><div class="plate-detail__grid">
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">使用状态</span><span class="plate-detail__value">${statusTag}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">所属柜</span><span class="plate-detail__value">1号餐盘柜</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">格口</span><span class="plate-detail__value">${s.id}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">绑定时间</span><span class="plate-detail__value">${s.bindTime}</span></div>
|
||||
</div></div>`;
|
||||
if(m){
|
||||
html+=`<div class="plate-detail__section"><div class="plate-detail__section-title">绑定会员信息</div><div class="plate-detail__grid">
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">会员姓名</span><span class="plate-detail__value">${m.name}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">会员等级</span><span class="plate-detail__value"><span class="ov-slot__level ${m.levelCls}">${m.level}</span></span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">手机号</span><span class="plate-detail__value">${m.phone}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">用户编号</span><span class="plate-detail__value" style="font-family:monospace">${m.code}</span></div>
|
||||
</div></div>`;
|
||||
html+=`<div class="plate-detail__section"><div class="plate-detail__section-title">使用情况</div><div class="plate-detail__grid">
|
||||
<div class="plate-detail__field" style="grid-column:1/-1"><span class="plate-detail__label">最后使用状态</span><span class="plate-detail__value">${s.lastUse}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">累计使用次数</span><span class="plate-detail__value">${s.useCount} 次</span></div>
|
||||
</div></div>`;
|
||||
}else{
|
||||
html+=`<div class="plate-detail__section"><div class="plate-detail__section-title">使用情况</div><div class="plate-detail__grid">
|
||||
<div class="plate-detail__field" style="grid-column:1/-1"><span class="plate-detail__label">最后使用状态</span><span class="plate-detail__value">${s.lastUse}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">累计使用次数</span><span class="plate-detail__value">${s.useCount} 次</span></div>
|
||||
</div></div>`;
|
||||
}
|
||||
/* 操作按钮:未绑定可分配;已绑定/使用中可解绑;异常停用不可操作 */
|
||||
let footer='';
|
||||
if(s.status==='free'){
|
||||
footer=`<button class="btn btn-primary" onclick="openPlateAssign(${idx})">格口分配</button>`;
|
||||
}else if(m){
|
||||
footer=`<button class="btn btn-danger" onclick="openPlateUnbind(${idx})">用户解绑</button>`;
|
||||
}else{
|
||||
footer=`<span style="font-size:var(--font-sm);color:var(--text-placeholder);align-self:center;">格口异常停用中,维修恢复后方可分配</span>`;
|
||||
}
|
||||
const mask=document.createElement('div');
|
||||
mask.className='slot-his-mask';
|
||||
mask.id='plateSlotMask';
|
||||
mask.innerHTML=`<div class="slot-his-modal" style="width:640px;height:auto;max-height:78vh">
|
||||
<div class="slot-his-modal__header"><div class="slot-his-modal__title">餐盘格口详情 — ${s.id}</div><button class="slot-his-modal__close" onclick="closePlateSlot()">×</button></div>
|
||||
<div class="slot-his-modal__body">${html}</div>
|
||||
<div class="slot-his-modal__footer" style="display:flex;justify-content:flex-end;gap:12px;">${footer}</div>
|
||||
</div>`;
|
||||
mask.addEventListener('click',e=>{if(e.target===mask)closePlateSlot();});
|
||||
document.body.appendChild(mask);
|
||||
}
|
||||
function closePlateSlot(){
|
||||
const m=document.getElementById('plateSlotMask');
|
||||
if(!m)return;
|
||||
m.classList.add('closing');
|
||||
setTimeout(()=>m.remove(),200);
|
||||
}
|
||||
function refreshPlateTab(){
|
||||
if(OV_CURRENT_TAB==='plate'&&document.getElementById('ovTabBody')){
|
||||
document.getElementById('ovTabBody').innerHTML=renderPlateCabinet();
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 格口分配:会员选择层 ===== */
|
||||
var PLATE_ASSIGN_IDX=-1;
|
||||
var PLATE_ASSIGN_SELECTED=null;
|
||||
function openPlateAssign(idx){
|
||||
PLATE_ASSIGN_IDX=idx;
|
||||
PLATE_ASSIGN_SELECTED=null;
|
||||
const mask=document.createElement('div');
|
||||
mask.className='member-picker-mask';
|
||||
mask.id='plateAssignMask';
|
||||
mask.innerHTML=`<div class="member-picker">
|
||||
<div class="member-picker__header"><div class="member-picker__title">格口分配 — 选择会员(${PLATE_SLOTS[idx].id})</div><button class="member-picker__close" onclick="closePlateAssign()">×</button></div>
|
||||
<div class="member-picker__search"><input type="text" placeholder="搜索会员姓名 / 手机号" oninput="filterPlateMembers(this.value)"></div>
|
||||
<div class="member-picker__body" id="plateMemberList"></div>
|
||||
<div class="member-picker__footer">
|
||||
<button class="btn btn-default" onclick="closePlateAssign()">取消</button>
|
||||
<button class="btn btn-primary" onclick="confirmPlateAssign()">确认分配</button>
|
||||
</div>
|
||||
</div>`;
|
||||
mask.addEventListener('click',e=>{if(e.target===mask)closePlateAssign();});
|
||||
document.body.appendChild(mask);
|
||||
renderPlateMemberList(PLATE_MEMBERS);
|
||||
}
|
||||
function closePlateAssign(){
|
||||
const m=document.getElementById('plateAssignMask');
|
||||
if(!m)return;
|
||||
m.classList.add('closing');
|
||||
setTimeout(()=>m.remove(),200);
|
||||
}
|
||||
function renderPlateMemberList(list){
|
||||
const wrap=document.getElementById('plateMemberList');
|
||||
if(!wrap)return;
|
||||
if(!list.length){
|
||||
wrap.innerHTML='<div class="member-picker__empty">未找到匹配的会员</div>';
|
||||
return;
|
||||
}
|
||||
wrap.innerHTML=list.map(m=>{
|
||||
const globalIdx=PLATE_MEMBERS.indexOf(m);
|
||||
const selected=PLATE_ASSIGN_SELECTED===m;
|
||||
return `<div class="member-picker__item${selected?' selected':''}" onclick="selectPlateMember(${globalIdx})">
|
||||
<div class="member-picker__avatar">${m.name.charAt(0)}</div>
|
||||
<div class="member-picker__info">
|
||||
<div class="member-picker__name">${m.name} <span class="ov-slot__level ${m.levelCls}" style="margin-left:4px">${m.level}</span></div>
|
||||
<div class="member-picker__meta">${m.phone} · ${m.dept}</div>
|
||||
</div>
|
||||
${selected?'<span style="color:var(--primary);font-size:var(--font-lg)">✓</span>':''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
function filterPlateMembers(kw){
|
||||
kw=(kw||'').trim();
|
||||
if(!kw){renderPlateMemberList(PLATE_MEMBERS);return;}
|
||||
renderPlateMemberList(PLATE_MEMBERS.filter(m=>m.name.includes(kw)||m.phone.includes(kw)));
|
||||
}
|
||||
function selectPlateMember(idx){
|
||||
PLATE_ASSIGN_SELECTED=PLATE_MEMBERS[idx];
|
||||
renderPlateMemberList(PLATE_MEMBERS);
|
||||
}
|
||||
function confirmPlateAssign(){
|
||||
if(!PLATE_ASSIGN_SELECTED){
|
||||
showOvToast('请先选择要分配的会员');
|
||||
return;
|
||||
}
|
||||
const s=PLATE_SLOTS[PLATE_ASSIGN_IDX];
|
||||
s.status='bound';
|
||||
s.member=PLATE_ASSIGN_SELECTED;
|
||||
s.bindTime='2026-08-18 16:50';
|
||||
s.lastUse='— · 分配后待首次使用';
|
||||
s.useCount=0;
|
||||
closePlateAssign();
|
||||
closePlateSlot();
|
||||
refreshPlateTab();
|
||||
showOvToast('已将格口 '+s.id+' 分配给会员 '+s.member.name+'(原型演示)');
|
||||
}
|
||||
|
||||
/* ===== 用户解绑:二次确认 ===== */
|
||||
function openPlateUnbind(idx){
|
||||
const s=PLATE_SLOTS[idx];
|
||||
const mask=document.createElement('div');
|
||||
mask.className='ov-confirm-mask';
|
||||
mask.id='plateUnbindMask';
|
||||
mask.innerHTML=`<div class="ov-confirm">
|
||||
<div class="ov-confirm__icon">⚠️</div>
|
||||
<div class="ov-confirm__msg">确认解绑该格口的会员?</div>
|
||||
<div class="ov-confirm__sub">格口 ${s.id} · ${s.member.name}(${s.member.level}),解绑后该会员餐盘回收,格口恢复为未绑定状态</div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:12px;">
|
||||
<button class="btn btn-default" onclick="closePlateUnbind()">取消</button>
|
||||
<button class="btn btn-danger" onclick="confirmPlateUnbind(${idx})">确认解绑</button>
|
||||
</div>
|
||||
</div>`;
|
||||
mask.addEventListener('click',e=>{if(e.target===mask)closePlateUnbind();});
|
||||
document.body.appendChild(mask);
|
||||
}
|
||||
function closePlateUnbind(){
|
||||
const m=document.getElementById('plateUnbindMask');
|
||||
if(!m)return;
|
||||
m.classList.add('closing');
|
||||
setTimeout(()=>m.remove(),200);
|
||||
}
|
||||
function confirmPlateUnbind(idx){
|
||||
const s=PLATE_SLOTS[idx];
|
||||
s.status='free';
|
||||
s.member=null;
|
||||
s.bindTime='—';
|
||||
s.lastUse='—';
|
||||
s.useCount=0;
|
||||
closePlateUnbind();
|
||||
closePlateSlot();
|
||||
refreshPlateTab();
|
||||
showOvToast('格口 '+s.id+' 已解绑,恢复为未绑定状态(原型演示)');
|
||||
}
|
||||
|
||||
/* ===== 净菜货柜 / 毛菜货柜(参考 sup-warehouse.html 智能货柜功能属性,重新布局) ===== */
|
||||
/* 格口状态:occupied 储物中 / idle 空闲 / offline 离线 */
|
||||
var STOCK_CABINETS={
|
||||
clean:{name:'3号净菜柜',code:'CAB-NC-001',type:'净菜柜',typeTag:'tag-blue',status:'在线 · 冷藏 2.4℃',
|
||||
slots:[
|
||||
{id:'NC3-01',status:'occupied',itemName:'净土豆丝',qty:10.2,op:'张厨师',inTime:'2026-08-18 09:15',inOrder:'IN-20260818-001',
|
||||
history:[{time:'2026-08-18 14:20',type:'取出',before:11.0,after:10.2,op:'王厨师',order:'IN-20260818-001',remark:'做土豆丝炒肉'},{time:'2026-08-18 11:30',type:'取出',before:11.8,after:11.0,op:'李主管',order:'IN-20260818-001',remark:'做宫保鸡丁'},{time:'2026-08-18 09:15',type:'存入',before:0,after:12.5,op:'张厨师',order:'IN-20260818-001',remark:'首次入柜'}]},
|
||||
{id:'NC3-02',status:'occupied',itemName:'净胡萝卜',qty:15.6,op:'李主管',inTime:'2026-08-18 09:20',inOrder:'IN-20260818-002',
|
||||
history:[{time:'2026-08-18 15:40',type:'取出',before:16.8,after:15.6,op:'王厨师',order:'IN-20260818-002',remark:'做胡萝卜炒肉'},{time:'2026-08-18 09:20',type:'存入',before:0,after:18.0,op:'李主管',order:'IN-20260818-002',remark:'首次入柜'}]},
|
||||
{id:'NC3-03',status:'occupied',itemName:'净香菇',qty:6.8,op:'王厨师',inTime:'2026-08-18 09:30',inOrder:'IN-20260818-018',
|
||||
history:[{time:'2026-08-18 16:10',type:'取出',before:7.5,after:6.8,op:'张厨师',order:'IN-20260818-018',remark:'做香菇青菜'},{time:'2026-08-18 09:30',type:'存入',before:0,after:9.0,op:'王厨师',order:'IN-20260818-018',remark:'首次入柜'}]},
|
||||
{id:'NC3-04',status:'occupied',itemName:'净西兰花',qty:8.5,op:'张厨师',inTime:'2026-08-18 09:40',inOrder:'IN-20260818-022',
|
||||
history:[{time:'2026-08-18 15:30',type:'取出',before:9.0,after:8.5,op:'李主管',order:'IN-20260818-022',remark:'做西兰花炒虾仁'},{time:'2026-08-18 09:40',type:'存入',before:0,after:10.0,op:'张厨师',order:'IN-20260818-022',remark:'首次入柜'}]},
|
||||
{id:'NC3-05',status:'occupied',itemName:'净白菜',qty:18.0,op:'王厨师',inTime:'2026-08-18 08:50',inOrder:'IN-20260818-006',
|
||||
history:[{time:'2026-08-18 11:10',type:'取出',before:18.7,after:18.0,op:'王厨师',order:'IN-20260818-006',remark:'做白菜炖豆腐'},{time:'2026-08-18 08:50',type:'存入',before:0,after:23.0,op:'王厨师',order:'IN-20260818-006',remark:'首次入柜'}]},
|
||||
{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'}
|
||||
]},
|
||||
raw:{name:'8号毛菜柜',code:'CAB-RW-008',type:'毛菜柜',typeTag:'tag-green',status:'在线 · 常温通风',
|
||||
slots:[
|
||||
{id:'RW8-A1',status:'occupied',itemName:'菠菜',qty:225.0,op:'张仓管',inTime:'2026-08-15 10:30',inOrder:'IN-20260815-001',
|
||||
history:[{time:'2026-08-17 09:00',type:'取出',before:261.0,after:225.0,op:'李仓管',order:'IN-20260815-001',remark:'净菜加工领用'},{time:'2026-08-15 10:30',type:'存入',before:0,after:286.0,op:'张仓管',order:'IN-20260815-001',remark:'毛菜入库'}]},
|
||||
{id:'RW8-A2',status:'occupied',itemName:'油麦菜',qty:60.0,op:'张仓管',inTime:'2026-08-15 11:10',inOrder:'IN-20260815-012',
|
||||
history:[{time:'2026-08-16 15:20',type:'取出',before:80.0,after:60.0,op:'李仓管',order:'IN-20260815-012',remark:'净菜加工领用'},{time:'2026-08-15 11:10',type:'存入',before:0,after:80.0,op:'张仓管',order:'IN-20260815-012',remark:'毛菜入库'}]},
|
||||
{id:'RW8-B1',status:'occupied',itemName:'小白菜',qty:80.0,op:'张仓管',inTime:'2026-08-16 08:20',inOrder:'IN-20260816-008',
|
||||
history:[{time:'2026-08-16 08:20',type:'存入',before:0,after:80.0,op:'张仓管',order:'IN-20260816-008',remark:'毛菜入库'}]},
|
||||
{id:'RW8-B2',status:'occupied',itemName:'生菜',qty:42.5,op:'张仓管',inTime:'2026-08-16 09:00',inOrder:'IN-20260816-009',
|
||||
history:[{time:'2026-08-17 14:30',type:'取出',before:50.0,after:42.5,op:'李仓管',order:'IN-20260816-009',remark:'晚餐备菜领用'},{time:'2026-08-16 09:00',type:'存入',before:0,after:50.0,op:'张仓管',order:'IN-20260816-009',remark:'毛菜入库'}]},
|
||||
{id:'RW8-B3',status:'occupied',itemName:'空心菜',qty:30.0,op:'张仓管',inTime:'2026-08-15 08:30',inOrder:'IN-20260815-015',
|
||||
history:[{time:'2026-08-16 10:10',type:'取出',before:36.0,after:30.0,op:'李仓管',order:'IN-20260815-015',remark:'净菜加工领用'},{time:'2026-08-15 08:30',type:'存入',before:0,after:36.0,op:'张仓管',order:'IN-20260815-015',remark:'毛菜入库'}]},
|
||||
{id:'RW8-C1',status:'occupied',itemName:'芹菜',qty:12.0,op:'张仓管',inTime:'2026-08-14 16:00',inOrder:'IN-20260814-003',
|
||||
history:[{time:'2026-08-17 08:50',type:'取出',before:15.0,after:12.0,op:'王仓管',order:'IN-20260814-003',remark:'凉拌芹菜领用'},{time:'2026-08-14 16:00',type:'存入',before:0,after:15.0,op:'张仓管',order:'IN-20260814-003',remark:'毛菜入库'}]},
|
||||
{id:'RW8-C2',status:'idle'},{id:'RW8-C3',status:'idle'}
|
||||
]}
|
||||
};
|
||||
|
||||
function renderStockCabinet(kind){
|
||||
const cab=STOCK_CABINETS[kind];
|
||||
const occupied=cab.slots.filter(s=>s.status==='occupied').length;
|
||||
const slotsHtml=cab.slots.map((s,i)=>{
|
||||
if(s.status==='idle'){
|
||||
return `<div class="ov-slot ov-slot--empty" onclick="showStockSlot('${kind}',${i})"><div class="ov-slot__id">${s.id}</div><div class="ov-slot__name">空闲</div></div>`;
|
||||
}
|
||||
const cls=s.status==='offline'?'ov-slot offline':'ov-slot occupied';
|
||||
const badge=s.status==='offline'?'<div class="ov-slot__badge gray"></div>':'<div class="ov-slot__badge green"></div>';
|
||||
const qtyHtml=s.status==='offline'?'<div class="ov-slot__expire" style="color:var(--text-placeholder)">离线</div>':`<div class="ov-slot__remain">${s.qty}kg</div><div class="ov-slot__expire">入库 ${s.inTime.slice(5,10)}</div>`;
|
||||
return `<div class="${cls}" onclick="showStockSlot('${kind}',${i})">
|
||||
${badge}
|
||||
<div class="ov-slot__id">${s.id}</div>
|
||||
<div class="ov-slot__name">${s.itemName}</div>
|
||||
${qtyHtml}
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="ov-cabinet-header">
|
||||
<div style="font-size:var(--font-base);font-weight:600;color:var(--text-primary)">${cab.name}(${cab.code})· <span class="tag ${cab.typeTag}">${cab.type}</span></div>
|
||||
<div class="ov-cabinet-status">${cab.status} · 已存放 ${occupied}/${cab.slots.length} 格口</div>
|
||||
</div>
|
||||
<div class="ov-cabinet-grid">${slotsHtml}</div>
|
||||
<div class="ov-cabinet-legend">
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot storing"></div>储物中</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot idle"></div>空闲</div>
|
||||
<div class="ov-legend__item"><div class="ov-legend__dot offline"></div>离线</div>
|
||||
<div class="ov-legend__item" style="margin-left:auto;color:var(--text-placeholder)">点击格口可查看存放详情与存取历史</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* 净菜/毛菜格口详情弹窗:存放信息 + 存取历史 */
|
||||
function showStockSlot(kind,idx){
|
||||
const cab=STOCK_CABINETS[kind];
|
||||
const s=cab.slots[idx];
|
||||
if(s.status==='idle'){
|
||||
showOvToast('该格口为空闲状态,暂无存放信息');
|
||||
return;
|
||||
}
|
||||
const statusTag=s.status==='occupied'?'<span class="tag tag-green">储物中</span>':'<span class="tag tag-gray">离线</span>';
|
||||
let html=`<div class="plate-detail__section"><div class="plate-detail__section-title">存放信息</div><div class="plate-detail__grid">
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">所属柜</span><span class="plate-detail__value">${cab.name}(${cab.code})</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">格口</span><span class="plate-detail__value">${s.id}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">格口状态</span><span class="plate-detail__value">${statusTag}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">存放物品</span><span class="plate-detail__value">${s.itemName}<span class="tag ${cab.typeTag}" style="margin-left:6px">${cab.type==='净菜柜'?'净菜':'毛菜'}</span></span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">当前数量(kg)</span><span class="plate-detail__value">${s.qty.toFixed(1)}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">操作人</span><span class="plate-detail__value">${s.op}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">入库时间</span><span class="plate-detail__value">${s.inTime}</span></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">关联入库单</span><span class="plate-detail__value" style="color:var(--primary)">${s.inOrder}</span></div>
|
||||
</div></div>`;
|
||||
const rows=(s.history||[]).slice().sort((a,b)=>b.time.localeCompare(a.time));
|
||||
if(rows.length){
|
||||
html+=`<div class="plate-detail__section"><div class="plate-detail__section-title">存取历史</div>
|
||||
<table class="slot-his-table"><thead><tr><th>操作时间</th><th>操作类型</th><th>操作前(kg)</th><th>操作后(kg)</th><th>余量变化</th><th>操作人</th><th>关联单号</th><th>备注</th></tr></thead><tbody>
|
||||
${rows.map(r=>{
|
||||
const isPos=r.type==='存入';
|
||||
const delta=Math.abs(r.after-r.before).toFixed(1);
|
||||
const typeHtml=isPos?'<span class="tag tag-blue">存入</span>':'<span class="tag tag-orange">取出</span>';
|
||||
return `<tr><td>${r.time}</td><td>${typeHtml}</td><td>${r.before.toFixed(1)}</td><td>${r.after.toFixed(1)}</td><td><span class="slot-his-delta ${isPos?'pos':'neg'}">${isPos?'+':'-'}${delta} ${isPos?'↑':'↓'}</span></td><td>${r.op}</td><td><a class="link">${r.order}</a></td><td>${r.remark||'—'}</td></tr>`;
|
||||
}).join('')}
|
||||
</tbody></table></div>`;
|
||||
}
|
||||
const mask=document.createElement('div');
|
||||
mask.className='slot-his-mask';
|
||||
mask.id='stockSlotMask';
|
||||
mask.innerHTML=`<div class="slot-his-modal" style="width:960px;height:auto;max-height:78vh">
|
||||
<div class="slot-his-modal__header"><div class="slot-his-modal__title">格口存放详情 — ${s.id}(${cab.name})</div><button class="slot-his-modal__close" onclick="closeStockSlot()">×</button></div>
|
||||
<div class="slot-his-modal__body">${html}</div>
|
||||
</div>`;
|
||||
mask.addEventListener('click',e=>{if(e.target===mask)closeStockSlot();});
|
||||
document.body.appendChild(mask);
|
||||
}
|
||||
function closeStockSlot(){
|
||||
const m=document.getElementById('stockSlotMask');
|
||||
if(!m)return;
|
||||
m.classList.add('closing');
|
||||
setTimeout(()=>m.remove(),200);
|
||||
}
|
||||
|
||||
/* ===== 智能电表 / 智能水表 / 智能气表(参考 prod-qc-daily.html 水电气表:余量展示 + 数据录入 + 日使用量统计) ===== */
|
||||
var ENERGY_STATE={
|
||||
elec:{title:'智能电表',icon:'⚡',meter:'EM-A01',unit:'kWh',remain:28734.6,todayUse:186,todayCost:167.4,rate:'当前功率',rateVal:'3.2 kW',hint:'当前功率正常,较昨日同时段下降 4.2%',color:'#1677ff'},
|
||||
water:{title:'智能水表',icon:'💧',meter:'WM-A01',unit:'m³',remain:12468.3,todayUse:22,todayCost:79.2,rate:'当前流量',rateVal:'1.8 L/min',hint:'当前流速正常,无异常用水告警',color:'#13c2c2'},
|
||||
gas:{title:'智能气表',icon:'🔥',meter:'GM-A01',unit:'m³',remain:8421.5,todayUse:68,todayCost:425.0,rate:'当前流量',rateVal:'4.6 m³/h',hint:'当前燃气流量正常,无泄漏告警',color:'#fa8c16'}
|
||||
};
|
||||
/* 日使用量 Mock:每表 12 天数据(时间倒序) */
|
||||
var ENERGY_DAILY={
|
||||
elec:[
|
||||
{date:'2026-08-17',use:372,cost:334.8,peak:'11:30',op:'自动采集'},{date:'2026-08-16',use:398,cost:358.2,peak:'11:45',op:'自动采集'},
|
||||
{date:'2026-08-15',use:386,cost:347.4,peak:'11:20',op:'自动采集'},{date:'2026-08-14',use:362,cost:325.8,peak:'12:00',op:'自动采集'},
|
||||
{date:'2026-08-13',use:405,cost:364.5,peak:'11:40',op:'自动采集'},{date:'2026-08-12',use:380,cost:342.0,peak:'11:50',op:'自动采集'},
|
||||
{date:'2026-08-11',use:358,cost:322.2,peak:'11:10',op:'自动采集'},{date:'2026-08-10',use:341,cost:306.9,peak:'10:55',op:'自动采集'},
|
||||
{date:'2026-08-09',use:329,cost:296.1,peak:'11:15',op:'自动采集'},{date:'2026-08-08',use:412,cost:370.8,peak:'12:10',op:'自动采集'},
|
||||
{date:'2026-08-07',use:396,cost:356.4,peak:'11:35',op:'自动采集'},{date:'2026-08-06',use:375,cost:337.5,peak:'11:25',op:'自动采集'}
|
||||
],
|
||||
water:[
|
||||
{date:'2026-08-17',use:40,cost:144.0,peak:'12:10',op:'自动采集'},{date:'2026-08-16',use:45,cost:162.0,peak:'12:20',op:'自动采集'},
|
||||
{date:'2026-08-15',use:42,cost:151.2,peak:'11:50',op:'自动采集'},{date:'2026-08-14',use:38,cost:136.8,peak:'12:00',op:'自动采集'},
|
||||
{date:'2026-08-13',use:46,cost:165.6,peak:'12:15',op:'自动采集'},{date:'2026-08-12',use:41,cost:147.6,peak:'11:45',op:'自动采集'},
|
||||
{date:'2026-08-11',use:36,cost:129.6,peak:'11:40',op:'自动采集'},{date:'2026-08-10',use:33,cost:118.8,peak:'11:30',op:'自动采集'},
|
||||
{date:'2026-08-09',use:31,cost:111.6,peak:'11:35',op:'自动采集'},{date:'2026-08-08',use:48,cost:172.8,peak:'12:30',op:'自动采集'},
|
||||
{date:'2026-08-07',use:44,cost:158.4,peak:'12:05',op:'自动采集'},{date:'2026-08-06',use:39,cost:140.4,peak:'11:55',op:'自动采集'}
|
||||
],
|
||||
gas:[
|
||||
{date:'2026-08-17',use:120,cost:750.0,peak:'11:20',op:'自动采集'},{date:'2026-08-16',use:130,cost:812.5,peak:'11:30',op:'自动采集'},
|
||||
{date:'2026-08-15',use:126,cost:787.5,peak:'11:10',op:'自动采集'},{date:'2026-08-14',use:115,cost:718.8,peak:'11:40',op:'自动采集'},
|
||||
{date:'2026-08-13',use:132,cost:825.0,peak:'11:25',op:'自动采集'},{date:'2026-08-12',use:122,cost:762.5,peak:'11:15',op:'自动采集'},
|
||||
{date:'2026-08-11',use:110,cost:687.5,peak:'11:05',op:'自动采集'},{date:'2026-08-10',use:104,cost:650.0,peak:'10:50',op:'自动采集'},
|
||||
{date:'2026-08-09',use:98,cost:612.5,peak:'10:45',op:'自动采集'},{date:'2026-08-08',use:138,cost:862.5,peak:'11:55',op:'自动采集'},
|
||||
{date:'2026-08-07',use:128,cost:800.0,peak:'11:35',op:'自动采集'},{date:'2026-08-06',use:117,cost:731.3,peak:'11:20',op:'自动采集'}
|
||||
]
|
||||
};
|
||||
var ENERGY_PAGE_SIZE=5;
|
||||
var ENERGY_CURRENT_PAGE={elec:1,water:1,gas:1};
|
||||
|
||||
function renderEnergyTab(type){
|
||||
const e=ENERGY_STATE[type];
|
||||
const unit=e.unit;
|
||||
return `
|
||||
<div class="ov-energy-card">
|
||||
<span class="ov-energy-card__entry" onclick="toggleEnergyEditOv('${type}')">✏️ 录入</span>
|
||||
<div class="ov-energy-card__header"><span class="ov-energy-card__icon">${e.icon}</span><span class="ov-energy-card__title">${e.title}</span><span class="ov-energy-card__meter">表号:${e.meter}</span></div>
|
||||
<div class="ov-energy-card__value" style="color:${e.color}" id="energyValue_${type}">${e.remain} <span>${unit}</span></div>
|
||||
<div class="ov-energy-card__rows">
|
||||
<div class="ov-energy-card__row"><span class="ov-energy-card__row-label">今日用量</span><span class="ov-energy-card__row-value" id="energyUse_${type}">${e.todayUse} ${unit}</span></div>
|
||||
<div class="ov-energy-card__row"><span class="ov-energy-card__row-label">今日费用</span><span class="ov-energy-card__row-value" id="energyCost_${type}">¥${e.todayCost.toFixed(1)}</span></div>
|
||||
<div class="ov-energy-card__row"><span class="ov-energy-card__row-label">状态</span><span class="ov-energy-card__row-value"><span class="tag tag-green">正常</span></span></div>
|
||||
<div class="ov-energy-card__row"><span class="ov-energy-card__row-label">${e.rate}</span><span class="ov-energy-card__row-value">${e.rateVal}</span></div>
|
||||
</div>
|
||||
<div class="ov-energy-card__hint">${e.hint}</div>
|
||||
<div class="ov-energy-edit" id="energyEdit_${type}" style="display:none;">
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">当前余量(${unit})</span><input type="number" step="0.1" id="energyInputRemain_${type}" value="${e.remain}"></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">今日用量(${unit})</span><input type="number" step="0.1" id="energyInputUse_${type}" value="${e.todayUse}"></div>
|
||||
<div class="plate-detail__field"><span class="plate-detail__label">今日费用(元)</span><input type="number" step="0.1" id="energyInputCost_${type}" value="${e.todayCost}"></div>
|
||||
<div class="ov-energy-edit__actions">
|
||||
<button class="btn btn-default" onclick="cancelEnergyEditOv('${type}')">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveEnergyEditOv('${type}')">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ov-section-title">日使用量统计</div>
|
||||
<div id="energyDaily_${type}"></div>`;
|
||||
}
|
||||
function renderEnergyDaily(type){
|
||||
const wrap=document.getElementById('energyDaily_'+type);
|
||||
if(!wrap)return;
|
||||
const list=ENERGY_DAILY[type];
|
||||
const e=ENERGY_STATE[type];
|
||||
const total=list.length;
|
||||
const totalPages=Math.max(1,Math.ceil(total/ENERGY_PAGE_SIZE));
|
||||
let page=ENERGY_CURRENT_PAGE[type];
|
||||
if(page>totalPages)page=totalPages;
|
||||
if(page<1)page=1;
|
||||
ENERGY_CURRENT_PAGE[type]=page;
|
||||
const start=(page-1)*ENERGY_PAGE_SIZE;
|
||||
const pageRows=list.slice(start,start+ENERGY_PAGE_SIZE);
|
||||
const unit=e.unit;
|
||||
let html='<div style="overflow-x:auto"><table class="ov-daily-table"><thead><tr><th>记录日期</th><th>用量('+unit+')</th><th>费用(元)</th><th>峰值时段</th><th>采集方式</th><th>操作</th></tr></thead><tbody>';
|
||||
pageRows.forEach(r=>{
|
||||
html+=`<tr><td>${r.date}</td><td>${r.use}</td><td>¥${r.cost.toFixed(1)}</td><td>${r.peak}</td><td>${r.op}</td><td class="td-actions"><a class="link" onclick="showOvToast('单日明细待开发(占位)')">详情</a></td></tr>`;
|
||||
});
|
||||
html+='</tbody></table></div>';
|
||||
html+=`<div class="ov-daily-pagination"><span class="ov-daily-pagination__total">共 ${total} 条</span>`;
|
||||
html+=`<button class="page-btn" onclick="goEnergyPage('${type}',${page-1})" ${page<=1?'disabled style="opacity:.4;cursor:not-allowed"':''}>‹</button>`;
|
||||
for(let i=1;i<=totalPages;i++){
|
||||
html+=`<button class="page-btn${i===page?' active':''}" onclick="goEnergyPage('${type}',${i})">${i}</button>`;
|
||||
}
|
||||
html+=`<button class="page-btn" onclick="goEnergyPage('${type}',${page+1})" ${page>=totalPages?'disabled style="opacity:.4;cursor:not-allowed"':''}>›</button></div>`;
|
||||
wrap.innerHTML=html;
|
||||
}
|
||||
function goEnergyPage(type,p){
|
||||
ENERGY_CURRENT_PAGE[type]=p;
|
||||
renderEnergyDaily(type);
|
||||
}
|
||||
function toggleEnergyEditOv(type){
|
||||
const box=document.getElementById('energyEdit_'+type);
|
||||
if(box)box.style.display=box.style.display==='none'?'grid':'none';
|
||||
}
|
||||
function cancelEnergyEditOv(type){
|
||||
const e=ENERGY_STATE[type];
|
||||
const box=document.getElementById('energyEdit_'+type);
|
||||
if(box){
|
||||
box.style.display='none';
|
||||
document.getElementById('energyInputRemain_'+type).value=e.remain;
|
||||
document.getElementById('energyInputUse_'+type).value=e.todayUse;
|
||||
document.getElementById('energyInputCost_'+type).value=e.todayCost;
|
||||
}
|
||||
}
|
||||
function saveEnergyEditOv(type){
|
||||
const e=ENERGY_STATE[type];
|
||||
const remain=parseFloat(document.getElementById('energyInputRemain_'+type).value);
|
||||
const use=parseFloat(document.getElementById('energyInputUse_'+type).value);
|
||||
const cost=parseFloat(document.getElementById('energyInputCost_'+type).value);
|
||||
if(isNaN(remain)||isNaN(use)||isNaN(cost)){showOvToast('请填写完整的录入数据');return;}
|
||||
e.remain=remain;e.todayUse=use;e.todayCost=cost;
|
||||
document.getElementById('energyValue_'+type).innerHTML=`${remain} <span>${e.unit}</span>`;
|
||||
document.getElementById('energyUse_'+type).textContent=`${use} ${e.unit}`;
|
||||
document.getElementById('energyCost_'+type).textContent=`¥${cost.toFixed(1)}`;
|
||||
document.getElementById('energyEdit_'+type).style.display='none';
|
||||
showOvToast(e.title+'数据已保存(原型演示)');
|
||||
}
|
||||
|
||||
/* ===== 格口历史记录弹窗(叠加在设备概览之上,参考净菜进柜历史记录弹窗) ===== */
|
||||
var OV_SLOT_HISTORY={
|
||||
0:{cabinet:'B区智能柜-1号',slot:'B1-01',rows:[
|
||||
{time:'2026-08-18 14:20',name:'宫保鸡丁餐品包',type:'取出',before:2.8,after:2.4,op:'取餐人 张*明',order:'QC-2026-0818-021',remark:'扫码取餐'},
|
||||
{time:'2026-08-18 12:05',name:'宫保鸡丁餐品包',type:'取出',before:3.3,after:2.8,op:'取餐人 李*华',order:'QC-2026-0818-015',remark:'扫码取餐'},
|
||||
{time:'2026-08-18 09:15',name:'宫保鸡丁餐品包',type:'存入',before:0.0,after:3.3,op:'李主管',order:'JC-2026-0818-001',remark:'烹制完入柜'}
|
||||
]},
|
||||
1:{cabinet:'B区智能柜-1号',slot:'B1-02',rows:[
|
||||
{time:'2026-08-18 15:40',name:'红烧肉餐品包',type:'取出',before:3.6,after:3.1,op:'取餐人 王*强',order:'QC-2026-0818-030',remark:'扫码取餐'},
|
||||
{time:'2026-08-18 11:00',name:'红烧肉餐品包',type:'取出',before:4.2,after:3.6,op:'取餐人 赵*敏',order:'QC-2026-0818-009',remark:'扫码取餐'},
|
||||
{time:'2026-08-18 09:20',name:'红烧肉餐品包',type:'存入',before:0.0,after:4.2,op:'李主管',order:'JC-2026-0818-002',remark:'烹制完入柜'}
|
||||
]},
|
||||
2:{cabinet:'B区智能柜-1号',slot:'B1-03',rows:[
|
||||
{time:'2026-08-18 16:10',name:'清蒸鱼餐品包',type:'取出',before:2.2,after:1.8,op:'取餐人 陈*芳',order:'QC-2026-0818-035',remark:'扫码取餐'},
|
||||
{time:'2026-08-18 13:20',name:'清蒸鱼餐品包',type:'取出',before:2.6,after:2.2,op:'取餐人 刘*洋',order:'QC-2026-0818-024',remark:'扫码取餐'},
|
||||
{time:'2026-08-18 10:30',name:'清蒸鱼餐品包',type:'存入',before:0.0,after:2.6,op:'王厨师',order:'JC-2026-0818-003',remark:'烹制完入柜'}
|
||||
]},
|
||||
3:{cabinet:'B区智能柜-1号',slot:'B1-04',rows:[
|
||||
{time:'2026-08-18 15:30',name:'净西兰花',type:'取用',before:5.0,after:4.5,op:'张厨师',order:'JC-2026-0818-012',remark:'做西兰花炒虾仁'},
|
||||
{time:'2026-08-18 13:10',name:'净西兰花',type:'取用',before:5.5,after:5.0,op:'张厨师',order:'JC-2026-0818-012',remark:'做西兰花炒虾仁'},
|
||||
{time:'2026-08-18 09:40',name:'净西兰花',type:'存入',before:0.0,after:5.5,op:'张厨师',order:'JC-2026-0818-004',remark:'首次入柜'}
|
||||
]},
|
||||
4:{cabinet:'B区智能柜-1号',slot:'B1-05',rows:[
|
||||
{time:'2026-08-18 15:40',name:'净胡萝卜',type:'取用',before:5.7,after:5.2,op:'王厨师',order:'JC-2026-0818-016',remark:'做胡萝卜炒肉'},
|
||||
{time:'2026-08-18 11:00',name:'净胡萝卜',type:'取用',before:6.2,after:5.7,op:'王厨师',order:'JC-2026-0818-016',remark:'做胡萝卜炒肉'},
|
||||
{time:'2026-08-18 09:20',name:'净胡萝卜',type:'存入',before:0.0,after:6.2,op:'李主管',order:'JC-2026-0818-005',remark:'首次入柜'}
|
||||
]},
|
||||
5:{cabinet:'B区智能柜-1号',slot:'B1-06',rows:[
|
||||
{time:'2026-08-12 11:10',name:'净白菜',type:'取用',before:6.5,after:6.0,op:'王厨师',order:'JC-2026-0812-008',remark:'做白菜炖豆腐'},
|
||||
{time:'2026-08-12 10:00',name:'净白菜',type:'取用',before:7.2,after:6.5,op:'王厨师',order:'JC-2026-0812-008',remark:'做醋溜白菜'},
|
||||
{time:'2026-08-11 17:30',name:'净白菜',type:'取用',before:7.8,after:7.2,op:'李主管',order:'JC-2026-0811-005',remark:'晚餐备菜'},
|
||||
{time:'2026-08-11 11:50',name:'净白菜',type:'取用',before:8.4,after:7.8,op:'张厨师',order:'JC-2026-0811-005',remark:'午餐备菜'},
|
||||
{time:'2026-08-10 14:30',name:'净白菜',type:'存入',before:0.0,after:8.4,op:'王厨师',order:'JC-2026-0810-003',remark:'首次入柜'}
|
||||
]},
|
||||
6:{cabinet:'B区智能柜-1号',slot:'B1-07',rows:[
|
||||
{time:'2026-08-18 16:30',name:'净香菇',type:'取用',before:2.6,after:2.2,op:'张厨师',order:'JC-2026-0818-018',remark:'做香菇青菜'},
|
||||
{time:'2026-08-18 14:00',name:'净香菇',type:'取用',before:3.0,after:2.6,op:'张厨师',order:'JC-2026-0818-018',remark:'做香菇青菜'},
|
||||
{time:'2026-08-18 09:30',name:'净香菇',type:'存入',before:0.0,after:3.0,op:'王厨师',order:'JC-2026-0818-006',remark:'首次入柜'}
|
||||
]}
|
||||
};
|
||||
var OV_HIS_PAGE_SIZE=5;
|
||||
var OV_HIS_CURRENT={idx:0,page:1};
|
||||
|
||||
function showSlotHistory(idx){
|
||||
const d=OV_SLOT_HISTORY[idx];
|
||||
if(!d){
|
||||
showOvToast('该格口暂无历史记录(空闲格口)');
|
||||
return;
|
||||
}
|
||||
OV_HIS_CURRENT={idx:idx,page:1};
|
||||
const cinCount=d.rows.filter(r=>r.type==='存入').length;
|
||||
const coutCount=d.rows.filter(r=>r.type!=='存入').length;
|
||||
const summary=`<div class="slot-his-summary">
|
||||
<div class="slot-his-summary__item"><span class="label">所属柜:</span><strong>${d.cabinet}</strong></div>
|
||||
<div class="slot-his-summary__item"><span class="label">格口:</span><strong>${d.slot}</strong></div>
|
||||
<div class="slot-his-summary__item"><span class="label">累计存入:</span><span class="slot-his-count in">${cinCount} 次</span></div>
|
||||
<div class="slot-his-summary__item"><span class="label">累计取出/取用:</span><span class="slot-his-count out">${coutCount} 次</span></div>
|
||||
</div>`;
|
||||
const mask=document.createElement('div');
|
||||
mask.className='slot-his-mask';
|
||||
mask.id='slotHisMask';
|
||||
mask.innerHTML=`<div class="slot-his-modal">
|
||||
<div class="slot-his-modal__header"><div class="slot-his-modal__title">格口历史记录 — ${d.slot}</div><button class="slot-his-modal__close" onclick="closeSlotHistory()">×</button></div>
|
||||
<div class="slot-his-modal__body">${summary}<div id="ovHisTableWrap"></div></div>
|
||||
<div class="slot-his-modal__footer"><div id="ovHisFooterWrap"></div></div>
|
||||
</div>`;
|
||||
mask.addEventListener('click',e=>{if(e.target===mask)closeSlotHistory();});
|
||||
document.body.appendChild(mask);
|
||||
renderOvHistoryPage();
|
||||
}
|
||||
function closeSlotHistory(){
|
||||
const m=document.getElementById('slotHisMask');
|
||||
if(!m)return;
|
||||
m.classList.add('closing');
|
||||
setTimeout(()=>m.remove(),200);
|
||||
}
|
||||
function renderOvHistoryPage(){
|
||||
const wrap=document.getElementById('ovHisTableWrap');
|
||||
const footer=document.getElementById('ovHisFooterWrap');
|
||||
if(!wrap)return;
|
||||
const d=OV_SLOT_HISTORY[OV_HIS_CURRENT.idx];
|
||||
/* 时间倒序展示(最新在前) */
|
||||
const sortedRows=d.rows.slice().sort((a,b)=>b.time.localeCompare(a.time));
|
||||
const total=sortedRows.length;
|
||||
const totalPages=Math.max(1,Math.ceil(total/OV_HIS_PAGE_SIZE));
|
||||
if(OV_HIS_CURRENT.page>totalPages)OV_HIS_CURRENT.page=totalPages;
|
||||
if(OV_HIS_CURRENT.page<1)OV_HIS_CURRENT.page=1;
|
||||
const start=(OV_HIS_CURRENT.page-1)*OV_HIS_PAGE_SIZE;
|
||||
const pageRows=sortedRows.slice(start,start+OV_HIS_PAGE_SIZE);
|
||||
let html='<div style="overflow-x:auto"><table class="slot-his-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(r=>{
|
||||
const isPos=r.type==='存入';
|
||||
const delta=Math.abs(r.after-r.before).toFixed(1);
|
||||
const deltaHtml=`<span class="slot-his-delta ${isPos?'pos':'neg'}">${isPos?'+':'-'}${delta} ${isPos?'↑':'↓'}</span>`;
|
||||
const typeHtml=isPos?'<span class="tag tag-blue">存入</span>':(r.type==='取出'?'<span class="tag tag-orange">取出</span>':'<span class="tag tag-orange">取用</span>');
|
||||
const orderHtml=r.order==='—'?'—':`<a class="link">${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;
|
||||
if(footer){
|
||||
let pg=`<div class="slot-his-pagination"><span class="slot-his-pagination__total">共 ${total} 条</span>`;
|
||||
pg+=`<button class="page-btn" onclick="goOvHistoryPage(${OV_HIS_CURRENT.page-1})" ${OV_HIS_CURRENT.page<=1?'disabled style="opacity:.4;cursor:not-allowed"':''}>‹</button>`;
|
||||
for(let i=1;i<=totalPages;i++){
|
||||
pg+=`<button class="page-btn${i===OV_HIS_CURRENT.page?' active':''}" onclick="goOvHistoryPage(${i})">${i}</button>`;
|
||||
}
|
||||
pg+=`<button class="page-btn" onclick="goOvHistoryPage(${OV_HIS_CURRENT.page+1})" ${OV_HIS_CURRENT.page>=totalPages?'disabled style="opacity:.4;cursor:not-allowed"':''}>›</button></div>`;
|
||||
footer.innerHTML=pg;
|
||||
}
|
||||
}
|
||||
function goOvHistoryPage(p){OV_HIS_CURRENT.page=p;renderOvHistoryPage();}
|
||||
|
||||
/* 轻量 toast */
|
||||
function showOvToast(msg){
|
||||
const old=document.getElementById('ovToast');
|
||||
if(old)old.remove();
|
||||
const t=document.createElement('div');
|
||||
t.id='ovToast';
|
||||
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(()=>{if(t)t.remove();},2200);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -544,12 +544,12 @@ tbody td.td-actions{overflow:visible;}
|
||||
<tr><th>会员卡号</th><th>姓名</th><th>用户编号</th><th>会员等级</th><th>当前积分</th><th>累计消费(元)</th><th>本月消费</th><th>开通日期</th><th>有效期至</th><th>最近活跃</th><th>会员状态</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>MB-202401-001</td><td>张伟</td><td>EMP001</td><td><span class="tag tag-red">钻石</span></td><td>8,628</td><td>12,860</td><td>¥1,286</td><td>2024-01-15</td><td>2027-01-14</td><td>2026-04-28 11:32</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a></td></tr>
|
||||
<tr><td>MB-202405-008</td><td>李娜</td><td>EMP002</td><td><span class="tag tag-orange">金卡</span></td><td>3,286</td><td>4,820</td><td>¥682</td><td>2024-05-08</td><td>2027-05-07</td><td>2026-04-28 12:08</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a></td></tr>
|
||||
<tr><td>MB-202508-015</td><td>赵敏</td><td>EMP004</td><td><span class="tag tag-blue">银卡</span></td><td>1,420</td><td>2,180</td><td>¥486</td><td>2025-08-12</td><td>2027-08-11</td><td>2026-04-28 11:30</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a></td></tr>
|
||||
<tr><td>MB-202510-026</td><td>刘芳</td><td>EMP006</td><td><span class="tag tag-blue">银卡</span></td><td>986</td><td>1,520</td><td>¥320</td><td>2025-10-20</td><td>2027-10-19</td><td>2026-04-28 11:48</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a></td></tr>
|
||||
<tr><td>MB-202312-003</td><td>孙磊</td><td>EMP010</td><td><span class="tag tag-orange">金卡</span></td><td>5,120</td><td>7,860</td><td>¥0</td><td>2023-12-08</td><td>2026-12-07</td><td>2026-04-15 12:20</td><td><span class="tag tag-orange">即将到期</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a></td></tr>
|
||||
<tr><td>MB-202402-005</td><td>周明</td><td>EMP008</td><td><span class="tag tag-red">钻石</span></td><td>12,486</td><td>16,820</td><td>¥0</td><td>2024-02-10</td><td>2027-02-09</td><td>2026-03-28 12:15</td><td><span class="tag tag-gray">休眠</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a></td></tr>
|
||||
<tr><td>MB-202401-001</td><td>张伟</td><td>EMP001</td><td><span class="tag tag-red">钻石</span></td><td>8,628</td><td>12,860</td><td>¥1,286</td><td>2024-01-15</td><td>2027-01-14</td><td>2026-04-28 11:32</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a> <a class="link" data-action="benefits">会员权益</a></td></tr>
|
||||
<tr><td>MB-202405-008</td><td>李娜</td><td>EMP002</td><td><span class="tag tag-orange">金卡</span></td><td>3,286</td><td>4,820</td><td>¥682</td><td>2024-05-08</td><td>2027-05-07</td><td>2026-04-28 12:08</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a> <a class="link" data-action="benefits">会员权益</a></td></tr>
|
||||
<tr><td>MB-202508-015</td><td>赵敏</td><td>EMP004</td><td><span class="tag tag-blue">银卡</span></td><td>1,420</td><td>2,180</td><td>¥486</td><td>2025-08-12</td><td>2027-08-11</td><td>2026-04-28 11:30</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a> <a class="link" data-action="benefits">会员权益</a></td></tr>
|
||||
<tr><td>MB-202510-026</td><td>刘芳</td><td>EMP006</td><td><span class="tag tag-blue">银卡</span></td><td>986</td><td>1,520</td><td>¥320</td><td>2025-10-20</td><td>2027-10-19</td><td>2026-04-28 11:48</td><td><span class="tag tag-green">正常</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a> <a class="link" data-action="benefits">会员权益</a></td></tr>
|
||||
<tr><td>MB-202312-003</td><td>孙磊</td><td>EMP010</td><td><span class="tag tag-orange">金卡</span></td><td>5,120</td><td>7,860</td><td>¥0</td><td>2023-12-08</td><td>2026-12-07</td><td>2026-04-15 12:20</td><td><span class="tag tag-orange">即将到期</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a> <a class="link" data-action="benefits">会员权益</a></td></tr>
|
||||
<tr><td>MB-202402-005</td><td>周明</td><td>EMP008</td><td><span class="tag tag-red">钻石</span></td><td>12,486</td><td>16,820</td><td>¥0</td><td>2024-02-10</td><td>2027-02-09</td><td>2026-03-28 12:15</td><td><span class="tag tag-gray">休眠</span></td><td class="td-actions"><a class="link" data-action="edit">编辑</a> <a class="link" data-action="detail">详情</a> <a class="link" data-action="face">人脸采集</a> <a class="link" data-action="benefits">会员权益</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1059,8 +1059,72 @@ document.querySelectorAll('.tab-panel')[2].querySelectorAll('tbody tr').forEach(
|
||||
// 人脸采集
|
||||
var faceLink = row.querySelector('[data-action="face"]');
|
||||
if (faceLink) faceLink.addEventListener('click', function() { showFaceModal(name); });
|
||||
// 会员权益
|
||||
var benefitsLink = row.querySelector('[data-action="benefits"]');
|
||||
if (benefitsLink) benefitsLink.addEventListener('click', function() { showVipBenefitsModal(name, tds); });
|
||||
});
|
||||
|
||||
/* ========== 会员权益弹窗(Tab3) ========== */
|
||||
/* 交互说明:点击操作列「会员权益」弹窗展示该会员的权益概要与专属餐盘柜绑定信息。
|
||||
会员权益体系尚在规划中,当前已明确权益为「专属餐盘柜」,此处回显柜子/格口/绑定时间/最后使用状态/累计使用次数。 */
|
||||
// 模拟数据:会员专属餐盘柜绑定信息(未配置的会员显示空态)
|
||||
var vipTrayLockerMap = {
|
||||
'张伟': { locker: 'A区-01号柜', cell: '12号格口', bindTime: '2024-01-20 10:32', lastUseTime: '2026-04-28 11:32', lastUseStatus: '已归还', useCount: 1286 },
|
||||
'李娜': { locker: 'A区-01号柜', cell: '08号格口', bindTime: '2024-05-15 09:18', lastUseTime: '2026-04-28 12:08', lastUseStatus: '使用中', useCount: 862 },
|
||||
'赵敏': { locker: 'B区-03号柜', cell: '21号格口', bindTime: '2025-08-20 14:05', lastUseTime: '2026-04-28 11:30', lastUseStatus: '已归还', useCount: 316 },
|
||||
'周明': { locker: 'B区-03号柜', cell: '05号格口', bindTime: '2024-02-18 16:47', lastUseTime: '2026-03-28 12:15', lastUseStatus: '已归还', useCount: 2048 }
|
||||
};
|
||||
// 等级权益文案映射(与等级配置 Tab 保持一致)
|
||||
var vipLevelBenefitsMap = {
|
||||
'钻石': '专属营养师、VIP定制菜单、季度体检券、专属餐盘柜、专属取餐窗口、2.0x 积分倍数、9.0折 点餐折扣',
|
||||
'金卡': '生日券 ¥30、AI营养周报、专属客服、专属餐盘柜、优先取餐、1.5x 积分倍数、9.5折 点餐折扣',
|
||||
'银卡': '生日券 ¥10、月度营养报告、专属餐盘柜、1.2x 积分倍数、9.8折 点餐折扣',
|
||||
'普通': '基础点餐功能、专属餐盘柜'
|
||||
};
|
||||
|
||||
function showVipBenefitsModal(name, tds) {
|
||||
var levelText = tds[3].querySelector('.tag').textContent;
|
||||
var cardNo = tds[0].textContent;
|
||||
var locker = vipTrayLockerMap[name];
|
||||
|
||||
// 专属餐盘柜信息区:已绑定展示明细,未绑定展示空态
|
||||
var lockerHtml;
|
||||
if (locker) {
|
||||
var statusTag = locker.lastUseStatus === '使用中' ? 'tag-blue' : 'tag-green';
|
||||
lockerHtml = ''
|
||||
+ '<table class="meal-history-table">'
|
||||
+ ' <thead><tr><th>所属柜子</th><th>格口</th><th>绑定时间</th><th>最后使用时间</th><th>最后使用状态</th><th>累计使用次数</th></tr></thead>'
|
||||
+ ' <tbody><tr>'
|
||||
+ ' <td>' + locker.locker + '</td>'
|
||||
+ ' <td>' + locker.cell + '</td>'
|
||||
+ ' <td>' + locker.bindTime + '</td>'
|
||||
+ ' <td>' + locker.lastUseTime + '</td>'
|
||||
+ ' <td><span class="tag ' + statusTag + '">' + locker.lastUseStatus + '</span></td>'
|
||||
+ ' <td>' + locker.useCount.toLocaleString() + ' 次</td>'
|
||||
+ ' </tr></tbody>'
|
||||
+ '</table>';
|
||||
} else {
|
||||
lockerHtml = ''
|
||||
+ '<div style="padding:32px 0;text-align:center;">'
|
||||
+ ' <div style="font-size:32px;margin-bottom:8px;">🗄️</div>'
|
||||
+ ' <div style="font-size:var(--font-sm);color:var(--text-secondary);">暂未绑定专属餐盘柜,可在餐盘柜管理中为该会员分配格口</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
var html = ''
|
||||
+ '<div style="background:#FFFBF0;border:1px solid #FFE7BA;border-radius:var(--radius-md);padding:10px 14px;margin-bottom:16px;font-size:var(--font-sm);color:#AD6800;">'
|
||||
+ ' 💡 会员权益体系持续规划中,当前已上线权益:专属餐盘柜'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:8px;">当前等级权益(' + levelText + '会员)</div>'
|
||||
+ '<div style="font-size:var(--font-sm);color:var(--text-secondary);line-height:1.8;margin-bottom:20px;padding:12px 14px;background:#FAFAFA;border-radius:var(--radius-md);">'
|
||||
+ (vipLevelBenefitsMap[levelText] || vipLevelBenefitsMap['普通'])
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--font-sm);font-weight:600;color:var(--text-primary);margin-bottom:8px;">🍽️ 专属餐盘柜信息</div>'
|
||||
+ lockerHtml;
|
||||
|
||||
Modal.open('会员权益 — ' + name + '(' + cardNo + ')', html, '', '<button class="btn btn-default" onclick="Modal.close()">关闭</button>');
|
||||
}
|
||||
|
||||
/* ========== 编辑会员信息弹窗(Tab3) ========== */
|
||||
function openVipEditModal(row) {
|
||||
var tds = row.querySelectorAll('td');
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# 营养用餐管理端 CHANGELOG
|
||||
|
||||
## [2026-08-18] feat: 订餐终端设备概览大弹窗 + 会员权益体系 + 采购库存抵扣 + 追溯多锅次分支
|
||||
|
||||
### 变更一:订餐终端设备概览大弹窗(`din-terminal.html`)
|
||||
- 新增「设备概览」大弹窗(1100px 宽·Tab 结构),按设备类型展示对应概览内容
|
||||
- 智能货柜可视化:格口网格(空闲/存放/临期预警/异常四态)+ 图例 + 格口绑定会员展示
|
||||
- 新增格口历史记录弹窗(叠加层):存取历史汇总 + 明细列表,参考净菜进柜历史弹窗样式
|
||||
- 支持格口分配/解绑操作,异常停用格口不可操作
|
||||
|
||||
### 变更二:会员权益概览弹窗(`din-user-mgmt.html`)
|
||||
- 新增「会员权益」弹窗:展示会员权益概要与专属餐盘柜信息
|
||||
- 会员等级配置(如季度体检券、专属取餐窗口、专属餐盘柜、AI 营养周报等权益明细)
|
||||
- 未绑定餐盘柜显示空态,格口使用状态/累计使用次数展示
|
||||
|
||||
### 变更三:采购单库存抵扣交互(`farm-purchase.html`)
|
||||
- 待确认采购单新增库存列与「一键抵扣」:库存充足建议整行移除、部分库存可部分抵扣采购量
|
||||
- 抵扣提示动态化(检测到有库存的行批量应用),克重缺失/单价待补充时留空提示
|
||||
- 隐藏/移除抵扣行的按钮交互
|
||||
|
||||
### 变更四:餐品食材选择器可搜索下拉(`din-dish-own.html`)
|
||||
- 食材选择器改造为可搜索下拉面板:分类过滤·关键词过滤·去重·候选列表渲染
|
||||
- 展开时自动聚焦搜索框,未找到匹配时显示空态;关闭面板状态记忆便于重新展开
|
||||
|
||||
### 变更五:取餐记录重量偏差筛选与详情(`din-order-pickup.html`)
|
||||
- 筛选栏新增「重量偏差范围」(最小值/最大值,单位克)筛选,餐线秤记录不参与该筛选
|
||||
- 取餐详情展示结算重量·实际重量·人员类别(员工/会员)·取餐方式/类型等字段
|
||||
|
||||
### 变更六:异常结果告警详情(`ano-result.html`)
|
||||
- 异常详情新增设备告警信息:人脸识别累计失败/连续失败(阈值触发)·档口秤/餐线秤告警
|
||||
- 异常记录条数与触发告警次数展示
|
||||
|
||||
### 变更七:追溯时间轴多锅次分支(`trace-timeline-dish.html` / `trace-timeline-ingredient.html`)
|
||||
- 烹制环节支持多锅次分支展示(分支 A/B/C 多条 COOK 记录并行)
|
||||
- 时间轴节点补充净菜全流程(农残检测·净菜加工/组配/入库/进柜/内供·干货入库/存储)·上餐监控·余量处置
|
||||
- 节点详情展示操作人·时间·监控摄像头(如后厨-CAM10),未显式声明时不重复展示
|
||||
|
||||
## [2026-08-14] optimize: 餐品/食材档案检索与录入体系强化 + 进货明细金额补全
|
||||
|
||||
### 变更一:餐品库高级搜索与筛选汇总(`din-dish-all.html` / `din-dish-own.html`)
|
||||
|
||||
@@ -131,6 +131,8 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
.items-table input,.items-table select{height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 8px;font-size:var(--font-xs);outline:none;width:100%;}
|
||||
.items-table input:focus,.items-table select:focus{border-color:var(--primary);}
|
||||
.items-table .btn-remove{color:var(--danger);cursor:pointer;font-size:16px;border:none;background:none;padding:4px;}
|
||||
/* 库存抵扣列:仅在"立即确认"(待确认订单)入口显示,其余场景整列隐藏 */
|
||||
.items-table.hide-stock .stock-col{display:none;}
|
||||
/* 规格输入组(表格内):克重 input + 单位 select 连在一起 */
|
||||
.items-table .spec-input-group{display:flex;align-items:stretch;height:30px;}
|
||||
.items-table .spec-input-group input{flex:1;border-radius:4px 0 0 4px;border-right:none;width:auto;}
|
||||
@@ -309,13 +311,13 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<tr class="detail-row" id="detail-1">
|
||||
<td colspan="11">
|
||||
<table class="sub-table">
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>采购重量</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-XLHU-080K-20260416-001</td><td>西兰花</td><td>蔬菜类</td><td>80</td><td>500g/袋</td><td>¥4.5</td><td>¥360</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-QCAI-120K-20260416-002</td><td>芹菜</td><td>蔬菜类</td><td>120</td><td>500g/袋</td><td>¥2.5</td><td>¥300</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-XLHU-080K-20260416-001</td><td>西兰花</td><td>蔬菜类</td><td>80</td><td>500g/袋</td><td>40 kg</td><td>¥4.5</td><td>¥360</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-QCAI-120K-20260416-002</td><td>芹菜</td><td>蔬菜类</td><td>120</td><td>500g/袋</td><td>60 kg</td><td>¥2.5</td><td>¥300</td><td>—</td><td>—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer">合计:2 种食材,200 kg,¥660</div>
|
||||
<div class="sub-table__footer">合计:2 种食材,100 kg,¥660</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 采购单2:待发货 -->
|
||||
@@ -335,14 +337,14 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<tr class="detail-row" id="detail-2">
|
||||
<td colspan="11">
|
||||
<table class="sub-table">
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>采购重量</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-BOCA-050K-20260415-002</td><td>菠菜</td><td>蔬菜类</td><td>50</td><td>500g/袋</td><td>¥2.5</td><td>¥125</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-BCAI-080K-20260415-003</td><td>白菜</td><td>蔬菜类</td><td>80</td><td>500g/袋</td><td>¥1.8</td><td>¥144</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-HLUB-120K-20260415-004</td><td>胡萝卜</td><td>蔬菜类</td><td>120</td><td>500g/袋</td><td>¥1.8</td><td>¥216</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-BOCA-050K-20260415-002</td><td>菠菜</td><td>蔬菜类</td><td>50</td><td>500g/袋</td><td>25 kg</td><td>¥2.5</td><td>¥125</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-BCAI-080K-20260415-003</td><td>白菜</td><td>蔬菜类</td><td>80</td><td>500g/袋</td><td>40 kg</td><td>¥1.8</td><td>¥144</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-HLUB-120K-20260415-004</td><td>胡萝卜</td><td>蔬菜类</td><td>120</td><td>500g/袋</td><td>60 kg</td><td>¥1.8</td><td>¥216</td><td>—</td><td>—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer">合计:3 种食材,250 kg,¥485</div>
|
||||
<div class="sub-table__footer">合计:3 种食材,125 kg,¥485</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 采购单3:待收货 -->
|
||||
@@ -362,13 +364,13 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<tr class="detail-row" id="detail-3">
|
||||
<td colspan="11">
|
||||
<table class="sub-table">
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>采购重量</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-XR01-MT-ZLRJ-080K-20260415-005</td><td>猪里脊</td><td>肉禽类</td><td>80</td><td>500g/袋</td><td>¥28</td><td>¥2,240</td><td>—</td><td>需冷链运输</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-XR01-MT-ZWUA-240K-20260415-006</td><td>猪五花</td><td>肉禽类</td><td>240</td><td>500g/袋</td><td>¥26</td><td>¥6,240</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-XR01-MT-ZLRJ-080K-20260415-005</td><td>猪里脊</td><td>肉禽类</td><td>80</td><td>500g/袋</td><td>40 kg</td><td>¥28</td><td>¥2,240</td><td>—</td><td>需冷链运输</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-XR01-MT-ZWUA-240K-20260415-006</td><td>猪五花</td><td>肉禽类</td><td>240</td><td>500g/袋</td><td>120 kg</td><td>¥26</td><td>¥6,240</td><td>—</td><td>—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer">合计:2 种食材,320 kg,¥8,480</div>
|
||||
<div class="sub-table__footer">合计:2 种食材,160 kg,¥8,480</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 采购单4:已完成 -->
|
||||
@@ -388,14 +390,14 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<tr class="detail-row" id="detail-4">
|
||||
<td colspan="11">
|
||||
<table class="sub-table">
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>采购重量</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-HS01-SF-CAYO-060K-20260414-001</td><td>草鱼</td><td>水产类</td><td>60</td><td>500g/袋</td><td>¥18</td><td>¥1,080</td><td>60</td><td>活鲜运输</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-HS01-SF-JIYU-080K-20260414-002</td><td>鲫鱼</td><td>水产类</td><td>80</td><td>500g/袋</td><td>¥16</td><td>¥1,280</td><td>80</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-HS01-SF-XIAA-050K-20260414-003</td><td>虾</td><td>水产类</td><td>50</td><td>500g/袋</td><td>¥30</td><td>¥1,500</td><td>—</td><td>冰鲜</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-HS01-SF-CAYO-060K-20260414-001</td><td>草鱼</td><td>水产类</td><td>60</td><td>500g/袋</td><td>30 kg</td><td>¥18</td><td>¥1,080</td><td>60</td><td>活鲜运输</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-HS01-SF-JIYU-080K-20260414-002</td><td>鲫鱼</td><td>水产类</td><td>80</td><td>500g/袋</td><td>40 kg</td><td>¥16</td><td>¥1,280</td><td>80</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-HS01-SF-XIAA-050K-20260414-003</td><td>虾</td><td>水产类</td><td>50</td><td>500g/袋</td><td>25 kg</td><td>¥30</td><td>¥1,500</td><td>—</td><td>冰鲜</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer">合计:3 种食材,190 kg,¥3,860</div>
|
||||
<div class="sub-table__footer">合计:3 种食材,95 kg,¥3,860</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 采购单5:已取消 -->
|
||||
@@ -415,13 +417,13 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<tr class="detail-row" id="detail-5">
|
||||
<td colspan="11">
|
||||
<table class="sub-table">
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>采购重量</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY02-DG-DAMI-1000-20260413-001</td><td>大米</td><td>粮油类</td><td>1000</td><td>25kg/袋</td><td>¥3.5</td><td>¥3,500</td><td>—</td><td>东北五常大米</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY02-DG-HSYO-168K-20260413-002</td><td>花生油</td><td>粮油类</td><td>168</td><td>5kg/桶</td><td>¥16</td><td>¥2,688</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY02-DG-DAMI-1000-20260413-001</td><td>大米</td><td>粮油类</td><td>1000</td><td>25kg/袋</td><td>25000 kg</td><td>¥3.5</td><td>¥3,500</td><td>—</td><td>东北五常大米</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY02-DG-HSYO-168K-20260413-002</td><td>花生油</td><td>粮油类</td><td>168</td><td>5kg/桶</td><td>840 kg</td><td>¥16</td><td>¥2,688</td><td>—</td><td>—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer">合计:2 种食材,1168 kg,¥6,188</div>
|
||||
<div class="sub-table__footer">合计:2 种食材,25840 kg,¥6,188</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 采购单6:待确认(系统自动生成,缺供应商与单价) -->
|
||||
@@ -441,13 +443,13 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<tr class="detail-row" id="detail-6">
|
||||
<td colspan="11">
|
||||
<table class="sub-table">
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<thead><tr><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>采购重量</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-XLHU-080K-20260416-007</td><td>西兰花</td><td>蔬菜类</td><td>80</td><td>500g/袋</td><td><span style="color:var(--danger);">待补充</span></td><td>—</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-QCAI-120K-20260416-008</td><td>芹菜</td><td>蔬菜类</td><td>120</td><td>500g/袋</td><td><span style="color:var(--danger);">待补充</span></td><td>—</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-XLHU-080K-20260416-007</td><td>西兰花</td><td>蔬菜类</td><td>80</td><td>500g/袋</td><td>40 kg</td><td><span style="color:var(--danger);">待补充</span></td><td>—</td><td>—</td><td>—</td></tr>
|
||||
<tr><td style="font-size:10px;color:#999;">ST-EX-LY01-VG-QCAI-120K-20260416-008</td><td>芹菜</td><td>蔬菜类</td><td>120</td><td>500g/袋</td><td>60 kg</td><td><span style="color:var(--danger);">待补充</span></td><td>—</td><td>—</td><td>—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="sub-table__footer" style="color:var(--warning);">合计:2 种食材,200 kg,单价待补充</div>
|
||||
<div class="sub-table__footer" style="color:var(--warning);">合计:2 种食材,100 kg,单价待补充</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -477,7 +479,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<!-- 系统生成订单的补全提示条(仅"立即确认"入口显示) -->
|
||||
<div id="confirmHintBar" style="display:none;margin-bottom:16px;padding:12px 16px;background:#FFF7E6;border:1px solid #FFE58F;border-left:4px solid var(--warning);border-radius:var(--radius-sm);align-items:center;gap:8px;font-size:var(--font-sm);color:var(--text-primary);">
|
||||
<span style="font-size:16px;">⚠️</span>
|
||||
<span>该采购单由系统自动生成,请补全食材单价和供应商信息后提交</span>
|
||||
<span>该采购单由系统自动生成,请补全食材单价和供应商信息后提交<span id="confirmStockHint" style="color:var(--warning);"></span></span>
|
||||
</div>
|
||||
<!-- 基本信息 -->
|
||||
<div class="form-grid">
|
||||
@@ -494,16 +496,19 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<!-- 食材明细 -->
|
||||
<div style="margin-top:20px;display:flex;align-items:center;justify-content:space-between;">
|
||||
<span style="font-size:var(--font-sm);font-weight:600;color:var(--text-primary);">食材明细</span>
|
||||
<button class="btn btn-primary btn-sm" onclick="addItemRow()">+ 添加食材</button>
|
||||
<span style="display:flex;gap:8px;">
|
||||
<button class="btn btn-default btn-sm" id="btnDeductAll" style="display:none;color:var(--warning);border-color:var(--warning);" onclick="deductAllStock()">⚡ 一键按库存抵扣</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="addItemRow()">+ 添加食材</button>
|
||||
</span>
|
||||
</div>
|
||||
<table class="items-table" id="itemsTable">
|
||||
<table class="items-table hide-stock" id="itemsTable">
|
||||
<thead>
|
||||
<tr><th style="width:140px;">溯源码</th><th style="width:110px;">食材名称</th><th style="width:80px;">食材分类</th><th style="width:80px;">采购数量</th><th style="width:150px;">包装规格</th><th style="width:80px;">单价(元)</th><th style="width:70px;">小计金额</th><th>备注</th><th style="width:36px;"></th></tr>
|
||||
<tr><th style="width:140px;">溯源码</th><th style="width:110px;">食材名称</th><th style="width:80px;">食材分类</th><th style="width:80px;">采购数量</th><th style="width:150px;">包装规格</th><th style="width:80px;">采购重量</th><th class="stock-col" style="width:120px;">当前库存</th><th style="width:80px;">单价(元)</th><th style="width:70px;">小计金额</th><th>备注</th><th style="width:36px;"></th></tr>
|
||||
</thead>
|
||||
<tbody id="itemsBody"></tbody>
|
||||
</table>
|
||||
<div class="items-summary">
|
||||
<span style="font-size:var(--font-sm);color:var(--text-secondary);">共 <strong id="itemCount">0</strong> 种食材</span>
|
||||
<span style="font-size:var(--font-sm);color:var(--text-secondary);">共 <strong id="itemCount">0</strong> 种食材<span id="stockDeductInfo" style="color:var(--warning);margin-left:8px;"></span></span>
|
||||
<span class="items-summary__total">采购总金额:<span style="color:var(--primary);font-size:var(--font-lg);" id="totalAmount">¥0</span></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -535,7 +540,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
|
||||
<div class="drawer-section-title">食材明细</div>
|
||||
<div style="padding:12px 24px 20px;">
|
||||
<table class="sub-table" id="drawerItemsTable">
|
||||
<thead><tr><th>序号</th><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<thead><tr><th>序号</th><th>溯源码</th><th>食材名称</th><th>食材分类</th><th>采购数量</th><th>包装规格</th><th>采购重量</th><th>单价</th><th>小计金额</th><th>验收数量</th><th>备注</th></tr></thead>
|
||||
<tbody id="drawerItemsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -697,6 +702,62 @@ function parseSpecText(specText){
|
||||
}
|
||||
return {gram:'',unit:''};
|
||||
}
|
||||
/* ===== 食材当前库存模拟数据(单位 kg,用于待确认采购单的库存抵扣提醒) ===== */
|
||||
var ingredientStock = {'西兰花':1, '芹菜':0.5};
|
||||
|
||||
/* 构建库存抵扣单元格内容:库存充足→建议移除;部分库存→可一键抵扣;无库存→灰字 */
|
||||
function buildStockCellHtml(name, qty, gram){
|
||||
var stock = ingredientStock[name] || 0;
|
||||
var demandKg = (qty && gram) ? qty * gram / 1000 : 0;
|
||||
if (stock <= 0) {
|
||||
return '<span style="color:var(--text-placeholder);">0 kg</span>';
|
||||
}
|
||||
if (demandKg > 0 && stock >= demandKg) {
|
||||
return '<span style="color:var(--success);font-weight:500;">库存充足</span> <a class="link" style="color:var(--danger);" onclick="removeItemRow(this)">移除</a>';
|
||||
}
|
||||
return '<span style="font-weight:500;">' + stock + ' kg</span>'
|
||||
+ '<div style="font-size:10px;color:var(--text-placeholder);">需求 ' + demandKg + ' kg</div>'
|
||||
+ '<a class="link" style="color:var(--warning);" onclick="deductStock(this)">按库存抵扣</a>';
|
||||
}
|
||||
/* 单行按库存抵扣:新数量 = ⌈(需求重量 − 库存) ÷ 单位克重⌉,不足一袋则整行移除 */
|
||||
function applyDeduct(tr){
|
||||
var qtyEl = tr.querySelector('.item-qty');
|
||||
var gramEl = tr.querySelector('.item-gram');
|
||||
var qty = parseFloat(qtyEl.value) || 0;
|
||||
var gram = parseFloat(gramEl.value) || 0;
|
||||
var name = tr.querySelector('select').value;
|
||||
var stock = ingredientStock[name] || 0;
|
||||
if (!qty || !gram || stock <= 0) return;
|
||||
if (!tr.dataset.origQty) tr.dataset.origQty = qty;
|
||||
var demandKg = qty * gram / 1000;
|
||||
var newQty = Math.ceil((demandKg - stock) * 1000 / gram);
|
||||
if (newQty <= 0) {
|
||||
tr.remove();
|
||||
recalcTotal();
|
||||
return;
|
||||
}
|
||||
qtyEl.value = newQty;
|
||||
calcRow(qtyEl);
|
||||
var stockEl = tr.querySelector('.item-stock');
|
||||
if (stockEl) stockEl.innerHTML = '<span style="color:var(--warning);">已抵扣 ' + stock + ' kg</span>';
|
||||
var note = document.createElement('div');
|
||||
note.style.cssText = 'font-size:10px;color:var(--warning);margin-top:2px;';
|
||||
note.textContent = '已由 ' + tr.dataset.origQty + ' 抵扣调整为 ' + newQty;
|
||||
qtyEl.parentElement.appendChild(note);
|
||||
}
|
||||
function deductStock(el){ applyDeduct(el.closest('tr')); }
|
||||
/* 一键按库存抵扣:对所有有库存的行批量应用 */
|
||||
function deductAllStock(){
|
||||
var rows = Array.prototype.slice.call(document.getElementById('itemsBody').rows);
|
||||
rows.forEach(applyDeduct);
|
||||
recalcTotal();
|
||||
}
|
||||
/* 计算采购重量:采购数量 × 包装规格克重,统一换算为 kg 展示 */
|
||||
function calcPurchaseWeight(qty, specText){
|
||||
var p = parseSpecText(specText);
|
||||
if(!qty || !p.gram) return '—';
|
||||
return (qty * p.gram / 1000).toLocaleString() + ' kg';
|
||||
}
|
||||
|
||||
/* ===== 模拟数据 ===== */
|
||||
var orders = [
|
||||
@@ -797,6 +858,9 @@ function openCreateModal() {
|
||||
document.getElementById('contactInfoBar').style.display = 'none';
|
||||
document.getElementById('confirmHintBar').style.display = 'none';
|
||||
setSubmitBtnText('提交采购单');
|
||||
document.getElementById('itemsTable').classList.add('hide-stock');
|
||||
document.getElementById('btnDeductAll').style.display = 'none';
|
||||
document.getElementById('confirmStockHint').textContent = '';
|
||||
document.getElementById('itemsBody').innerHTML = '';
|
||||
addItemRow();
|
||||
recalcTotal();
|
||||
@@ -813,12 +877,19 @@ function openEditModal(idx) {
|
||||
showSupplierContact();
|
||||
document.getElementById('confirmHintBar').style.display = 'none';
|
||||
setSubmitBtnText('提交采购单');
|
||||
/* 普通新建/编辑模式:隐藏库存抵扣列与一键抵扣按钮 */
|
||||
document.getElementById('itemsTable').classList.add('hide-stock');
|
||||
document.getElementById('btnDeductAll').style.display = 'none';
|
||||
document.getElementById('confirmStockHint').textContent = '';
|
||||
var tbody = document.getElementById('itemsBody');
|
||||
tbody.innerHTML = '';
|
||||
o.items.forEach(function(it) {
|
||||
var specParts = parseSpecText(it.spec);
|
||||
/* 待确认订单:单价为 null 时空值回填 + 橙色边框提醒 */
|
||||
var priceVal = (it.price === null || it.price === undefined) ? '' : it.price;
|
||||
/* 采购重量回填:数量 × 克重换算为 kg,克重缺失时留空 */
|
||||
var specP = parseSpecText(it.spec);
|
||||
var weightVal = (it.qty && specP.gram) ? (it.qty * specP.gram / 1000) : '';
|
||||
var subtotalText = (it.subtotal === '—' || !it.subtotal) ? '¥0' : it.subtotal;
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td><input type="text" value="'+it.trace+'" placeholder="请输入溯源码(选填)"></td>'
|
||||
@@ -826,6 +897,8 @@ function openEditModal(idx) {
|
||||
+'<td><select><option'+(it.cat==='蔬菜类'?' selected':'')+'>蔬菜类</option><option'+(it.cat==='肉禽类'?' selected':'')+'>肉禽类</option><option'+(it.cat==='水产类'?' selected':'')+'>水产类</option><option'+(it.cat==='粮油类'?' selected':'')+'>粮油类</option><option>调味品</option><option>豆制品</option></select></td>'
|
||||
+'<td><input type="number" class="item-qty" value="'+it.qty+'" onchange="calcRow(this)"></td>'
|
||||
+'<td><div class="spec-input-group"><input type="number" class="item-gram" value="'+specParts.gram+'" placeholder="克重" onchange="calcRow(this)"><select class="item-unit" onchange="calcRow(this)">'+SPEC_UNIT_OPTIONS.map(function(u){return '<option'+(u===specParts.unit?' selected':'')+'>'+u+'</option>';}).join('')+'</select></div></td>'
|
||||
+'<td><input type="number" class="item-weight-input" value="'+weightVal+'" placeholder="0" step="0.1" onchange="calcFromWeight(this)"></td>'
|
||||
+'<td class="stock-col item-stock" style="font-size:var(--font-xs);">'+buildStockCellHtml(it.name, it.qty, specParts.gram)+'</td>'
|
||||
+'<td><input type="number" class="item-price" value="'+priceVal+'" placeholder="0" onchange="calcRow(this)" style="border-color:var(--warning);"></td>'
|
||||
+'<td class="item-subtotal" style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">'+subtotalText+'</td>'
|
||||
+'<td><input type="text" value="'+(it.remark==='—'?'':it.remark)+'" placeholder="选填"></td>'
|
||||
@@ -847,6 +920,8 @@ function addItemRow() {
|
||||
+'<td><select><option>蔬菜类</option><option>肉禽类</option><option>水产类</option><option>粮油类</option><option>调味品</option><option>豆制品</option></select></td>'
|
||||
+'<td><input type="number" class="item-qty" placeholder="0" onchange="calcRow(this)"></td>'
|
||||
+'<td><div class="spec-input-group"><input type="number" class="item-gram" placeholder="克重" onchange="calcRow(this)"><select class="item-unit" onchange="calcRow(this)">'+SPEC_UNIT_OPTIONS.map(function(u){return '<option>'+u+'</option>';}).join('')+'</select></div></td>'
|
||||
+'<td><input type="number" class="item-weight-input" placeholder="0" step="0.1" onchange="calcFromWeight(this)"></td>'
|
||||
+'<td class="stock-col item-stock" style="font-size:var(--font-xs);"><span style="color:var(--text-placeholder);">—</span></td>'
|
||||
+'<td><input type="number" class="item-price" placeholder="0" onchange="calcRow(this)"></td>'
|
||||
+'<td class="item-subtotal" style="font-size:var(--font-xs);color:var(--text-primary);font-weight:500;">¥0</td>'
|
||||
+'<td><input type="text" placeholder="选填"></td>'
|
||||
@@ -868,8 +943,22 @@ function calcRow(el) {
|
||||
var price = parseFloat(tr.querySelector('.item-price').value) || 0;
|
||||
var sub = qty * price;
|
||||
tr.querySelector('.item-subtotal').textContent = '¥' + sub.toLocaleString();
|
||||
/* 同步刷新采购重量:采购数量 × 包装规格克重 */
|
||||
var gram = parseFloat(tr.querySelector('.item-gram').value) || 0;
|
||||
var weightEl = tr.querySelector('.item-weight-input');
|
||||
if (weightEl) weightEl.value = (qty && gram) ? Math.round(qty * gram / 100) / 10 : '';
|
||||
recalcTotal();
|
||||
}
|
||||
/* 反向联动:手动输入采购重量(kg) → 换算采购数量 = 重量×1000÷克重,四舍五入取整袋 */
|
||||
function calcFromWeight(el) {
|
||||
var tr = el.closest('tr');
|
||||
var weight = parseFloat(el.value) || 0;
|
||||
var gram = parseFloat(tr.querySelector('.item-gram').value) || 0;
|
||||
if (weight > 0 && gram > 0) {
|
||||
tr.querySelector('.item-qty').value = Math.round(weight * 1000 / gram);
|
||||
}
|
||||
calcRow(el);
|
||||
}
|
||||
/* 汇总总金额 */
|
||||
function recalcTotal() {
|
||||
var rows = document.getElementById('itemsBody').rows;
|
||||
@@ -881,6 +970,15 @@ function recalcTotal() {
|
||||
}
|
||||
document.getElementById('itemCount').textContent = rows.length;
|
||||
document.getElementById('totalAmount').textContent = '¥' + total.toLocaleString();
|
||||
/* 汇总已抵扣重量:Σ(原始数量 − 当前数量) × 单位克重 */
|
||||
var deductKg = 0;
|
||||
for (var j = 0; j < rows.length; j++) {
|
||||
if (!rows[j].dataset.origQty) continue;
|
||||
var g = parseFloat(rows[j].querySelector('.item-gram').value) || 0;
|
||||
var curQty = parseFloat(rows[j].querySelector('.item-qty').value) || 0;
|
||||
deductKg += (parseFloat(rows[j].dataset.origQty) - curQty) * g / 1000;
|
||||
}
|
||||
document.getElementById('stockDeductInfo').textContent = deductKg > 0 ? '已抵扣 ' + deductKg + ' kg' : '';
|
||||
}
|
||||
|
||||
/* ===== 详情抽屉 ===== */
|
||||
@@ -924,7 +1022,7 @@ function showDetail(idx) {
|
||||
var priceHtml = (it.price === null || it.price === '' || it.price === undefined)
|
||||
? '<span style="color:var(--danger);">待补充</span>'
|
||||
: '¥'+it.price;
|
||||
tr.innerHTML = '<td>'+(i+1)+'</td><td style="font-size:10px;color:#999;">'+it.trace+'</td><td>'+it.name+'</td><td>'+it.cat+'</td><td>'+it.qty+'</td><td>'+it.spec+'</td><td>'+priceHtml+'</td><td>'+(it.subtotal||'—')+'</td><td>'+(it.acceptedQty||'—')+'</td><td>'+it.remark+'</td>';
|
||||
tr.innerHTML = '<td>'+(i+1)+'</td><td style="font-size:10px;color:#999;">'+it.trace+'</td><td>'+it.name+'</td><td>'+it.cat+'</td><td>'+it.qty+'</td><td>'+it.spec+'</td><td>'+calcPurchaseWeight(it.qty, it.spec)+'</td><td>'+priceHtml+'</td><td>'+(it.subtotal||'—')+'</td><td>'+(it.acceptedQty||'—')+'</td><td>'+it.remark+'</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
/* 配送信息(待收货/已完成显示) */
|
||||
@@ -986,6 +1084,14 @@ function confirmOrder(idx) {
|
||||
openEditModal(idx);
|
||||
document.getElementById('confirmHintBar').style.display = 'flex';
|
||||
setSubmitBtnText('确认并提交采购单');
|
||||
/* 库存抵扣:检测有库存的食材数量,动态提示并展示库存列/一键抵扣按钮 */
|
||||
var o = orders[idx];
|
||||
var stockCount = o.items.filter(function(it){ return (ingredientStock[it.name] || 0) > 0; }).length;
|
||||
document.getElementById('confirmStockHint').textContent = stockCount > 0
|
||||
? ';检测到 ' + stockCount + ' 种食材有可用库存,可抵扣部分采购量'
|
||||
: '';
|
||||
document.getElementById('itemsTable').classList.remove('hide-stock');
|
||||
document.getElementById('btnDeductAll').style.display = stockCount > 0 ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
/* ===== 取消订单弹窗 ===== */
|
||||
|
||||
@@ -243,7 +243,7 @@ var INGREDIENTS = [
|
||||
{stage:'净菜内供',time:'2026-04-22 09:30',title:'净菜领用出库(3条分支)',trace:'NC-OUT-20260422-001',detail:'分支A:切丁5kg → 后厨A组<br>分支B:切片3kg → 后厨B组<br>分支C:切条2kg → 后厨C组',badge:'x3',status:'ok',video:['净菜内供','2026-04-22 09:30','出库口-CAM08','00:02:00']},
|
||||
{stage:'净菜进柜',time:'2026-04-22 09:45',title:'后厨冷柜暂存(2条分支)',trace:'KC-IN-20260422-001',detail:'分支A:切丁5kg → 后厨冷柜KC-01<br>分支B:切片3kg → 后厨冷柜KC-02',badge:'x2',status:'ok',video:['净菜进柜','2026-04-22 09:45','后厨柜-CAM08','00:01:20']},
|
||||
{stage:'净菜组配',time:'2026-04-22 10:20',title:'菜品组配(2条分支)',trace:'CMB-20260422-001',detail:'分支A:CMB-001 宫保鸡丁 5.0kg · 李组配<br>分支B:CMB-006 辣子鸡 3.5kg · 张组配',badge:'x2',status:'ok',video:['净菜组配','2026-04-22 10:20','组配间-CAM09','00:02:40']},
|
||||
{stage:'按单烹制',time:'2026-04-22 11:00 ~ 11:35',title:'烹制任务 · 多锅次烹制(3条分支)',trace:'COOK-20260422-001',detail:'分支A:COOK-001(宫保鸡丁)张厨师 2锅<br>分支B:COOK-009(辣子鸡)王厨师 1锅<br>分支C:COOK-015(青椒鸡丁)李厨师 1锅',badge:'x3',status:'ok',video:['按单烹制','2026-04-22 11:00','后厨-CAM10','00:04:20']},
|
||||
{stage:'按单烹制',time:'2026-04-22 11:00 ~ 11:35',operator:'张厨师 等3人',title:'烹制任务 · 多锅次烹制(3条分支)',trace:'COOK-20260422-001',detail:'分支A:COOK-001(宫保鸡丁)张厨师 2锅<br>分支B:COOK-009(辣子鸡)王厨师 1锅<br>分支C:COOK-015(青椒鸡丁)李厨师 1锅',badge:'x3',status:'ok',video:['按单烹制','2026-04-22 11:00','后厨-CAM10','00:04:20']},
|
||||
{stage:'上餐监控',time:'2026-04-22 11:30 ~ 12:00',title:'上餐批次 · 多餐线分发(3条分支)',trace:'SERVE-20260422-001',detail:'分支A:SERVE-001 → A区中餐线 6.8kg<br>分支B:SERVE-007 → B区中餐线 1.4kg<br>分支C:SERVE-015 → C区中餐线 2.1kg',badge:'x3',status:'ok',video:['上餐监控','2026-04-22 11:30','餐线-CAM11','00:03:05']},
|
||||
{stage:'余量处置',time:'2026-04-22 13:00 ~ 13:20',title:'余量处置 · 多餐线余量登记(3条分支)',trace:'DISP-20260422-001',detail:'分支A:余量0.3kg → 冷藏留用<br>分支B:余量0.1kg → 废弃销毁<br>分支C:余量0.2kg → 废弃销毁<br>经办人:李处置',badge:'x3',status:'ok',video:['余量处置','2026-04-22 13:00','余量区-CAM12','00:01:30']},
|
||||
{stage:'营养检验',time:'2026-04-22 13:30',title:'营养检测 · 成品抽检',trace:'NUT-20260422-001',detail:'COOK-001(宫保鸡丁)检测单号:NUT-20260422-001<br>热量:186kcal/100g · 蛋白质:18.2g · 脂肪:9.4g · 碳水:8.1g<br>检测结论:<strong style="color:#52c41a">达标</strong>',badge:'',status:'ok',video:['营养检验','2026-04-22 13:30','检测室-CAM13','00:02:15']},
|
||||
@@ -290,6 +290,15 @@ var INGREDIENTS = [
|
||||
|
||||
var INGREDIENT_EMOJIS = ['🥩','🥜','🌶️','🧄'];
|
||||
|
||||
/* 环节默认操作人映射(节点未显式声明 operator 时使用) */
|
||||
var STAGE_OPERATORS = {
|
||||
'采供入库':'赵采购','卫生检测':'陈检测','毛菜入库':'王仓管','毛菜存储':'系统监控',
|
||||
'净菜加工':'刘加工','净菜入库':'王仓管','净菜内供':'王仓管','净菜进柜':'王仓管',
|
||||
'净菜组配':'李组配','按单烹制':'张厨师','上餐监控':'系统记录','余量处置':'李处置',
|
||||
'营养检验':'周营养师','用户取餐':'系统记录','种植采收':'孙农技','农残检测':'陈检测',
|
||||
'干货入库':'王仓管','干货存储':'系统监控','预处理':'刘加工'
|
||||
};
|
||||
|
||||
/* 当前状态 */
|
||||
var currentIngIdx = 0;
|
||||
var currentNodeIdx = 0;
|
||||
@@ -321,13 +330,16 @@ function renderNodeDetail(node) {
|
||||
var iconBg = NODE_ICON_BG[stage] || '#F5F5F5';
|
||||
var iconColor = NODE_COLORS[stage] || '#999';
|
||||
var icon = NODE_ICONS[stage] || '●';
|
||||
var operator = node.operator || STAGE_OPERATORS[stage] || '—';
|
||||
/* 详情中已含操作人(经办人/采收人/厨师等叫法)时不重复展示 */
|
||||
var detailHtml = node.detail.indexOf(operator.split(' ')[0]) === -1 ? '操作人:' + operator + '<br>' + node.detail : node.detail;
|
||||
var html = '<div class="node-card">';
|
||||
html += '<div class="node-card__icon" style="background:' + iconBg + ';color:' + iconColor + ';">' + icon + '</div>';
|
||||
html += '<div class="node-card__body">';
|
||||
html += '<div class="node-card__header"><span class="node-card__stage">' + icon + ' ' + stage + '</span><span class="node-card__time">' + node.time + '</span></div>';
|
||||
html += '<div class="node-card__header"><span class="node-card__stage">' + icon + ' ' + stage + '</span></div>';
|
||||
html += '<div class="node-card__title">' + node.title + '</div>';
|
||||
html += '<div class="node-card__trace">溯源码:' + node.trace + '</div>';
|
||||
html += '<div class="node-card__detail">' + node.detail + '</div>';
|
||||
html += '<div class="node-card__trace">时间:' + node.time + ' · 溯源码:' + node.trace + '</div>';
|
||||
html += '<div class="node-card__detail">' + detailHtml + '</div>';
|
||||
if (node.video) {
|
||||
html += '<div class="node-card__video" onclick="openVideoModal(\'' + node.video[0] + '\',\'' + node.video[1] + '\',\'' + node.video[2] + '\',\'' + node.video[3] + '\')">🎬 查看视频</div>';
|
||||
}
|
||||
|
||||
@@ -218,13 +218,22 @@ var NODES = [
|
||||
{stage:'净菜内供',time:'2026-04-22 09:30',title:'净菜领用出库(3条分支)',trace:'NC-OUT-20260422-001',detail:'分支A:切丁5kg → 后厨A组<br>分支B:切片3kg → 后厨B组<br>分支C:切条2kg → 后厨C组',badge:'x3',status:'ok',video:['净菜内供','2026-04-22 09:30','出库口-CAM08','00:02:00']},
|
||||
{stage:'净菜进柜',time:'2026-04-22 09:45',title:'后厨冷柜暂存(2条分支)',trace:'KC-IN-20260422-001',detail:'分支A:切丁5kg → 后厨冷柜KC-01<br>分支B:切片3kg → 后厨冷柜KC-02',badge:'x2',status:'ok',video:['净菜进柜','2026-04-22 09:45','后厨柜-CAM08','00:01:20']},
|
||||
{stage:'净菜组配',time:'2026-04-22 10:20',title:'菜品组配(2条分支)',trace:'CMB-20260422-001',detail:'分支A:CMB-001 宫保鸡丁 5.0kg · 李组配<br>分支B:CMB-006 辣子鸡 3.5kg · 张组配',badge:'x2',status:'ok',video:['净菜组配','2026-04-22 10:20','组配间-CAM09','00:02:40']},
|
||||
{stage:'按单烹制',time:'2026-04-22 11:00 ~ 11:35',title:'烹制任务 · 多锅次烹制(3条分支)',trace:'COOK-20260422-001',detail:'分支A:COOK-001(宫保鸡丁)张厨师 2锅<br>分支B:COOK-009(辣子鸡)王厨师 1锅<br>分支C:COOK-015(青椒鸡丁)李厨师 1锅',badge:'x3',status:'ok',video:['按单烹制','2026-04-22 11:00','后厨-CAM10','00:04:20']},
|
||||
{stage:'按单烹制',time:'2026-04-22 11:00 ~ 11:35',operator:'张厨师 等3人',title:'烹制任务 · 多锅次烹制(3条分支)',trace:'COOK-20260422-001',detail:'分支A:COOK-001(宫保鸡丁)张厨师 2锅<br>分支B:COOK-009(辣子鸡)王厨师 1锅<br>分支C:COOK-015(青椒鸡丁)李厨师 1锅',badge:'x3',status:'ok',video:['按单烹制','2026-04-22 11:00','后厨-CAM10','00:04:20']},
|
||||
{stage:'上餐监控',time:'2026-04-22 11:30 ~ 12:00',title:'上餐批次 · 多餐线分发(3条分支)',trace:'SERVE-20260422-001',detail:'分支A:SERVE-001 → A区中餐线 6.8kg<br>分支B:SERVE-007 → B区中餐线 1.4kg<br>分支C:SERVE-015 → C区中餐线 2.1kg',badge:'x3',status:'ok',video:['上餐监控','2026-04-22 11:30','餐线-CAM11','00:03:05']},
|
||||
{stage:'余量处置',time:'2026-04-22 13:00 ~ 13:20',title:'余量处置 · 多餐线余量登记(3条分支)',trace:'DISP-20260422-001',detail:'分支A:余量0.3kg → 冷藏留用<br>分支B:余量0.1kg → 废弃销毁<br>分支C:余量0.2kg → 废弃销毁<br>经办人:李处置',badge:'x3',status:'ok',video:['余量处置','2026-04-22 13:00','余量区-CAM12','00:01:30']},
|
||||
{stage:'营养检验',time:'2026-04-22 13:30',title:'营养检测 · 成品抽检',trace:'NUT-20260422-001',detail:'COOK-001(宫保鸡丁)检测单号:NUT-20260422-001<br>热量:186kcal/100g · 蛋白质:18.2g · 脂肪:9.4g · 碳水:8.1g<br>检测结论:<strong style="color:#52c41a">达标</strong>',badge:'',status:'ok',video:['营养检验','2026-04-22 13:30','检测室-CAM13','00:02:15']},
|
||||
{stage:'用户取餐',time:'2026-04-22 11:40 ~ 12:30',title:'关联订单食材映射',trace:'ORD-20260422-021',detail:'关联订单:SO20260422021#1、SO20260422087#2、SO20260422103#1<br>用于召回时可直接反查受影响订单范围',badge:'',status:'ok',video:['用户取餐','2026-04-22 11:40','取餐口-CAM14','00:01:50']}
|
||||
];
|
||||
|
||||
/* 环节默认操作人映射(节点未显式声明 operator 时使用) */
|
||||
var STAGE_OPERATORS = {
|
||||
'采供入库':'赵采购','卫生检测':'陈检测','毛菜入库':'王仓管','毛菜存储':'系统监控',
|
||||
'净菜加工':'刘加工','净菜入库':'王仓管','净菜内供':'王仓管','净菜进柜':'王仓管',
|
||||
'净菜组配':'李组配','按单烹制':'张厨师','上餐监控':'系统记录','余量处置':'李处置',
|
||||
'营养检验':'周营养师','用户取餐':'系统记录','种植采收':'孙农技','农残检测':'陈检测',
|
||||
'干货入库':'王仓管','干货存储':'系统监控','预处理':'刘加工'
|
||||
};
|
||||
|
||||
/* 当前节点索引 */
|
||||
var currentNodeIdx = 0;
|
||||
|
||||
@@ -254,13 +263,16 @@ function renderNodeDetail(node) {
|
||||
var iconBg = NODE_ICON_BG[stage] || '#F5F5F5';
|
||||
var iconColor = NODE_COLORS[stage] || '#999';
|
||||
var icon = NODE_ICONS[stage] || '●';
|
||||
var operator = node.operator || STAGE_OPERATORS[stage] || '—';
|
||||
/* 详情中已含操作人(经办人/采收人/厨师等叫法)时不重复展示 */
|
||||
var detailHtml = node.detail.indexOf(operator.split(' ')[0]) === -1 ? '操作人:' + operator + '<br>' + node.detail : node.detail;
|
||||
var html = '<div class="node-card">';
|
||||
html += '<div class="node-card__icon" style="background:' + iconBg + ';color:' + iconColor + ';">' + icon + '</div>';
|
||||
html += '<div class="node-card__body">';
|
||||
html += '<div class="node-card__header"><span class="node-card__stage">' + icon + ' ' + stage + '</span><span class="node-card__time">' + node.time + '</span></div>';
|
||||
html += '<div class="node-card__header"><span class="node-card__stage">' + icon + ' ' + stage + '</span></div>';
|
||||
html += '<div class="node-card__title">' + node.title + '</div>';
|
||||
html += '<div class="node-card__trace">溯源码:' + node.trace + '</div>';
|
||||
html += '<div class="node-card__detail">' + node.detail + '</div>';
|
||||
html += '<div class="node-card__trace">时间:' + node.time + ' · 溯源码:' + node.trace + '</div>';
|
||||
html += '<div class="node-card__detail">' + detailHtml + '</div>';
|
||||
if (node.video) {
|
||||
html += '<div class="node-card__video" onclick="openVideoModal(\'' + node.video[0] + '\',\'' + node.video[1] + '\',\'' + node.video[2] + '\',\'' + node.video[3] + '\')">🎬 查看视频</div>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user