feat: 毛菜验收秤PAD外壳改造 + 后厨补餐屏首版设计 + 配送H5流程降噪 + 营养档案检索录入强化

- 毛菜验收秤全页面 PAD 居中外壳(grid方案) + 验收列表动态列(有订单10列/无订单8列) + 返回导航referer统一
- 加工提示屏更名后厨补餐屏并完成首版设计(16格菜品余量监控四态看板 + 补餐/食谱来源标签)
- 配送H5: 移除派单倒计时与异常按钮; 配送中页新增商家联系条/备注展示/清单卡片化
- 营养管理: 餐品库高级搜索+筛选汇总条+操作历史; 食材录入改行内选择器+净菜包毛利率; 食材详情操作历史; 进货明细补金额单价

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-14 19:09:53 +08:00
co-authored by Claude Opus 4.7
parent 44e7f322ff
commit b4dfe31b25
25 changed files with 2930 additions and 691 deletions
+307 -143
View File
@@ -141,6 +141,36 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.ingredient-table{width:100%;border-collapse:collapse;font-size:var(--font-sm);margin-top:12px;}
.ingredient-table th{background:#FAFAFA;padding:10px 12px;text-align:left;color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);}
.ingredient-table td{padding:10px 12px;border-bottom:1px solid #F0F0F0;color:var(--text-primary);}
/* 餐品构成 - 双栏布局:左清单表格 + 右食材选择列表 */
.ingredient-picker{display:flex;gap:16px;align-items:stretch;}
.ingredient-picker__left{flex:1;min-width:0;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;}
.ingredient-picker__left .ingredient-table{margin-top:0;}
.ingredient-picker__left .ingredient-table th,.ingredient-picker__left .ingredient-table td{font-size:var(--font-xs);}
.ingredient-picker__left .ingredient-table th{padding:8px 10px;}
.ingredient-picker__left .ingredient-table td{padding:6px 10px;}
.ingredient-picker__right{width:280px;flex-shrink:0;background:#FAFBFC;border:1px solid var(--border);border-radius:var(--radius-sm);display:flex;flex-direction:column;max-height:360px;}
.ingredient-picker__search{padding:10px;border-bottom:1px solid #F0F0F0;}
.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-xs);outline:none;background:#fff;}
.ingredient-picker__search input:focus{border-color:var(--primary);}
.ingredient-picker__list{flex:1;overflow-y:auto;padding:6px 0;}
.ingredient-picker__item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;transition:background .15s;border-left:2px solid transparent;}
.ingredient-picker__item:hover{background:var(--primary-light);}
.ingredient-picker__item.is-added{cursor:default;background:#F5F5F5;opacity:0.65;}
.ingredient-picker__item.is-added:hover{background:#F5F5F5;}
.ingredient-picker__code{font-size:var(--font-xs);color:var(--text-placeholder);font-family:Menlo,Consolas,monospace;flex-shrink:0;min-width:54px;}
.ingredient-picker__name{font-size:var(--font-sm);color:var(--text-primary);flex:1;min-width:0;}
.ingredient-picker__badge{font-size:var(--font-xs);color:var(--text-placeholder);background:#E8E8E8;border-radius:8px;padding:1px 6px;flex-shrink:0;}
.ingredient-picker__empty{padding:24px 12px;font-size:var(--font-xs);color:var(--text-placeholder);text-align:center;}
/* 食材清单行内表单(净菜下拉 + 规格下拉) */
.ingredient-table .td-code{font-family:Menlo,Consolas,monospace;font-size:var(--font-xs);color:var(--text-secondary);}
.ingredient-table .td-net{min-width:180px;}
.ingredient-table .td-net .td-net__inner{display:flex;align-items:center;gap:6px;}
.ingredient-table .td-net .ingredient-net-select{flex:1;min-width:0;height:28px;font-size:var(--font-xs);}
.ingredient-table .td-net .ingredient-spec-select{flex:0 0 76px;height:28px;font-size:var(--font-xs);}
.ingredient-table .td-amount input{width:80px;height:28px;font-size:var(--font-xs);}
.ingredient-table .form-select--sm{height:28px;font-size:var(--font-xs);padding:0 8px;background-position:right 6px center;}
.ingredient-table .form-input--sm{height:28px;font-size:var(--font-xs);padding:0 8px;}
.ingredient-table .td-actions{white-space:nowrap;}
/* 弹窗 */
.dialog-mask{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.45);z-index:200;align-items:center;justify-content:center;}
.dialog-mask.open{display:flex;}
@@ -236,9 +266,15 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.cost-detail-block__table th,.cost-detail-block__table td{padding:8px 12px;font-size:var(--font-xs);text-align:left;border-bottom:1px solid #F0F0F0;}
.cost-detail-block__table th{color:var(--text-secondary);font-weight:500;background:#FAFBFC;}
.cost-detail-block__table tr:last-child td{border-bottom:none;}
.cost-detail-block__table th:nth-child(2),.cost-detail-block__table th:nth-child(3),.cost-detail-block__table th:nth-child(4),
.cost-detail-block__table td:nth-child(2),.cost-detail-block__table td:nth-child(4){text-align:right;}
.cost-detail-block__table td:nth-child(3){text-align:right;padding:6px 12px;}
/* 列对齐:用量(3)/单价(4)/小计(7)右对齐,价格更新方式(6)居中 */
.cost-detail-block__table th:nth-child(3),.cost-detail-block__table th:nth-child(4),.cost-detail-block__table th:nth-child(7),
.cost-detail-block__table td:nth-child(3),.cost-detail-block__table td:nth-child(7){text-align:right;}
.cost-detail-block__table th:nth-child(6),.cost-detail-block__table td:nth-child(6){text-align:center;}
.cost-detail-block__table td:nth-child(4){text-align:right;padding:6px 12px;}
/* 价格更新方式胶囊(只读) */
.cost-price-mode{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:var(--font-xs);line-height:1.4;}
.cost-price-mode--manual{background:#F5F5F5;color:#666;}
.cost-price-mode--accept{background:#F6FFED;color:#52C41A;}
.cost-detail__unit-input{width:90px;height:28px;padding:2px 8px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--font-xs);text-align:right;color:var(--text-primary);}
.cost-detail__unit-input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.1);}
.cost-detail-block__total{display:flex;justify-content:flex-end;align-items:center;gap:6px;padding-top:6px;font-size:var(--font-sm);}
@@ -376,38 +412,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
</div>
</main>
<!-- 新增食材弹窗 -->
<div class="dialog-mask" id="ingredientDialog">
<div class="dialog">
<div class="dialog__header">
<div class="dialog__title">新增食材</div>
<button class="dialog__close" onclick="closeIngredientDialog()"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
</div>
<div class="dialog__body">
<div class="form-row">
<div class="form-group"><label>食材名称<span class="required">*</span></label>
<select class="form-select" id="ingredientNameSelect" onchange="selectIngredient(this.value)"><option value="">请选择食材</option></select>
</div>
<div class="form-group"><label>净菜类型<span class="required">*</span></label><select class="form-select" id="ingredientNetType" disabled><option value="">请先选择食材</option></select></div>
</div>
<div class="form-row"><div class="form-group"><label>用量(g/份)<span class="required">*</span></label><input class="form-input" type="number" id="ingredientAmount" placeholder="请输入用量"></div><div class="form-group"><label>食材类型</label><select class="form-select" id="ingredientType"><option>辅材</option><option>主材</option><option>调料</option></select></div></div>
<div class="form-row"><div class="form-group"><label>过敏原</label><input class="form-input" type="text" id="ingredientAllergen" placeholder="无则填无"></div><div class="form-group"><label>备注</label><input class="form-input" type="text" id="ingredientNote" placeholder="请输入备注信息"></div></div>
</div>
<div class="dialog__footer">
<button class="btn btn-default" onclick="closeIngredientDialog()">取消</button>
<button class="btn btn-primary" onclick="confirmAddIngredient()">确定</button>
</div>
</div>
</div>
<script>
function toggleSubgroup(header){header.parentElement.classList.toggle('open');}
function openIngredientDialog(){
initIngredientNameOptions();
resetIngredientForm();
document.getElementById('ingredientDialog').classList.add('open');
}
function closeIngredientDialog(){document.getElementById('ingredientDialog').classList.remove('open');}
var urlParams = new URLSearchParams(window.location.search);
var pageMode = urlParams.get('mode') || 'add';
@@ -418,9 +424,9 @@ var INNOV_MAP = {
expNo:'INN-H002', goal:'添加燕麦、藜麦,膳食纤维提升3倍', members:'厨师长陈磊 / 营养师王芳',
name:'高纤维主食包', category:'主食', meal:'早/午', pyramid:'谷薯杂豆', allergen:'无', tag:'高纤维',
composition:[
{name:'燕麦',netType:'燕麦',amount:80,pct:'40%',type:'主材',allergen:'无',note:'提升纤维'},
{name:'藜麦',netType:'藜麦',amount:60,pct:'30%',type:'主材',allergen:'无',note:'低GI'},
{name:'大米',netType:'熟米饭',amount:60,pct:'30%',type:'辅材',allergen:'无',note:'基底'}
{code:'YQ-025',name:'燕麦',netName:'燕麦片',amount:80,type:'主材'},
{code:'YQ-026',name:'藜麦',netName:'藜麦粒',amount:60,type:'主材'},
{code:'YQ-011',name:'大米',netName:'熟米饭',amount:60,type:'辅材'}
],
cook:{mainMethod:'蒸',subMethod:'无',heat:'大火→中火',duration:30,temp:100,steps:4,desc:''},
nutri:{calorie:342,protein:12.8,carb:58.4,fat:5.2,vitC:0,mineral:38,fiber:9.6,sodium:210},
@@ -433,10 +439,10 @@ var INNOV_MAP = {
expNo:'INN-H004', goal:'搭配维C食材,补铁效率提升2倍', members:'厨师长赵敏 / 营养师李明',
name:'富铁青菜汤', category:'汤品', meal:'午/晚', pyramid:'蔬菜', allergen:'大豆', tag:'高铁',
composition:[
{name:'菠菜',netType:'菠菜',amount:120,pct:'40%',type:'主材',allergen:'无',note:'高铁来源'},
{name:'豆腐',netType:'豆腐',amount:80,pct:'27%',type:'主材',allergen:'大豆',note:'补充蛋白'},
{name:'胡萝卜',netType:'胡萝卜',amount:40,pct:'13%',type:'辅材',allergen:'无',note:'维C协同'},
{name:'食用油',netType:'净油',amount:5,pct:'2%',type:'调料',allergen:'无',note:'少量'}
{code:'YQ-007',name:'菠菜',netName:'菠菜段',netSpecAlias:'长段',amount:120,type:'主材'},
{code:'YQ-013',name:'豆腐',netName:'豆腐块',amount:80,type:'主材'},
{code:'YQ-009',name:'胡萝卜',netName:'胡萝卜丝',netSpecAlias:'细丝',amount:40,type:'辅材'},
{code:'YQ-014',name:'食用油',netName:'净油',amount:5,type:'调料'}
],
cook:{mainMethod:'煮',subMethod:'无',heat:'大火→小火',duration:15,temp:100,steps:3,desc:''},
nutri:{calorie:68,protein:5.4,carb:6.2,fat:2.8,vitC:32,mineral:86,fiber:2.4,sodium:320},
@@ -496,19 +502,29 @@ var STEP_HTML = [
<button type="button" class="dynamic-row-add" onclick="addPromoVideoRow()"> 添加宣传视频</button>
<div class="upload-hint">已上传的宣传视频将独立显示在员工端,每个可单独启用/停用</div>`,
// Step 1: 餐品构成(含熟重 + 营养指标自动计算)
// Step 1: 餐品构成(含熟重 + 双栏食材清单 + 营养指标自动计算)
`<div class="form-card__title">🧩 餐品构成</div>
<div class="form-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:8px;">
<div class="form-group"><label class="form-label">熟重(g)<span class="required">*</span></label><input class="form-input" id="cookedWeightInput" type="number" min="0" step="0.1" placeholder="请输入熟重" oninput="recalcAddNutri()"></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<span style="font-size:var(--font-sm);font-weight:600;">食材清单<span class="required">*</span></span>
<button class="btn btn-primary" style="height:30px;font-size:var(--font-xs);" onclick="openIngredientDialog()"> 新增食材</button>
<span style="font-size:var(--font-xs);color:var(--text-placeholder);">从右侧食材列表点击添加,选定食材后选择净菜类型(多规格净菜需先选净菜再选规格)</span>
</div>
<div class="ingredient-picker">
<div class="ingredient-picker__left">
<table class="ingredient-table" id="ingredientTable">
<thead><tr><th>食材编码</th><th>食材名称</th><th>食材类型</th><th>净菜类型</th><th>用量(g/份)</th><th style="width:60px;">操作</th></tr></thead>
<tbody id="ingredientTbody"><tr class="ingredient-empty"><td colspan="6" style="text-align:center;color:var(--text-placeholder);padding:32px;">暂无食材,请从右侧点击食材添加</td></tr></tbody>
</table>
</div>
<div class="ingredient-picker__right">
<div class="ingredient-picker__search">
<input type="text" id="ingredientSearchInput" placeholder="按食材名称或净菜标准名、净菜俗名搜索" oninput="filterIngredientPicker()">
</div>
<div class="ingredient-picker__list" id="ingredientPickerList"></div>
</div>
</div>
<table class="ingredient-table" id="ingredientTable">
<thead><tr><th>食材名称</th><th>净菜类型</th><th>用量(g/份)</th><th>食材类型</th><th>过敏原</th><th>备注</th><th>操作</th></tr></thead>
<tbody id="ingredientTbody"><tr><td colspan="7" style="text-align:center;color:var(--text-placeholder);padding:32px;">暂无食材,请点击"新增食材"添加</td></tr></tbody>
</table>
<div class="form-subtitle">📊 营养指标 <span style="font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;">(按 100g 熟重计)</span></div>
<div class="nutri-readonly-tip">ⓘ 营养指标根据食材清单自动计算,不可手动修改;请先录入食材后系统会自动汇总</div>
<div class="nutri-hero-form">
@@ -561,8 +577,8 @@ var STEP_HTML = [
<div class="cost-detail-block">
<div class="cost-detail-block__title">🧾 食材成本明细(每100g</div>
<table class="cost-detail-block__table">
<thead><tr><th>食材编号</th><th>食材名称</th><th>用量(g)</th><th>单价(元/kg)</th><th>价格更新时间</th><th>小计(元)</th></tr></thead>
<tbody id="costDetailTbody"><tr><td colspan="6" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr></tbody>
<thead><tr><th>食材编号</th><th>食材名称</th><th>用量(g)</th><th>单价(元/kg)</th><th>价格更新时间</th><th>价格更新方式</th><th>小计(元)</th></tr></thead>
<tbody id="costDetailTbody"><tr><td colspan="7" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr></tbody>
</table>
<div class="cost-detail-block__total"><span>食材总成本(100g):</span><strong>¥<span id="costDetailTotal">0.00</span></strong></div>
</div>
@@ -581,9 +597,9 @@ var STEP_HTML = [
</div>
<div class="form-subtitle">📐 餐品规格</div>
<table class="spec-table" id="specTable">
<thead><tr><th style="width:200px;">规格(克重/份型)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>默认</th><th style="width:60px;">操作</th></tr></thead>
<thead><tr><th style="width:200px;">规格(克重/份型)</th><th>餐品售价(元/份)</th><th>净菜包毛利率(%)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>默认</th><th style="width:60px;">操作</th></tr></thead>
<tbody id="specBody">
<tr class="spec-add-row" onclick="addSpecRow(this)"><td colspan="6" style="text-align:center;"> 添加规格</td></tr>
<tr class="spec-add-row" onclick="addSpecRow(this)"><td colspan="7" style="text-align:center;"> 添加规格</td></tr>
</tbody>
</table>`,
@@ -646,6 +662,10 @@ function renderStep(){
/* Step 0 阶段无需联动营养(食材在 Step 1) */
}
if(currentStep===1){
/* 初始化右侧食材选择列表(若已通过 fillEditData 预填,则该次渲染会反映已添加状态) */
if(!document.getElementById('ingredientPickerList').children.length){
renderIngredientPicker('');
}
setTimeout(function(){ recalcAddNutri(); }, 0);
}
if(pageMode==='edit' || pageMode==='convert') fillEditData();
@@ -706,7 +726,7 @@ function addSpecRow(addRow){
if(document.querySelector('.spec-edit-row')) return;
var tr=document.createElement('tr');
tr.className='spec-edit-row';
tr.innerHTML='<td><div class="spec-input-group"><input class="form-input" type="number" placeholder="克重" min="0"><select class="form-select">'+buildPortionOptions()+'</select></div></td><td><input class="form-input" type="number" placeholder="餐品售价"></td><td><input class="form-input" type="number" placeholder="净菜包售价"></td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td></td><td><button class="btn-text btn-text--primary" onclick="confirmSpec(this)">确定</button> <button class="btn-text btn-text--danger" onclick="cancelSpec(this)">取消</button></td>';
tr.innerHTML='<td><div class="spec-input-group"><input class="form-input" type="number" placeholder="克重" min="0"><select class="form-select">'+buildPortionOptions()+'</select></div></td><td><input class="form-input" type="number" placeholder="餐品售价"></td><td><input class="form-input" type="number" placeholder="净菜包毛利率" min="0" max="100" step="0.1"></td><td><input class="form-input" type="number" placeholder="净菜包售价"></td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td></td><td><button class="btn-text btn-text--primary" onclick="confirmSpec(this)">确定</button> <button class="btn-text btn-text--danger" onclick="cancelSpec(this)">取消</button></td>';
addRow.parentNode.insertBefore(tr,addRow);
}
function confirmSpec(btn){
@@ -715,18 +735,21 @@ function confirmSpec(btn){
var gramInput=group.querySelector('input[type="number"]');
var sel=group.querySelector('select');
var priceInput=row.querySelectorAll('input[type="number"]')[1];
var cleanPriceInput=row.querySelectorAll('input[type="number"]')[2];
var cleanMarginInput=row.querySelectorAll('input[type="number"]')[2];
var cleanPriceInput=row.querySelectorAll('input[type="number"]')[3];
var portion=sel?sel.value:'';
var gram=gramInput?gramInput.value.trim():'';
var price=priceInput?priceInput.value.trim():'';
var cleanMargin=cleanMarginInput?cleanMarginInput.value.trim():'';
var cleanPrice=cleanPriceInput?cleanPriceInput.value.trim():'';
if(!portion||portion==='请选择'||!gram||!price){alert('请填写完整信息');return;}
var cleanMarginText = cleanMargin ? parseFloat(cleanMargin).toFixed(1)+'%' : '—';
var cleanPriceText = cleanPrice ? parseFloat(cleanPrice).toFixed(2) : '—';
var saleActive = !!row.querySelector('.sale-toggle.active');
var saleHtml = '<div class="sale-toggle'+(saleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(saleActive?'上架':'下架')+'</span></div>';
var specViewHtml='<div class="spec-cell-view"><span class="spec-cell-view__gram">'+parseFloat(gram)+'g</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">'+portion+'</span></div>';
row.className='';
row.innerHTML='<td>'+specViewHtml+'</td><td>'+parseFloat(price).toFixed(2)+'</td><td>'+cleanPriceText+'</td><td>'+saleHtml+'</td><td><div class="spec-toggle" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td>';
row.innerHTML='<td>'+specViewHtml+'</td><td>'+parseFloat(price).toFixed(2)+'</td><td>'+cleanMarginText+'</td><td>'+cleanPriceText+'</td><td>'+saleHtml+'</td><td><div class="spec-toggle" onclick="toggleDefault(this)"><span class="spec-toggle__switch"></span><span class="spec-toggle__label">默认</span></div></td><td><button class="btn-text btn-text--danger" onclick="removeSpec(this)">删除</button></td>';
}
function toggleCleanSale(el){
var active = el.classList.toggle('active');
@@ -799,29 +822,26 @@ function calcCostPrice(){
row.style.display = 'none';
return;
}
/* 食材来源:Step 1 餐品构成中的食材表 #ingredientTbody,若无则用演示数据 */
var rows = document.querySelectorAll('#ingredientTbody tr');
var hasRealIngredient = rows.length > 0 && !(rows.length === 1 && rows[0].querySelector('td[colspan]'));
/* 食材来源:Step 1 餐品构成中的食材表行数据(由 getIngredientRowsData 统一读取),无则用演示数据 */
var ingRows = getIngredientRowsData();
var hasRealIngredient = ingRows.length > 0;
var items = [];
if(!hasRealIngredient){
items = [
{code:'ING-001', name:'鸡胸肉', amount:120, unitPrice:25.00, priceDate:'2026-08-04'},
{code:'ING-014', name:'西兰花', amount:80, unitPrice:10.00, priceDate:'2026-08-03'},
{code:'ING-027', name:'橄榄油', amount:10, unitPrice:35.00, priceDate:'2026-08-01'},
{code:'ING-031', name:'食盐', amount:3, unitPrice:6.00, priceDate:'2026-07-28'}
{code:'YQ-004', name:'鸡胸肉', amount:120, unitPrice:25.00, priceDate:'2026-08-04', priceMode:'最新验收价'},
{code:'YQ-024', name:'时令青菜', amount:80, unitPrice:10.00, priceDate:'2026-08-03', priceMode:'最新验收价'},
{code:'YQ-014', name:'食用油', amount:10, unitPrice:35.00, priceDate:'2026-08-01', priceMode:'手工录入'},
{code:'YQ-018', name:'食盐', amount:3, unitPrice:6.00, priceDate:'2026-07-28', priceMode:'手工录入'}
];
} else {
rows.forEach(function(r, i){
var tds = r.querySelectorAll('td');
if(tds.length < 3) return;
var name = tds[0] ? tds[0].textContent : '';
var amount = parseFloat(tds[2] ? tds[2].textContent : 0) || 0;
ingRows.forEach(function(d, i){
/* 简化原型:单价按食材类型粗略给(主材25元/kg / 辅材12元/kg / 调料8元/kg) */
var typeText = tds[3] ? tds[3].textContent : '';
var unitPrice = 25;
if(typeText.indexOf('辅材')>=0) unitPrice = 12;
else if(typeText.indexOf('调料')>=0) unitPrice = 8;
items.push({code:'ING-' + String(i+1).padStart(3,'0'), name:name, amount:amount, unitPrice:unitPrice, priceDate:'2026-08-05'});
if(d.type==='辅材') unitPrice = 12;
else if(d.type==='调料') unitPrice = 8;
/* 价格更新方式:主材默认走"最新验收价",辅材/调料默认"手工录入" */
var priceMode = (d.type==='辅材' || d.type==='调料') ? '手工录入' : '最新验收价';
items.push({code:d.code, name:d.name, amount:d.amount, unitPrice:unitPrice, priceDate:'2026-08-05', priceMode:priceMode});
});
}
/* 按 100g 餐品等比缩放:scale = 100 / Σ原用量,缩放后所有用量合计 = 100g */
@@ -832,18 +852,22 @@ function calcCostPrice(){
var total = 0;
var html = '';
if(items.length === 0 || scale === 0){
html = '<tr><td colspan="6" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr>';
html = '<tr><td colspan="7" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr>';
} else {
items.forEach(function(d, idx){
var scaledAmount = d.amount * scale;
var subtotal = scaledAmount * d.unitPrice / 1000;
total += subtotal;
/* 价格更新方式只读胶囊:手工录入(灰)/最新验收价(绿) */
var modeClass = d.priceMode === '最新验收价' ? 'cost-price-mode--accept' : 'cost-price-mode--manual';
var modeHtml = '<span class="cost-price-mode '+modeClass+'">'+(d.priceMode||'手工录入')+'</span>';
html += '<tr>'
+ '<td>'+d.code+'</td>'
+ '<td>'+d.name+'</td>'
+ '<td class="cost-detail__amount" data-row-idx="'+idx+'">'+scaledAmount.toFixed(2)+'</td>'
+ '<td><input class="cost-detail__unit-input" type="number" step="0.01" min="0" value="'+d.unitPrice.toFixed(2)+'" data-row-idx="'+idx+'" oninput="recalcCostRow(this)"></td>'
+ '<td class="cost-detail__price-date">'+d.priceDate+'</td>'
+ '<td class="cost-detail__price-mode">'+modeHtml+'</td>'
+ '<td class="cost-detail__subtotal" data-row-idx="'+idx+'">'+subtotal.toFixed(2)+'</td>'
+ '</tr>';
});
@@ -986,77 +1010,164 @@ function removeSpec(btn){
if(wasDefault){var first=document.querySelector('#specBody tr:not(.spec-add-row):not(.spec-edit-row) .spec-toggle');if(first)first.classList.add('active');}
}
/* 食材搜索选择 */
/* 食材清单数据:每个食材含 code + 多个净菜(netName 净菜形式,本身可含-分隔符) + 净菜规格别名(aliases 多规格时需先选净菜再选规格) */
var INGREDIENT_LIST=[
{name:'猪五花肉',types:['红烧块','五花片']},{name:'猪里脊肉',types:['里脊丝','里脊片']},{name:'牛腩',types:['牛腩块']},{name:'鸡胸肉',types:['鸡丁','鸡丝']},{name:'鸡蛋',types:['鸡蛋液','鸡蛋块']},
{name:'番茄',types:['番茄块','番茄丁']},{name:'菠菜',types:['菠菜段','菠菜泥']},{name:'土豆',types:['土豆丝','土豆块','土豆泥']},{name:'胡萝卜',types:['胡萝卜丝','胡萝卜块']},{name:'大白菜',types:['白菜帮','白菜叶']},
{name:'大米',types:['熟米饭','米粒']},{name:'面粉',types:['面粉糊']},{name:'豆腐',types:['豆腐块','豆腐丁']},{name:'食用油',types:['净油']},{name:'生姜',types:['姜丝','姜片']},
{name:'大蒜',types:['蒜末','蒜片']},{name:'生抽',types:['调味汁']},{name:'食盐',types:['盐粒']},{name:'白糖',types:['糖浆']},{name:'料酒',types:['料酒汁']}
{code:'YQ-001',name:'猪五花肉',netTypes:[{netName:'红烧块'},{netName:'五花片',aliases:['薄片','厚片']}]},
{code:'YQ-002',name:'猪里脊肉',netTypes:[{netName:'里脊丝',aliases:['丝','粗丝']},{netName:'里脊片',aliases:['薄片','厚片']}]},
{code:'YQ-003',name:'牛腩',netTypes:[{netName:'牛腩块',aliases:['块','大块']}]},
{code:'YQ-004',name:'鸡胸肉',netTypes:[{netName:'鸡丁',aliases:['大丁','小丁']},{netName:'鸡丝',aliases:['细丝','粗丝']}]},
{code:'YQ-005',name:'鸡蛋',netTypes:[{netName:'鸡蛋液'},{netName:'鸡蛋块'}]},
{code:'YQ-006',name:'番茄',netTypes:[{netName:'番茄块'},{netName:'番茄丁',aliases:['大丁','小丁']}]},
{code:'YQ-007',name:'菠菜',netTypes:[{netName:'菠菜段',aliases:['长段','短段']},{netName:'菠菜泥'}]},
{code:'YQ-008',name:'土豆',netTypes:[{netName:'土豆-丝',aliases:['细丝','粗丝']},{netName:'土豆块'},{netName:'土豆泥'}]},
{code:'YQ-009',name:'胡萝卜',netTypes:[{netName:'胡萝卜丝',aliases:['细丝','粗丝']},{netName:'胡萝卜块'}]},
{code:'YQ-010',name:'大白菜',netTypes:[{netName:'白菜帮'},{netName:'白菜叶'}]},
{code:'YQ-011',name:'大米',netTypes:[{netName:'熟米饭'},{netName:'米粒'}]},
{code:'YQ-012',name:'面粉',netTypes:[{netName:'面粉糊'}]},
{code:'YQ-013',name:'豆腐',netTypes:[{netName:'豆腐块'},{netName:'豆腐丁',aliases:['大丁','小丁']}]},
{code:'YQ-014',name:'食用油',netTypes:[{netName:'净油'}]},
{code:'YQ-015',name:'生姜',netTypes:[{netName:'姜丝',aliases:['细丝','粗丝']},{netName:'姜片',aliases:['薄片','厚片']}]},
{code:'YQ-016',name:'大蒜',netTypes:[{netName:'蒜末',aliases:['细末','粗末']},{netName:'蒜片',aliases:['薄片','厚片']}]},
{code:'YQ-017',name:'生抽',netTypes:[{netName:'调味汁'}]},
{code:'YQ-018',name:'食盐',netTypes:[{netName:'盐粒'}]},
{code:'YQ-019',name:'白糖',netTypes:[{netName:'糖浆'}]},
{code:'YQ-020',name:'料酒',netTypes:[{netName:'料酒汁'}]},
{code:'YQ-021',name:'食醋',netTypes:[{netName:'调味汁'}]},
{code:'YQ-022',name:'淀粉',netTypes:[{netName:'淀粉糊'}]},
{code:'YQ-023',name:'冰糖',netTypes:[{netName:'糖浆'}]},
{code:'YQ-024',name:'时令青菜',netTypes:[{netName:'青菜段',aliases:['长段','短段']}]},
{code:'YQ-025',name:'燕麦',netTypes:[{netName:'燕麦片'}]},
{code:'YQ-026',name:'藜麦',netTypes:[{netName:'藜麦粒'}]}
];
function initIngredientNameOptions(){
var nameSelect=document.getElementById('ingredientNameSelect');
if(!nameSelect) return;
nameSelect.innerHTML='<option value="">请选择食材</option>'+INGREDIENT_LIST.map(function(item){
return '<option value="'+item.name+'">'+item.name+'</option>';
/* 渲染右侧食材选择列表(按搜索关键词过滤,已添加的食材显示"已添加"标记并禁用点击) */
function renderIngredientPicker(keyword){
var listEl=document.getElementById('ingredientPickerList');
if(!listEl) return;
var kw=(keyword||'').trim().toLowerCase();
var html=INGREDIENT_LIST.filter(function(item){
if(!kw) return true;
return (item.name||'').toLowerCase().indexOf(kw)>=0 || (item.code||'').toLowerCase().indexOf(kw)>=0;
}).map(function(item){
var added=isIngredientAdded(item.code);
var cls=added?'ingredient-picker__item is-added':'ingredient-picker__item';
var attr=added?'':'onclick="addIngredientRow(\''+item.code+'\')"';
var badge=added?'<span class="ingredient-picker__badge">已添加</span>':'';
return '<div class="'+cls+'" data-code="'+item.code+'" '+attr+'>'
+'<span class="ingredient-picker__code">'+item.code+'</span>'
+'<span class="ingredient-picker__name">'+item.name+'</span>'
+badge
+'</div>';
}).join('');
if(!html) html='<div class="ingredient-picker__empty">无匹配食材</div>';
listEl.innerHTML=html;
}
function resetIngredientForm(){
var nameSelect=document.getElementById('ingredientNameSelect');
if(nameSelect) nameSelect.value='';
var netTypeSelect=document.getElementById('ingredientNetType');
if(netTypeSelect){
netTypeSelect.innerHTML='<option value="">请先选择食材</option>';
netTypeSelect.disabled=true;
}
document.getElementById('ingredientAmount').value='';
document.getElementById('ingredientType').selectedIndex=0;
document.getElementById('ingredientAllergen').value='';
document.getElementById('ingredientNote').value='';
function filterIngredientPicker(){
var input=document.getElementById('ingredientSearchInput');
renderIngredientPicker(input?input.value:'');
}
function selectIngredient(name){
var nameSelect=document.getElementById('ingredientNameSelect');
if(nameSelect) nameSelect.value=name;
var match=INGREDIENT_LIST.find(function(item){return item.name===name;});
var netTypeSelect=document.getElementById('ingredientNetType');
if(netTypeSelect){
netTypeSelect.innerHTML='';
if(match && match.types && match.types.length){
netTypeSelect.disabled=false;
netTypeSelect.innerHTML=match.types.map(function(type){return '<option value="'+type+'">'+type+'</option>';}).join('');
} else {
netTypeSelect.disabled=true;
netTypeSelect.innerHTML='<option value="">暂无净菜类型</option>';
}
}
function isIngredientAdded(code){
var tbody=document.getElementById('ingredientTbody');
if(!tbody) return false;
return !!tbody.querySelector('tr[data-code="'+code+'"]');
}
function confirmAddIngredient(){
var name=document.getElementById('ingredientNameSelect').value.trim();
var netType=document.getElementById('ingredientNetType').value.trim();
var amount=document.getElementById('ingredientAmount').value.trim();
if(!name){alert('请选择食材');return;}
if(!netType){alert('请选择净菜类型');return;}
if(!amount){alert('请填写用量');return;}
var ingredientType=document.getElementById('ingredientType').value;
var allergen=document.getElementById('ingredientAllergen').value.trim()||'无';
var note=document.getElementById('ingredientNote').value.trim()||'—';
var tbody=document.querySelector('.ingredient-table tbody');
/* 移除空状态行 */
var emptyRow=tbody.querySelector('td[colspan]');
if(emptyRow) emptyRow.parentElement.remove();
/* 点击右侧食材项:在左侧食材清单末尾添加一行,包含净菜下拉+条件显示的规格下拉 */
function addIngredientRow(code){
if(isIngredientAdded(code)) return;
var item=INGREDIENT_LIST.find(function(it){return it.code===code;});
if(!item) return;
var tbody=document.getElementById('ingredientTbody');
if(!tbody) return;
var emptyRow=tbody.querySelector('.ingredient-empty');
if(emptyRow) emptyRow.remove();
var netOptions='<option value="">请选择净菜</option>'+(item.netTypes||[]).map(function(n){
return '<option value="'+n.netName+'">'+n.netName+'</option>';
}).join('');
var tr=document.createElement('tr');
var typeTagMap={'主材':'tag-blue','辅材':'tag-gray','调料':'tag-orange'};
tr.innerHTML='<td>'+name+'</td><td>'+netType+'</td><td>'+amount+'</td>'
+'<td><span class="tag '+(typeTagMap[ingredientType]||'tag-gray')+'">'+ingredientType+'</span></td>'
+'<td>'+allergen+'</td>'
+'<td>'+note+'</td>'
+'<td><a class="link" onclick="this.closest(\'tr\').remove()">删除</a></td>';
tr.className='ingredient-row';
tr.setAttribute('data-code',code);
tr.setAttribute('data-name',item.name);
tr.innerHTML=''
+'<td class="td-code">'+code+'</td>'
+'<td class="td-name">'+item.name+'</td>'
+'<td class="td-type"><select class="form-select form-select--sm" onchange="onIngredientTypeChange(this)">'
+'<option value="主材">主材</option>'
+'<option value="辅材" selected>辅材</option>'
+'<option value="调料">调料</option>'
+'</select></td>'
+'<td class="td-net"><div class="td-net__inner">'
+'<select class="form-select form-select--sm ingredient-net-select" onchange="onNetTypeChange(this)">'+netOptions+'</select>'
+'<select class="form-select form-select--sm ingredient-spec-select" style="display:none;" onchange="onNetSpecChange(this)"></select>'
+'</div></td>'
+'<td class="td-amount"><input type="number" class="form-input form-input--sm ingredient-amount-input" placeholder="用量" min="0" step="0.1" oninput="recalcAfterIngredientChange()"></td>'
+'<td class="td-actions"><a class="link" onclick="removeIngredientRow(this)">删除</a></td>';
tbody.appendChild(tr);
/* 重置表单 */
resetIngredientForm();
closeIngredientDialog();
/* 重新计算营养指标和定价 */
renderIngredientPicker(document.getElementById('ingredientSearchInput')?document.getElementById('ingredientSearchInput').value:'');
recalcAfterIngredientChange();
}
function removeIngredientRow(btn){
var tr=btn.closest('tr');
if(!tr) return;
var tbody=tr.parentElement;
tr.remove();
if(tbody && tbody.querySelectorAll('tr.ingredient-row').length===0){
tbody.innerHTML='<tr class="ingredient-empty"><td colspan="6" style="text-align:center;color:var(--text-placeholder);padding:32px;">暂无食材,请从右侧点击食材添加</td></tr>';
}
renderIngredientPicker(document.getElementById('ingredientSearchInput')?document.getElementById('ingredientSearchInput').value:'');
recalcAfterIngredientChange();
}
function onIngredientTypeChange(sel){
recalcAfterIngredientChange();
}
/* 净菜下拉变化:如该净菜有多规格(aliases 长度>0)则显示规格下拉,否则隐藏 */
function onNetTypeChange(sel){
var tr=sel.closest('tr');
if(!tr) return;
var code=tr.getAttribute('data-code');
var item=INGREDIENT_LIST.find(function(it){return it.code===code;});
var netName=sel.value;
var specSel=tr.querySelector('.ingredient-spec-select');
if(!netName){
if(specSel){specSel.style.display='none';specSel.innerHTML='';}
recalcAfterIngredientChange();
return;
}
var netItem=item?(item.netTypes||[]).find(function(n){return n.netName===netName;}):null;
var aliases=netItem?(netItem.aliases||[]):[];
if(aliases.length>0){
if(specSel){
specSel.style.display='';
specSel.innerHTML='<option value="">请选择规格</option>'+aliases.map(function(a){return '<option value="'+a+'">'+a+'</option>';}).join('');
specSel.value='';
}
} else {
if(specSel){specSel.style.display='none';specSel.innerHTML='';}
}
recalcAfterIngredientChange();
}
function onNetSpecChange(sel){
recalcAfterIngredientChange();
}
/* 读取左侧食材清单所有行数据,供成本核算 calcCostPrice 使用 */
function getIngredientRowsData(){
var tbody=document.getElementById('ingredientTbody');
if(!tbody) return [];
var rows=tbody.querySelectorAll('tr.ingredient-row');
var result=[];
rows.forEach(function(r){
var code=r.getAttribute('data-code');
var name=r.getAttribute('data-name');
var typeSel=r.querySelector('.td-type select');
var type=typeSel?typeSel.value:'辅材';
var netSel=r.querySelector('.ingredient-net-select');
var netName=netSel?netSel.value:'';
var specSel=r.querySelector('.ingredient-spec-select');
var alias=(specSel && specSel.style.display!=='none')?specSel.value:'';
var amountInput=r.querySelector('.ingredient-amount-input');
var amount=amountInput?(parseFloat(amountInput.value)||0):0;
result.push({code:code,name:name,type:type,netName:netName,alias:alias,amount:amount});
});
return result;
}
/* 图片上传 */
function triggerImageUpload(){
@@ -1130,9 +1241,9 @@ var EDIT_DATA = {
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],
specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanPrice:6,cleanSale:false,isDefault:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanPrice:13,cleanSale:false,isDefault:false}],
specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanMargin:55.0,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanMargin:50.0,cleanPrice:6,cleanSale:false,isDefault:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanMargin:58.0,cleanPrice:13,cleanSale:false,isDefault:false}],
cookedWeight:200,
composition:[{name:'五花肉',netType:'红烧块',amount:150,pct:'60%',type:'主材',allergen:'无',note:'—'},{name:'冰糖',netType:'糖浆',amount:20,pct:'8%',type:'调料',allergen:'无',note:'上色'},{name:'酱油',netType:'调味汁',amount:15,pct:'6%',type:'调料',allergen:'含大豆',note:'调味'},{name:'料酒',netType:'料酒汁',amount:10,pct:'4%',type:'调料',allergen:'无',note:'去腥'}],
composition:[{code:'YQ-001',name:'五花肉',netName:'红烧块',amount:150,type:'主材'},{code:'YQ-023',name:'冰糖',netName:'糖浆',amount:20,type:'调料'},{code:'YQ-017',name:'生抽',netName:'调味汁',amount:15,type:'调料'},{code:'YQ-020',name:'料酒',netName:'料酒汁',amount:10,type:'调料'}],
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,temp:100,steps:6,desc:'1.五花肉切块焯水;2.锅中煎至金黄;3.加冰糖炒色;4.加酱油料酒;5.加水小火炖40分钟;6.大火收汁'},
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:2.1,mineral:45,fiber:1.8,sodium:486,calcium:18,iron:2.4,zinc:1.8,potassium:280,cholesterol:88,vitA:14,vitE:0.6,vitB1:0.22,vitB2:0.12,magnesium:20,phosphorus:160,selenium:18},
taste:{sour:1,sweet:3,bitter:1,spicy:1,salty:4,smell:4.2,flavorScore:3.8,flavorGrade:'good'},
@@ -1173,7 +1284,7 @@ if(pageMode==='convert' && INNOV){
EDIT_DATA.tag = INNOV.tag;
EDIT_DATA.status = '上架';
EDIT_DATA.crowd = [];
EDIT_DATA.specs = [{portion:'标准份',baseGram:350,unit:'份',price:15,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'小份',baseGram:200,unit:'份',price:10,cleanPrice:7,cleanSale:false,isDefault:false}];
EDIT_DATA.specs = [{portion:'标准份',baseGram:350,unit:'份',price:15,cleanMargin:52.0,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'小份',baseGram:200,unit:'份',price:10,cleanMargin:48.0,cleanPrice:7,cleanSale:false,isDefault:false}];
EDIT_DATA.cookedWeight = INNOV.cookedWeight || 200;
EDIT_DATA.composition = INNOV.composition;
EDIT_DATA.cook = INNOV.cook;
@@ -1220,18 +1331,70 @@ function fillEditData(){
renderPromoVideoList();
}
} else if(currentStep===1){
// 餐品构成 - 熟重 + 食材表格 + 营养指标只读自动计算
// 餐品构成 - 熟重 + 双栏食材清单(左清单+右选择) + 营养指标(只读自动计算)
if(inputs[0] && d.cookedWeight!==undefined) inputs[0].value = d.cookedWeight;
var tbody = container.querySelector('#ingredientTable tbody');
// 先初始化右侧食材选择列表
renderIngredientPicker('');
var tbody = container.querySelector('#ingredientTbody');
if(tbody){
tbody.innerHTML = '';
d.composition.forEach(function(c){
var tr = document.createElement('tr');
tr.innerHTML = (pageMode==='convert'
? '<td>'+c.name+'</td><td>'+(c.netType||'—')+'</td><td>'+c.amount+'g</td><td><span class="tag '+(c.type==='主材'?'tag-blue':c.type==='调料'?'tag-orange':'tag-gray')+'">'+(c.type||'辅材')+'</span></td><td>'+c.allergen+'</td><td>'+c.note+'</td><td><span style="color:#bbb;cursor:not-allowed;">编辑</span> <span style="color:#bbb;cursor:not-allowed;">删除</span></td>'
: '<td>'+c.name+'</td><td>'+(c.netType||'—')+'</td><td>'+c.amount+'g</td><td><span class="tag '+(c.type==='主材'?'tag-blue':c.type==='调料'?'tag-orange':'tag-gray')+'">'+(c.type||'辅材')+'</span></td><td>'+c.allergen+'</td><td>'+c.note+'</td><td><span class="link">编辑</span> <span class="link" style="color:var(--danger)">删除</span></td>');
tbody.appendChild(tr);
(d.composition||[]).forEach(function(c){
// 在 INGREDIENT_LIST 中按食材名匹配,以复用 addIngredientRow 的标准流程
var matched = INGREDIENT_LIST.find(function(it){return it.name===c.name;});
if(matched){
addIngredientRow(matched.code);
var tr = tbody.querySelector('tr[data-code="'+matched.code+'"]');
if(!tr) return;
var typeSel = tr.querySelector('.td-type select');
if(typeSel && c.type) typeSel.value = c.type;
var netSel = tr.querySelector('.ingredient-net-select');
if(netSel && c.netName){
// 若原数据 netName 不在 INGREDIENT_LIST 的 netTypes 中,追加一个 option 兜底
if(!netSel.querySelector('option[value="'+c.netName+'"]')){
var opt = document.createElement('option');
opt.value = c.netName;
opt.textContent = c.netName;
netSel.appendChild(opt);
}
netSel.value = c.netName;
onNetTypeChange(netSel);
var specSel = tr.querySelector('.ingredient-spec-select');
if(specSel && c.netSpecAlias && specSel.style.display!=='none'){
if(!specSel.querySelector('option[value="'+c.netSpecAlias+'"]')){
var sOpt = document.createElement('option');
sOpt.value = c.netSpecAlias;
sOpt.textContent = c.netSpecAlias;
specSel.appendChild(sOpt);
}
specSel.value = c.netSpecAlias;
}
}
var amountInput = tr.querySelector('.ingredient-amount-input');
if(amountInput && c.amount!==undefined) amountInput.value = c.amount;
} else {
// INGREDIENT_LIST 中无匹配:直接构建只读行(用 composition 原值)
var tr2 = document.createElement('tr');
tr2.className = 'ingredient-row';
tr2.setAttribute('data-code', c.code||'');
tr2.setAttribute('data-name', c.name||'');
tr2.innerHTML = ''
+ '<td class="td-code">'+(c.code||'—')+'</td>'
+ '<td class="td-name">'+(c.name||'—')+'</td>'
+ '<td class="td-type"><select class="form-select form-select--sm"><option value="主材">主材</option><option value="辅材">辅材</option><option value="调料">调料</option></select></td>'
+ '<td class="td-net"><div class="td-net__inner"><select class="form-select form-select--sm ingredient-net-select"><option value="'+(c.netName||'')+'">'+(c.netName||'—')+'</option></select><select class="form-select form-select--sm ingredient-spec-select" style="display:none;"></select></div></td>'
+ '<td class="td-amount"><input type="number" class="form-input form-input--sm ingredient-amount-input" value="'+(c.amount||0)+'"></td>'
+ '<td class="td-actions"><a class="link" onclick="removeIngredientRow(this)">删除</a></td>';
tbody.appendChild(tr2);
var tSel = tr2.querySelector('.td-type select');
if(tSel && c.type) tSel.value = c.type;
}
});
// 若未预填任何项,显示空态
if(tbody.querySelectorAll('tr.ingredient-row').length===0){
tbody.innerHTML = '<tr class="ingredient-empty"><td colspan="6" style="text-align:center;color:var(--text-placeholder);padding:32px;">暂无食材,请从右侧点击食材添加</td></tr>';
}
// 重新渲染右侧以反映已添加状态
renderIngredientPicker(document.getElementById('ingredientSearchInput')?document.getElementById('ingredientSearchInput').value:'');
}
// 营养指标 - 通过 ID 设置只读文本(4 大项 + 16 项更多)
if(d.nutri){
@@ -1259,7 +1422,8 @@ function fillEditData(){
var saleHtml = '<div class="sale-toggle'+(saleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(saleActive?'上架':'下架')+'</span></div>';
var portionVal = s.portion || s.spec || '';
var gramVal = s.baseGram!==undefined?s.baseGram:(s.weight!==undefined?s.weight:'');
tr.innerHTML = '<td><div class="spec-input-group"><input class="form-input form-input--sm" type="number" value="'+gramVal+'"><select class="form-select form-select--sm">'+buildPortionOptions(portionVal)+'</select></div></td><td><input class="form-input form-input--sm" value="'+s.price+'"></td><td><input class="form-input form-input--sm" value="'+(s.cleanPrice!==undefined?s.cleanPrice:'')+'"></td><td>'+saleHtml+'</td><td><input type="checkbox"'+(s.isDefault?' checked':'')+'></td><td><span class="link" onclick="this.closest(\'tr\').remove()">删除</span></td>';
var cleanMarginVal = s.cleanMargin!==undefined?s.cleanMargin:'';
tr.innerHTML = '<td><div class="spec-input-group"><input class="form-input form-input--sm" type="number" value="'+gramVal+'"><select class="form-select form-select--sm">'+buildPortionOptions(portionVal)+'</select></div></td><td><input class="form-input form-input--sm" value="'+s.price+'"></td><td><input class="form-input form-input--sm" type="number" step="0.1" min="0" max="100" value="'+cleanMarginVal+'" placeholder="净菜包毛利率"></td><td><input class="form-input form-input--sm" value="'+(s.cleanPrice!==undefined?s.cleanPrice:'')+'"></td><td>'+saleHtml+'</td><td><input type="checkbox"'+(s.isDefault?' checked':'')+'></td><td><span class="link" onclick="this.closest(\'tr\').remove()">删除</span></td>';
specBody.appendChild(tr);
});
}
+275 -60
View File
@@ -73,6 +73,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.panel-left__search input:focus{border-color:var(--primary);}
.panel-left__filter{display:flex;gap:8px;}
.panel-left__filter select{height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 24px 0 10px;font-size:var(--font-xs);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;flex:1;}
/* 高级搜索筛选条件汇总条(显示已应用的筛选条件,可一键清空) */
.panel-left__filter-summary{margin-top:8px;padding:6px 10px;background:var(--primary-light);border:1px solid #BAE7FF;border-radius:var(--radius-sm);font-size:var(--font-xs);color:var(--primary);display:flex;align-items:center;justify-content:space-between;gap:8px;}
.panel-left__filter-summary__text{flex:1;line-height:1.4;}
.panel-left__filter-summary__clear{flex-shrink:0;cursor:pointer;color:var(--danger);font-size:var(--font-xs);background:none;border:none;padding:0 4px;}
.panel-left__filter-summary__clear:hover{text-decoration:underline;}
.panel-left__actions{display:flex;gap:8px;padding:10px 20px;border-bottom:1px solid var(--border);flex-wrap:wrap;}
.panel-left__actions .btn{height:30px;font-size:var(--font-xs);padding:0 10px;}
.food-list{flex:1;overflow-y:auto;padding:4px 0;}
@@ -116,6 +121,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.info-table td{padding:10px 12px;border-bottom:1px solid var(--border);color:var(--text-primary);}
.info-table tr:last-child td{border-bottom:none;}
.info-table tr:hover{background:#F8FBFF;}
/* 净菜类型单元格:净菜名称(主样式)+ 净菜规格别名(弱化) */
.net-type{font-size:var(--font-sm);color:var(--text-primary);}
.net-type-alias{margin-left:6px;font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;}
.spec-detail-table th{padding:6px 12px;font-size:12px;}
.spec-detail-table td{padding:6px 12px;font-size:12px;}
/* 规格只读展示(克重g / 份型 合并单列) */
@@ -157,6 +165,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.form-input:focus,.form-select:focus{border-color:var(--primary);}
.form-select{appearance: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 10px center;padding-right:28px;}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;}
/* 高级搜索表单网格(2 列) + 区间输入(双 input 用 ~ 分隔) */
.adv-search-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 18px;}
.adv-search-grid .form-group{margin-bottom:0;}
.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;}
/* 操作列固定右侧 */
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;}
@@ -322,13 +336,15 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<div class="panel-left__header">
<div class="panel-left__title">餐品列表</div>
<div class="panel-left__search">
<input type="text" placeholder="搜索餐品名称/编号...">
<button class="btn btn-primary" style="height:32px;padding:0 12px;font-size:var(--font-xs);">搜索</button>
<input type="text" id="dishQuickSearchInput" placeholder="搜索餐品名称/编号..." oninput="onQuickSearchInput(this.value)">
<button class="btn btn-primary" style="height:32px;padding:0 12px;font-size:var(--font-xs);" onclick="onQuickSearch()">搜索</button>
<button class="btn btn-default" style="height:32px;padding:0 10px;font-size:var(--font-xs);" onclick="openAdvancedSearchDialog()">⚙ 高级搜索</button>
</div>
<div class="panel-left__filter">
<select><option>全部分类</option><option>主食</option><option>荤菜</option><option>素菜</option><option>汤品</option><option>小吃</option></select>
<select><option>全部状态</option><option>启用</option><option>停用</option></select>
<select id="dishCategoryFilter" onchange="onQuickSearch()"><option>全部分类</option><option>主食</option><option>荤菜</option><option>素菜</option><option>汤品</option><option>小吃</option></select>
<select id="dishStatusFilter" onchange="onQuickSearch()"><option>全部状态</option><option>启用</option><option>停用</option></select>
</div>
<div class="panel-left__filter-summary" id="filterSummary" style="display:none;"></div>
</div>
<div class="panel-left__actions">
<a href="din-dish-add.html" class="btn btn-primary"> 新增餐品</a>
@@ -336,8 +352,9 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<button class="btn btn-default" onclick="openDishCategoryDialog()">📂 餐品分类管理</button>
</div>
<div class="food-list" id="dishList"></div>
<div id="dishListEmpty" style="display:none;text-align:center;padding:40px 20px;color:var(--text-placeholder);font-size:var(--font-sm);">未找到匹配的餐品<br><span style="font-size:var(--font-xs);">请调整搜索条件或 <span class="link" onclick="clearAdvancedSearch()">清空筛选</span></span></div>
<div class="panel-left__footer">
<span>486</span>
<span id="dishListCount">0</span>
<div style="display:flex;gap:4px;">
<button class="page-btn"></button><button class="page-btn active">1</button><button class="page-btn">2</button><button class="page-btn">3</button><button class="page-btn"></button>
</div>
@@ -367,58 +384,115 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
</div>
</main>
<!-- 高级搜索弹窗 -->
<div class="dialog-mask" id="advancedSearchDialog">
<div class="dialog" style="width:720px;">
<div class="dialog__header">
<div class="dialog__title">⚙ 高级搜索</div>
<button class="dialog__close" onclick="closeAdvancedSearchDialog()"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
</div>
<div class="dialog__body">
<div class="adv-search-grid">
<div class="form-group"><label>餐品名称</label><input class="form-input" id="advName" placeholder="支持模糊匹配"></div>
<div class="form-group"><label>餐品编号</label><input class="form-input" id="advCode" placeholder="如 DISH001"></div>
<div class="form-group"><label>餐品分类</label>
<select class="form-select" id="advCategory">
<option value="">全部</option>
<option>主食</option><option>荤菜</option><option>素菜</option><option>汤品</option><option>小吃</option>
</select>
</div>
<div class="form-group"><label>适用餐次</label>
<select class="form-select" id="advMeal">
<option value="">全部</option>
<option>早餐</option><option>午餐</option><option>晚餐</option><option>早/午</option><option>午/晚</option><option>早/午/晚</option>
</select>
</div>
<div class="form-group"><label>餐品标签</label>
<select class="form-select" id="advTag">
<option value="">全部</option>
<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"><label>上架状态</label>
<select class="form-select" id="advStatus">
<option value="">全部</option>
<option>启用</option><option>停用</option>
</select>
</div>
<div class="form-group"><label>食材成本区间(元/100g)</label>
<div class="adv-range"><input class="form-input" id="advCostMin" type="number" step="0.01" min="0" placeholder="最低"><span class="adv-range__sep">~</span><input class="form-input" id="advCostMax" type="number" step="0.01" min="0" placeholder="最高"></div>
</div>
<div class="form-group"><label>餐品售价区间(元/份)</label>
<div class="adv-range"><input class="form-input" id="advPriceMin" type="number" step="0.01" min="0" placeholder="最低"><span class="adv-range__sep">~</span><input class="form-input" id="advPriceMax" type="number" step="0.01" min="0" placeholder="最高"></div>
</div>
<div class="form-group"><label>创建时间</label>
<div class="adv-range"><input class="form-input" id="advCreatedStart" type="date"><span class="adv-range__sep">~</span><input class="form-input" id="advCreatedEnd" type="date"></div>
</div>
<div class="form-group"><label>更新时间</label>
<div class="adv-range"><input class="form-input" id="advUpdatedStart" type="date"><span class="adv-range__sep">~</span><input class="form-input" id="advUpdatedEnd" type="date"></div>
</div>
</div>
</div>
<div class="dialog__footer">
<button class="btn btn-default" onclick="resetAdvancedSearch()">重置</button>
<button class="btn btn-default" onclick="closeAdvancedSearchDialog()">取消</button>
<button class="btn btn-primary" onclick="applyAdvancedSearch()">应用筛选</button>
</div>
</div>
</div>
<script>
// 餐品数据
const DISHES = [
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanPrice:6,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanMargin:55.0,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanMargin:50.0,cleanPrice:6,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanMargin:58.0,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',updateTime:'2026-08-10',
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
composition:[{name:'猪五花肉',netType:'红烧块',amount:160,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'生姜',netType:'姜片',amount:5,pct:'2.5%',state:'净菜',type:'辅材',allergen:'无',note:'—'},{name:'生抽',netType:'调味汁',amount:15,pct:'7.5%',state:'原料',type:'调料',allergen:'含麸质',note:'酱油'},{name:'冰糖',netType:'糖浆',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'上色提味'},{name:'料酒',netType:'料酒汁',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'去腥'}],
composition:[{code:'YQ-001',name:'猪五花肉',netName:'红烧块',amount:160,type:'主材'},{code:'YQ-015',name:'生姜',netName:'姜片',amount:5,type:'辅材'},{code:'YQ-017',name:'生抽',netName:'调味汁',amount:15,type:'调料'},{code:'YQ-023',name:'冰糖',netName:'糖浆',amount:10,type:'调料'},{code:'YQ-020',name:'料酒',netName:'料酒汁',amount:10,type:'调料'}],
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:0.8,mineral:42,fiber:0.3,sodium:486,calcium:18,iron:2.4,zinc:1.8,potassium:280,cholesterol:88,vitA:14,vitE:0.6,vitB1:0.22,vitB2:0.12,magnesium:20,phosphorus:160,selenium:18,copper:0.12,manganese:0.05,nutriGrade:'偏差',note:'铁2.4mg、锌1.8mg'},
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,temp:'100→80',steps:6},
taste:{sour:1,sweet:3,bitter:1,spicy:1,salty:4,smell:4.2,flavorScore:3.8,flavorGrade:'良好'},
color:{mainColor:'红褐色',uniformity:4,gloss:4,freshness:4,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'良好'}
},
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,cleanMargin:60.0,cleanPrice:4.5,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,cleanMargin:55.0,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',updateTime:'2026-08-09',
pricing:{basePrice:'3.00',ingredientCost:1.80,per100gCost:1.20,profitRate:150,suggestedPrice:'3.00'},
promoVideos:[{title:'清炒时蔬宣传片',duration:'45秒',size:'18MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
composition:[{name:'时令青菜',netType:'青菜段',amount:120,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'蒜',netType:'蒜末',amount:5,pct:'3.3%',state:'净菜',type:'调料',allergen:'无',note:'—'},{name:'食用油',netType:'净油',amount:8,pct:'5.3%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'1.3%',state:'原料',type:'调料',allergen:'无',note:'—'}],
composition:[{code:'YQ-024',name:'时令青菜',netName:'青菜段',netSpecAlias:'长段',amount:120,type:'主材'},{code:'YQ-016',name:'蒜',netName:'蒜末',netSpecAlias:'细末',amount:5,type:'调料'},{code:'YQ-014',name:'食用油',netName:'净油',amount:8,type:'调料'},{code:'YQ-018',name:'食盐',netName:'盐粒',amount:2,type:'调料'}],
nutri:{calorie:68,protein:2.1,carb:8.6,fat:3.2,vitC:28.5,mineral:86,fiber:2.4,sodium:186,calcium:45,iron:1.2,zinc:0.4,potassium:280,cholesterol:0,vitA:268,vitE:0.9,vitB1:0.04,vitB2:0.06,magnesium:24,phosphorus:42,selenium:1.2,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'铁1.2mg、钙45mg'},
cook:{mainMethod:'炒',subMethod:'—',heat:'大火',duration:5,temp:'200',steps:4},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:2,smell:3.5,flavorScore:3.2,flavorGrade:'一般'},
color:{mainColor:'翠绿色',uniformity:5,gloss:4,freshness:5,appearance:4,colorScore:4.5,colorGrade:'优秀'},
texture:{hardness:3,sticky:1,crispy:4,smooth:3,elastic:2,textureScore:3.4,textureGrade:'良好'}
},
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,cleanMargin:42.0,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',updateTime:'2026-08-08',
pricing:{basePrice:'1.67',ingredientCost:1.20,per100gCost:0.40,profitRate:150,suggestedPrice:'1.00'},
promoVideos:[{title:'番茄鸡蛋汤宣传片',duration:'1分钟',size:'24MB',enabled:true},{title:'家常做法教程',duration:'3分钟',size:'80MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
composition:[{name:'番茄',netType:'番茄',amount:150,pct:'50%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'鸡蛋',netType:'鸡蛋液',amount:60,pct:'20%',state:'原料',type:'主材',allergen:'含蛋类',note:'—'},{name:'食用油',netType:'净油',amount:5,pct:'1.7%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'0.7%',state:'原料',type:'调料',allergen:'无',note:'—'}],
composition:[{code:'YQ-006',name:'番茄',netName:'番茄块',amount:150,type:'主材'},{code:'YQ-005',name:'鸡蛋',netName:'鸡蛋液',amount:60,type:'主材'},{code:'YQ-014',name:'食用油',netName:'净油',amount:5,type:'调料'},{code:'YQ-018',name:'食盐',netName:'盐粒',amount:2,type:'调料'}],
nutri:{calorie:45,protein:3.8,carb:4.2,fat:2.1,vitC:18.2,mineral:32,fiber:1.1,sodium:280,calcium:18,iron:0.8,zinc:0.4,potassium:210,cholesterol:110,vitA:62,vitE:0.5,vitB1:0.04,vitB2:0.12,magnesium:12,phosphorus:80,selenium:8,copper:0.05,manganese:0.06,nutriGrade:'达标',note:'铁0.8mg、钾210mg'},
cook:{mainMethod:'煮',subMethod:'炒',heat:'中火',duration:12,temp:'100',steps:5},
taste:{sour:3,sweet:2,bitter:1,spicy:1,salty:2,smell:3.8,flavorScore:3.5,flavorGrade:'良好'},
color:{mainColor:'橙红色',uniformity:4,gloss:3,freshness:5,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:1,sticky:2,crispy:1,smooth:5,elastic:2,textureScore:3.8,textureGrade:'良好'}
},
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,cleanPrice:2,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,cleanPrice:1.5,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,cleanMargin:38.0,cleanPrice:2,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,cleanMargin:35.0,cleanPrice:1.5,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:4,cleanMargin:40.0,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',updateTime:'2026-07-22',
pricing:{basePrice:'1.50',ingredientCost:1.00,per100gCost:0.50,profitRate:150,suggestedPrice:'1.25'},
promoVideos:[],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
composition:[{name:'大米',netType:'熟米饭',amount:100,pct:'100%',state:'原料',type:'主材',allergen:'无',note:'—'}],
composition:[{code:'YQ-011',name:'大米',netName:'熟米饭',amount:100,type:'主材'}],
nutri:{calorie:232,protein:4.8,carb:51.2,fat:0.6,vitC:0,mineral:12,fiber:0.4,sodium:3,calcium:10,iron:0.5,zinc:0.8,potassium:43,cholesterol:0,vitA:0,vitE:0.1,vitB1:0.02,vitB2:0.02,magnesium:12,phosphorus:43,selenium:1.5,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'—'},
cook:{mainMethod:'蒸',subMethod:'—',heat:'大火→中火',duration:30,temp:'100',steps:2},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:1,smell:3.0,flavorScore:2.8,flavorGrade:'一般'},
color:{mainColor:'乳白色',uniformity:5,gloss:4,freshness:4,appearance:4,colorScore:4.2,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:3,elastic:2,textureScore:3.2,textureGrade:'一般'}
},
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,cleanMargin:62.0,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,cleanMargin:65.0,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',updateTime:'2026-08-05',
pricing:{basePrice:'7.78',ingredientCost:5.20,per100gCost:2.89,profitRate:170,suggestedPrice:'7.80'},
promoVideos:[{title:'糖醋里脊宣传片',duration:'2分钟',size:'58MB',enabled:true},{title:'正宗做法揭秘',duration:'5分钟',size:'150MB',enabled:true},{title:'出品摆盘展示',duration:'30秒',size:'12MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
composition:[{name:'猪里脊肉',netType:'里脊',amount:120,pct:'66.7%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'淀粉',netType:'淀粉糊',amount:20,pct:'11.1%',state:'原料',type:'辅材',allergen:'无',note:'挂糊'},{name:'白糖',netType:'糖浆',amount:25,pct:'13.9%',state:'料',type:'调料',allergen:'无',note:'糖醋汁'},{name:'食醋',netType:'调味汁',amount:15,pct:'8.3%',state:'原料',type:'调料',allergen:'无',note:'糖醋汁'}],
composition:[{code:'YQ-002',name:'里脊',netName:'里脊丝',netSpecAlias:'细丝',amount:120,type:'主材'},{code:'YQ-022',name:'淀粉',netName:'淀粉糊',amount:20,type:'辅材'},{code:'YQ-019',name:'白糖',netName:'糖浆',amount:25,type:'料'},{code:'YQ-021',name:'食醋',netName:'调味汁',amount:15,type:'调料'}],
nutri:{calorie:420,protein:22.4,carb:38.2,fat:18.6,vitC:1.2,mineral:28,fiber:0.6,sodium:620,calcium:22,iron:1.8,zinc:1.6,potassium:340,cholesterol:75,vitA:8,vitE:0.4,vitB1:0.45,vitB2:0.18,magnesium:24,phosphorus:200,selenium:14,copper:0.1,manganese:0.08,nutriGrade:'未达标',note:'铁1.8mg'},
cook:{mainMethod:'炸',subMethod:'炒',heat:'大火',duration:15,temp:'180',steps:7},
taste:{sour:4,sweet:4,bitter:1,spicy:1,salty:3,smell:4.0,flavorScore:4.2,flavorGrade:'良好'},
@@ -429,16 +503,20 @@ const DISHES = [
let currentDish = 0;
let currentTab = 0;
/* 列表渲染基于 filteredDishes(由快速搜索 / 高级搜索筛选产生),默认全量 */
let filteredDishes = [];
/* 高级搜索已应用条件(用于汇总条显示) */
let appliedAdvSearch = null;
function renderDishList(){
document.getElementById('dishList').innerHTML = DISHES.map((d,i)=>`
document.getElementById('dishList').innerHTML = filteredDishes.map((entry)=>{const d=entry.dish;const i=entry.idx;return `
<div class="food-item${i===currentDish?' active':''}" onclick="selectDish(${i})">
<div class="food-item__icon">${d.icon}</div>
<div class="food-item__info">
<div class="food-item__name">${d.name}</div>
<div class="food-item__meta"><span>${d.id}</span><span>${d.category}</span><span class="tag tag-${d.status==='启用'?'green':'gray'}" style="margin:0;padding:1px 6px;">${d.status}</span></div>
<div class="food-item__meta"><span>${d.updateTime||'—'}</span><span>${d.category}</span><span class="tag tag-${d.status==='启用'?'green':'gray'}" style="margin:0;padding:1px 6px;">${d.status}</span></div>
</div>
</div>`).join('');
</div>`;}).join('');
}
function selectDish(idx){ currentDish=idx; renderDishList(); renderDetail(); }
@@ -507,16 +585,17 @@ function renderBasic(d){
function renderPricing(d){
var specRows = (d.specs||[]).map(function(s){
var statusHtml = s.isDefault ? '<span class="tag tag-blue" style="font-size:11px;">默认</span>' : '';
var cleanMargin = (s.cleanMargin!==undefined && s.cleanMargin!==null) ? parseFloat(s.cleanMargin).toFixed(1)+'%' : '—';
var cleanPrice = (s.cleanPrice!==undefined && s.cleanPrice!==null) ? parseFloat(s.cleanPrice).toFixed(2) : '—';
var cleanSaleActive = s.cleanSale === true;
var cleanSaleHtml = '<div class="sale-toggle'+(cleanSaleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(cleanSaleActive?'上架':'下架')+'</span></div>';
var gramText=s.baseGram?(parseFloat(s.baseGram)+'g'):'';
var specViewHtml='<div class="spec-cell-view"><span class="spec-cell-view__gram">'+gramText+'</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">'+(s.portion||'—')+'</span></div>';
return '<tr><td>'+specViewHtml+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
return '<tr><td>'+specViewHtml+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanMargin+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
}).join('');
return renderPricingSection(d)
+ '<div class="info-section"><div class="info-section__title">📐 餐品规格(共 '+(d.specs||[]).length+' 项)</div>'
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>'+specRows+'</tbody></table></div>';
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>餐品售价(元/份)</th><th>净菜包毛利率</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>'+specRows+'</tbody></table></div>';
}
/* 餐品定价分区(普通 info-section 样式,详情页只展示字段,成本核算仅在编辑/新增页) */
@@ -557,7 +636,15 @@ function renderPromoVideosSection(d){
}
function renderComposition(d){
const rows = (d.composition||[]).map(c=>`<tr><td>${c.name}</td><td>${c.netType}</td><td>${c.amount}</td><td>${c.pct}</td><td><span class="tag tag-${c.type==='主材'?'blue':c.type==='调料'?'orange':'gray'}">${c.type||'辅材'}</span></td><td>${c.allergen}</td><td>${c.note}</td></tr>`).join('');
const rows = (d.composition||[]).map(c=>{
/* 净菜类型:主名称(已含-分隔符) + 净菜规格别名(弱化样式) */
var netName = c.netName || c.netType || '—';
var netTypeHtml = c.netSpecAlias
? '<span class="net-type">'+netName+'</span><span class="net-type-alias">'+c.netSpecAlias+'</span>'
: '<span class="net-type">'+netName+'</span>';
var typeTag = c.type==='主材'?'blue':c.type==='调料'?'orange':'gray';
return '<tr><td>'+(c.code||'—')+'</td><td>'+(c.name||'—')+'</td><td><span class="tag tag-'+typeTag+'">'+(c.type||'辅材')+'</span></td><td>'+netTypeHtml+'</td><td>'+(c.amount||0)+'g</td></tr>';
}).join('');
return `<div class="composition-head">
<div class="composition-head__title">🧩 餐品构成</div>
<a class="link" href="din-dish-add.html?mode=edit&tab=1">编辑</a>
@@ -567,7 +654,7 @@ function renderComposition(d){
<div class="info-cell"><div class="info-cell__label">熟重</div><div class="info-cell__value">${(d.cookedWeight!==undefined&&d.cookedWeight!==null)?(d.cookedWeight+'g'):'—'}</div></div>
</div></div>
<div class="info-section"><div class="info-section__title">食材构成(共 ${(d.composition||[]).length} 项)</div>
<table class="info-table"><thead><tr><th>食材名称</th><th>净菜类型</th><th>用量(g/份)</th><th>占比(%)</th><th>食材类型</th><th>过敏原</th><th>备注</th></tr></thead><tbody>${rows}</tbody></table></div>
<table class="info-table"><thead><tr><th>食材编码</th><th>食材名称</th><th>食材类型</th><th>净菜类型</th><th>用量(g/份)</th></tr></thead><tbody>${rows}</tbody></table></div>
${renderNutriSection(d)}`;
}
@@ -717,9 +804,172 @@ function togglePromoVideo(el, dishIdx, videoIdx){
(function(){var a=document.querySelector('.sidebar-item.active');if(!a)return;var s=a.closest('.sidebar-subgroup');if(s)s.classList.add('open');})();
// 初始化
renderDishList();
renderDetail();
// 初始化(先建立 filteredDishes 再渲染列表/详情)
applyFilters();
/* ============ 搜索与筛选 ============ */
/* 收集快速搜索 + 高级搜索条件,刷新 filteredDishes 与汇总条 */
function applyFilters(){
var kw = (document.getElementById('dishQuickSearchInput')||{}).value || '';
kw = kw.trim().toLowerCase();
var cat = (document.getElementById('dishCategoryFilter')||{}).value || '';
var stat = (document.getElementById('dishStatusFilter')||{}).value || '';
/* 应用快速搜索条件 */
var quickFiltered = DISHES.filter(function(d,i){
var matchKw = !kw || (d.name||'').toLowerCase().indexOf(kw)>=0 || (d.id||'').toLowerCase().indexOf(kw)>=0;
var matchCat = !cat || cat==='全部分类' || d.category===cat;
var matchStat = !stat || stat==='全部状态' || d.status===stat;
return matchKw && matchCat && matchStat;
});
/* 再叠加高级搜索条件 */
var list = quickFiltered;
if(appliedAdvSearch){
list = quickFiltered.filter(function(d){
return matchAdvanced(d, appliedAdvSearch);
});
}
filteredDishes = list.map(function(d){
var idx = DISHES.indexOf(d);
return {dish:d, idx:idx};
});
/* 若当前选中项被筛掉,则自动选第一条 */
var stillIn = filteredDishes.some(function(e){return e.idx===currentDish;});
if(!stillIn && filteredDishes.length>0){
currentDish = filteredDishes[0].idx;
currentTab = 0;
}
/* 更新汇总条 */
updateFilterSummary();
/* 重新渲染 */
renderDishList();
renderDetail();
/* 更新底部计数 */
var counter = document.getElementById('dishListCount');
if(counter) counter.textContent = '共 '+filteredDishes.length+' 条';
/* 空态提示 */
var emptyHint = document.getElementById('dishListEmpty');
if(emptyHint){
emptyHint.style.display = filteredDishes.length===0 ? 'block' : 'none';
}
}
/* 单条餐品是否匹配高级搜索条件 */
function matchAdvanced(d, q){
if(q.name && (d.name||'').toLowerCase().indexOf(q.name.toLowerCase())<0) return false;
if(q.code && (d.id||'').toLowerCase().indexOf(q.code.toLowerCase())<0) return false;
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.status && d.status!==q.status) return false;
/* 食材成本区间 */
var cost = (d.pricing && d.pricing.ingredientCost!==undefined) ? d.pricing.ingredientCost : null;
if(cost===null) cost = 0;
if(q.costMin!==null && cost < q.costMin) return false;
if(q.costMax!==null && cost > q.costMax) return false;
/* 餐品售价区间:取规格中最低价/最高价 */
var prices = (d.specs||[]).map(function(s){return s.price;});
var minPrice = prices.length ? Math.min.apply(null, prices) : 0;
var maxPrice = prices.length ? Math.max.apply(null, prices) : 0;
if(q.priceMin!==null && maxPrice < q.priceMin) return false;
if(q.priceMax!==null && minPrice > q.priceMax) return false;
/* 时间区间(字符串比较即可,演示数据格式 YYYY-MM-DD) */
if(q.updatedStart && (d.updateTime||'') < q.updatedStart) return false;
if(q.updatedEnd && (d.updateTime||'') > q.updatedEnd) return false;
/* 创建时间演示数据缺失,留作扩展(命中即跳过比较) */
return true;
}
/* 更新筛选汇总条 */
function updateFilterSummary(){
var sumEl = document.getElementById('filterSummary');
if(!sumEl) return;
var bits = [];
if(appliedAdvSearch){
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:'状态',
costMin:'成本≥',costMax:'成本≤',priceMin:'售价≥',priceMax:'售价≤',
updatedStart:'更新≥',updatedEnd:'更新≤',createdStart:'创建≥',createdEnd:'创建≤'};
var label = labelMap[k] || k;
var suffix = (k.indexOf('Min')>=0 || k.indexOf('Max')>=0) ? (k.indexOf('cost')>=0?'元':(k.indexOf('price')>=0?'元':'')) : '';
bits.push(label + v + suffix);
});
}
if(bits.length===0){
sumEl.style.display = 'none';
sumEl.innerHTML = '';
} else {
sumEl.style.display = 'flex';
sumEl.innerHTML = '<span class="panel-left__filter-summary__text">已应用筛选:' + bits.join(' · ') + '</span>'
+ '<button class="panel-left__filter-summary__clear" onclick="clearAdvancedSearch()">✕ 清空</button>';
}
}
/* 快速搜索(输入框 + 分类/状态下拉触发) */
function onQuickSearchInput(){ applyFilters(); }
function onQuickSearch(){ applyFilters(); }
/* 高级搜索弹窗 */
function openAdvancedSearchDialog(){
/* 把已应用条件回填到表单 */
var q = appliedAdvSearch || {};
setVal('advName', q.name||'');
setVal('advCode', q.code||'');
setVal('advCategory', q.category||'');
setVal('advMeal', q.meal||'');
setVal('advTag', q.tag||'');
setVal('advStatus', q.status||'');
setVal('advCostMin', q.costMin!==null && q.costMin!==undefined ? q.costMin : '');
setVal('advCostMax', q.costMax!==null && q.costMax!==undefined ? q.costMax : '');
setVal('advPriceMin', q.priceMin!==null && q.priceMin!==undefined ? q.priceMin : '');
setVal('advPriceMax', q.priceMax!==null && q.priceMax!==undefined ? q.priceMax : '');
setVal('advUpdatedStart', q.updatedStart||'');
setVal('advUpdatedEnd', q.updatedEnd||'');
setVal('advCreatedStart', q.createdStart||'');
setVal('advCreatedEnd', q.createdEnd||'');
document.getElementById('advancedSearchDialog').classList.add('open');
}
function closeAdvancedSearchDialog(){
document.getElementById('advancedSearchDialog').classList.remove('open');
}
function resetAdvancedSearch(){
['advName','advCode','advCategory','advMeal','advTag','advStatus','advCostMin','advCostMax','advPriceMin','advPriceMax','advUpdatedStart','advUpdatedEnd','advCreatedStart','advCreatedEnd'].forEach(function(id){ setVal(id,''); });
}
function applyAdvancedSearch(){
appliedAdvSearch = {
name: getVal('advName'),
code: getVal('advCode'),
category: getVal('advCategory'),
meal: getVal('advMeal'),
tag: getVal('advTag'),
status: getVal('advStatus'),
costMin: getNum('advCostMin'),
costMax: getNum('advCostMax'),
priceMin: getNum('advPriceMin'),
priceMax: getNum('advPriceMax'),
updatedStart: getVal('advUpdatedStart'),
updatedEnd: getVal('advUpdatedEnd'),
createdStart: getVal('advCreatedStart'),
createdEnd: getVal('advCreatedEnd')
};
/* 全部空字段视为未设置 */
var hasAny = Object.keys(appliedAdvSearch).some(function(k){
var v = appliedAdvSearch[k];
return v!==null && v!==undefined && v!=='';
});
if(!hasAny) appliedAdvSearch = null;
closeAdvancedSearchDialog();
applyFilters();
}
function clearAdvancedSearch(){
appliedAdvSearch = null;
applyFilters();
}
function setVal(id, v){ var el = document.getElementById(id); if(el) el.value = v; }
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; }
/* ========== 跨页 specs 共享(阶段三任务 3.0) ========== */
/* 把 DISHES 数组序列化到 localStorage,供下游订单/组配/包装等页面读取 */
@@ -862,41 +1112,6 @@ function deleteDishCat(id){
}
</script>
<!-- 新增食材弹窗 -->
<div class="dialog-mask" id="ingredientDialog">
<div class="dialog">
<div class="dialog__header">
<div class="dialog__title">新增食材</div>
<button class="dialog__close" onclick="closeIngredientDialog()">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
<div class="dialog__body">
<div class="form-row">
<div class="form-group"><label>食材名称<span class="required">*</span></label><input class="form-input" type="text" placeholder="请输入食材名称"></div>
<div class="form-group"><label>净菜类型<span class="required">*</span></label><select class="form-select"><option>请选择</option><option>土豆丝</option><option>土豆块</option><option>土豆泥</option><option>番茄块</option><option>蒜末</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>用量(g/份)<span class="required">*</span></label><input class="form-input" type="number" placeholder="请输入用量"></div>
<div class="form-group"><label>加工状态</label><select class="form-select"><option>请选择</option><option>原料</option><option>净菜</option><option>半成品</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>食材类型</label><select class="form-select"><option>辅材</option><option>主材</option><option>调料</option></select></div>
<div class="form-group"><label>过敏原</label><input class="form-input" type="text" placeholder="无则填无"></div>
</div>
<div class="form-group"><label>备注</label><input class="form-input" type="text" placeholder="请输入备注信息"></div>
</div>
<div class="dialog__footer">
<button class="btn btn-default" onclick="closeIngredientDialog()">取消</button>
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
<script>
function openIngredientDialog(){document.getElementById('ingredientDialog').classList.add('open');}
function closeIngredientDialog(){document.getElementById('ingredientDialog').classList.remove('open');}
</script>
<!-- 餐品分类管理弹窗 -->
<div class="dialog-mask" id="dishCategoryDialog">
<div class="dialog" style="width:820px;">
+411 -66
View File
@@ -73,6 +73,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.panel-left__search input:focus{border-color:var(--primary);}
.panel-left__filter{display:flex;gap:8px;}
.panel-left__filter select{height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 24px 0 10px;font-size:var(--font-xs);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;flex:1;}
/* 高级搜索筛选条件汇总条(显示已应用的筛选条件,可一键清空) */
.panel-left__filter-summary{margin-top:8px;padding:6px 10px;background:var(--primary-light);border:1px solid #BAE7FF;border-radius:var(--radius-sm);font-size:var(--font-xs);color:var(--primary);display:flex;align-items:center;justify-content:space-between;gap:8px;}
.panel-left__filter-summary__text{flex:1;line-height:1.4;}
.panel-left__filter-summary__clear{flex-shrink:0;cursor:pointer;color:var(--danger);font-size:var(--font-xs);background:none;border:none;padding:0 4px;}
.panel-left__filter-summary__clear:hover{text-decoration:underline;}
.panel-left__actions{display:flex;gap:8px;padding:10px 20px;border-bottom:1px solid var(--border);flex-wrap:wrap;}
.panel-left__actions .btn{height:30px;font-size:var(--font-xs);padding:0 10px;}
.food-list{flex:1;overflow-y:auto;padding:4px 0;}
@@ -111,6 +116,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
/* 餐品构成 Tab 顶部标题栏(含编辑按钮,控制整个 Tab 区域) */
.composition-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;}
.composition-head__title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);}
/* 净菜类型:主名称 + 规格别名(弱化) */
.net-type{font-size:var(--font-sm);color:var(--text-primary);}
.net-type-alias{margin-left:6px;font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;}
.info-table{width:100%;border-collapse:collapse;font-size:var(--font-sm);}
.info-table th{background:#FAFAFA;padding:10px 12px;text-align:left;color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);}
.info-table td{padding:10px 12px;border-bottom:1px solid var(--border);color:var(--text-primary);}
@@ -157,6 +165,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.form-input:focus,.form-select:focus{border-color:var(--primary);}
.form-select{appearance: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 10px center;padding-right:28px;}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;}
/* 高级搜索表单网格(2 列) + 区间输入(双 input 用 ~ 分隔) */
.adv-search-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 18px;}
.adv-search-grid .form-group{margin-bottom:0;}
.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;}
/* 操作列固定右侧 */
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;}
@@ -228,6 +242,38 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
.page-header-bar__right{display:flex;align-items:center;gap:12px;}
.page-header-bar__right label{font-size:var(--font-sm);color:var(--text-secondary);white-space:nowrap;}
.page-header-bar__right .canteen-select{height:36px;border:2px solid var(--primary);border-radius:var(--radius-sm);padding:0 32px 0 14px;font-size:var(--font-sm);color:var(--primary);font-weight:500;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='%231890FF'/%3E%3C/svg%3E") no-repeat right 12px center;appearance:none;cursor:pointer;min-width:140px;}
/* 操作历史 */
.history-filter{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;}
.history-filter select,.history-filter input[type="text"]{height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 10px;font-size:var(--font-xs);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;}
.history-filter select:focus,.history-filter input[type="text"]:focus{border-color:var(--primary);}
.history-filter input[type="text"]{padding-right:10px;background:#fff;appearance:auto;cursor:text;}
.history-filter .btn{height:32px;font-size:var(--font-xs);padding:0 12px;}
/* 时间轴 */
.history-timeline{position:relative;padding-left:20px;}
.history-timeline::before{content:'';position:absolute;left:5px;top:6px;bottom:6px;width:2px;background:#E8E8E8;}
.history-item{position:relative;padding:0 0 18px 24px;}
.history-item:last-child{padding-bottom:0;}
.history-item::before{content:'';position:absolute;left:-19px;top:4px;width:12px;height:12px;border-radius:50%;background:#fff;border:2px solid var(--primary);box-shadow:0 0 0 3px #E6F7FF;}
.history-item--create::before{border-color:var(--success);box-shadow:0 0 0 3px #F6FFED;}
.history-item--edit::before{border-color:var(--primary);box-shadow:0 0 0 3px #E6F7FF;}
.history-item--disable::before{border-color:var(--text-placeholder);box-shadow:0 0 0 3px #F5F5F5;}
.history-item--price::before{border-color:var(--warning);box-shadow:0 0 0 3px #FFF7E6;}
.history-item--delete::before{border-color:var(--danger);box-shadow:0 0 0 3px #FFF1F0;}
.history-item--restore::before{border-color:#722ED1;box-shadow:0 0 0 3px #F9F0FF;}
.history-card{background:#FAFBFC;border:1px solid #F0F0F0;border-radius:var(--radius-sm);padding:12px 16px;}
.history-card__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px;}
.history-card__time{font-size:var(--font-xs);color:var(--text-placeholder);margin-left:auto;}
.history-card__op{font-size:var(--font-sm);font-weight:500;color:var(--text-primary);}
.history-card__user{font-size:var(--font-xs);color:var(--text-secondary);display:flex;align-items:center;gap:4px;}
.history-card__summary{font-size:var(--font-sm);color:var(--text-secondary);line-height:1.6;margin-top:4px;}
.history-card__changes{margin-top:8px;background:#fff;border:1px solid #F0F0F0;border-radius:var(--radius-sm);overflow:hidden;}
.history-change{display:grid;grid-template-columns:120px 1fr 1fr;align-items:center;font-size:var(--font-xs);border-bottom:1px solid #F5F5F5;}
.history-change:last-child{border-bottom:none;}
.history-change__field{padding:6px 12px;color:var(--text-secondary);background:#FAFAFA;border-right:1px solid #F0F0F0;}
.history-change__old{padding:6px 12px;color:var(--text-placeholder);text-decoration:line-through;border-right:1px dashed #E8E8E8;}
.history-change__new{padding:6px 12px;color:var(--text-primary);font-weight:500;}
.history-empty{text-align:center;padding:40px 0;color:var(--text-placeholder);font-size:var(--font-sm);}
</style>
</head>
<body>
@@ -346,13 +392,15 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<div class="panel-left__header">
<div class="panel-left__title">餐品列表</div>
<div class="panel-left__search">
<input type="text" placeholder="搜索餐品名称/编号...">
<button class="btn btn-primary" style="height:32px;padding:0 12px;font-size:var(--font-xs);">搜索</button>
<input type="text" id="dishQuickSearchInput" placeholder="搜索餐品名称/编号..." oninput="onQuickSearchInput(this.value)">
<button class="btn btn-primary" style="height:32px;padding:0 12px;font-size:var(--font-xs);" onclick="onQuickSearch()">搜索</button>
<button class="btn btn-default" style="height:32px;padding:0 10px;font-size:var(--font-xs);" onclick="openAdvancedSearchDialog()">⚙ 高级搜索</button>
</div>
<div class="panel-left__filter">
<select><option>全部分类</option><option>主食</option><option>荤菜</option><option>素菜</option><option>汤品</option><option>小吃</option></select>
<select><option>全部状态</option><option>启用</option><option>停用</option></select>
<select id="dishCategoryFilter" onchange="onQuickSearch()"><option>全部分类</option><option>主食</option><option>荤菜</option><option>素菜</option><option>汤品</option><option>小吃</option></select>
<select id="dishStatusFilter" onchange="onQuickSearch()"><option>全部状态</option><option>启用</option><option>停用</option></select>
</div>
<div class="panel-left__filter-summary" id="filterSummary" style="display:none;"></div>
</div>
<div class="panel-left__actions">
<a href="din-dish-add.html" class="btn btn-primary"> 新增餐品</a>
@@ -360,8 +408,9 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<button class="btn btn-default" onclick="openDishCategoryDialog()">📂 餐品分类管理</button>
</div>
<div class="food-list" id="dishList"></div>
<div id="dishListEmpty" style="display:none;text-align:center;padding:40px 20px;color:var(--text-placeholder);font-size:var(--font-sm);">未找到匹配的餐品<br><span style="font-size:var(--font-xs);">请调整搜索条件或 <span class="link" onclick="clearAdvancedSearch()">清空筛选</span></span></div>
<div class="panel-left__footer">
<span>486</span>
<span id="dishListCount">0</span>
<div style="display:flex;gap:4px;">
<button class="page-btn"></button><button class="page-btn active">1</button><button class="page-btn">2</button><button class="page-btn">3</button><button class="page-btn"></button>
</div>
@@ -384,6 +433,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<div class="detail-tab" onclick="switchDetailTab(4)">👅 味道档案</div>
<div class="detail-tab" onclick="switchDetailTab(5)">🫦 口感档案</div>
<div class="detail-tab" onclick="switchDetailTab(6)">🎨 色泽档案</div>
<div class="detail-tab" onclick="switchDetailTab(7)">🕘 操作历史</div>
</div>
</div>
<div class="panel-right__body" id="detailBody"></div>
@@ -391,78 +441,168 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
</div>
</main>
<!-- 高级搜索弹窗 -->
<div class="dialog-mask" id="advancedSearchDialog">
<div class="dialog" style="width:720px;">
<div class="dialog__header">
<div class="dialog__title">⚙ 高级搜索</div>
<button class="dialog__close" onclick="closeAdvancedSearchDialog()"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
</div>
<div class="dialog__body">
<div class="adv-search-grid">
<div class="form-group"><label>餐品名称</label><input class="form-input" id="advName" placeholder="支持模糊匹配"></div>
<div class="form-group"><label>餐品编号</label><input class="form-input" id="advCode" placeholder="如 DISH001"></div>
<div class="form-group"><label>餐品分类</label>
<select class="form-select" id="advCategory">
<option value="">全部</option>
<option>主食</option><option>荤菜</option><option>素菜</option><option>汤品</option><option>小吃</option>
</select>
</div>
<div class="form-group"><label>适用餐次</label>
<select class="form-select" id="advMeal">
<option value="">全部</option>
<option>早餐</option><option>午餐</option><option>晚餐</option><option>早/午</option><option>午/晚</option><option>早/午/晚</option>
</select>
</div>
<div class="form-group"><label>餐品标签</label>
<select class="form-select" id="advTag">
<option value="">全部</option>
<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"><label>上架状态</label>
<select class="form-select" id="advStatus">
<option value="">全部</option>
<option>启用</option><option>停用</option>
</select>
</div>
<div class="form-group"><label>食材成本区间(元/100g)</label>
<div class="adv-range"><input class="form-input" id="advCostMin" type="number" step="0.01" min="0" placeholder="最低"><span class="adv-range__sep">~</span><input class="form-input" id="advCostMax" type="number" step="0.01" min="0" placeholder="最高"></div>
</div>
<div class="form-group"><label>餐品售价区间(元/份)</label>
<div class="adv-range"><input class="form-input" id="advPriceMin" type="number" step="0.01" min="0" placeholder="最低"><span class="adv-range__sep">~</span><input class="form-input" id="advPriceMax" type="number" step="0.01" min="0" placeholder="最高"></div>
</div>
<div class="form-group"><label>创建时间</label>
<div class="adv-range"><input class="form-input" id="advCreatedStart" type="date"><span class="adv-range__sep">~</span><input class="form-input" id="advCreatedEnd" type="date"></div>
</div>
<div class="form-group"><label>更新时间</label>
<div class="adv-range"><input class="form-input" id="advUpdatedStart" type="date"><span class="adv-range__sep">~</span><input class="form-input" id="advUpdatedEnd" type="date"></div>
</div>
</div>
</div>
<div class="dialog__footer">
<button class="btn btn-default" onclick="resetAdvancedSearch()">重置</button>
<button class="btn btn-default" onclick="closeAdvancedSearchDialog()">取消</button>
<button class="btn btn-primary" onclick="applyAdvancedSearch()">应用筛选</button>
</div>
</div>
</div>
<script>
// 餐品数据
const DISHES = [
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanPrice:6,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,cleanMargin:55.0,cleanPrice:9,cleanSale:false,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,cleanMargin:50.0,cleanPrice:6,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:18,cleanMargin:58.0,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',updateTime:'2026-08-10',
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
composition:[{name:'猪五花肉',netType:'红烧块',amount:160,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'生姜',netType:'姜片',amount:5,pct:'2.5%',state:'净菜',type:'辅材',allergen:'无',note:'—'},{name:'生抽',netType:'调味汁',amount:15,pct:'7.5%',state:'原料',type:'调料',allergen:'含麸质',note:'酱油'},{name:'冰糖',netType:'糖浆',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'上色提味'},{name:'料酒',netType:'料酒汁',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'去腥'}],
composition:[{code:'YQ-001',name:'猪五花肉',netName:'红烧块',amount:160,type:'主材'},{code:'YQ-015',name:'生姜',netName:'姜片',amount:5,type:'辅材'},{code:'YQ-017',name:'生抽',netName:'调味汁',amount:15,type:'调料'},{code:'YQ-023',name:'冰糖',netName:'糖浆',amount:10,type:'调料'},{code:'YQ-020',name:'料酒',netName:'料酒汁',amount:10,type:'调料'}],
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:0.8,mineral:42,fiber:0.3,sodium:486,calcium:18,iron:2.4,zinc:1.8,potassium:280,cholesterol:88,vitA:14,vitE:0.6,vitB1:0.22,vitB2:0.12,magnesium:20,phosphorus:160,selenium:18,copper:0.12,manganese:0.05,nutriGrade:'偏差',note:'铁2.4mg、锌1.8mg'},
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,temp:'100→80',steps:6},
taste:{sour:1,sweet:3,bitter:1,spicy:1,salty:4,smell:4.2,flavorScore:3.8,flavorGrade:'良好'},
color:{mainColor:'红褐色',uniformity:4,gloss:4,freshness:4,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'良好'}
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'良好'},
history:[
{type:'新增',title:'创建餐品',time:'2026-05-10 09:30',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'手动添加「红烧肉」餐品基础档案,初始化基本档案与定价信息。'},
{type:'编辑',title:'完善餐品构成',time:'2026-05-22 14:15',user:'李建国',role:'营养师',dept:'营养管理部',summary:'新增冰糖、料酒两味调料,补全红烧调味体系。',changes:[{field:'餐品构成',old:'3 项',new:'5 项'},{field:'烹饪工艺-步骤数',old:'4 步',new:'6 步'}]},
{type:'价格调整',title:'更新餐品定价',time:'2026-06-08 11:00',user:'张明',role:'成本核算员',dept:'财务部',summary:'食材成本上调,重新核算标准份售价。',changes:[{field:'标准份售价',old:'10 元',new:'12 元'},{field:'食材成本',old:'4.20 元',new:'4.80 元'}]},
{type:'编辑',title:'补充宣传视频',time:'2026-07-05 16:40',user:'陈晓燕',role:'运营员',dept:'运营部',summary:'新增「红烧肉宣传片」与「制作工艺展示」两条视频素材。',changes:[{field:'宣传视频',old:'0 条',new:'2 条'}]},
{type:'编辑',title:'修订色泽档案',time:'2026-08-10 10:25',user:'李建国',role:'营养师',dept:'营养管理部',summary:'色泽评分根据试菜反馈微调,综合评级维持良好。',changes:[{field:'卖相评分',old:'3.5',new:'4'},{field:'综合色泽评分',old:'3.8',new:'4.0'}]}
]
},
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,cleanMargin:60.0,cleanPrice:4.5,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,cleanMargin:55.0,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',updateTime:'2026-08-09',
pricing:{basePrice:'3.00',ingredientCost:1.80,per100gCost:1.20,profitRate:150,suggestedPrice:'3.00'},
promoVideos:[{title:'清炒时蔬宣传片',duration:'45秒',size:'18MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
composition:[{name:'时令青菜',netType:'青菜段',amount:120,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'蒜',netType:'蒜末',amount:5,pct:'3.3%',state:'净菜',type:'调料',allergen:'无',note:'—'},{name:'食用油',netType:'净油',amount:8,pct:'5.3%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'1.3%',state:'原料',type:'调料',allergen:'无',note:'—'}],
composition:[{code:'YQ-024',name:'时令青菜',netName:'青菜段',netSpecAlias:'长段',amount:120,type:'主材'},{code:'YQ-016',name:'蒜',netName:'蒜末',netSpecAlias:'细末',amount:5,type:'调料'},{code:'YQ-014',name:'食用油',netName:'净油',amount:8,type:'调料'},{code:'YQ-018',name:'食盐',netName:'盐粒',amount:2,type:'调料'}],
nutri:{calorie:68,protein:2.1,carb:8.6,fat:3.2,vitC:28.5,mineral:86,fiber:2.4,sodium:186,calcium:45,iron:1.2,zinc:0.4,potassium:280,cholesterol:0,vitA:268,vitE:0.9,vitB1:0.04,vitB2:0.06,magnesium:24,phosphorus:42,selenium:1.2,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'铁1.2mg、钙45mg'},
cook:{mainMethod:'炒',subMethod:'—',heat:'大火',duration:5,temp:'200',steps:4},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:2,smell:3.5,flavorScore:3.2,flavorGrade:'一般'},
color:{mainColor:'翠绿色',uniformity:5,gloss:4,freshness:5,appearance:4,colorScore:4.5,colorGrade:'优秀'},
texture:{hardness:3,sticky:1,crispy:4,smooth:3,elastic:2,textureScore:3.4,textureGrade:'良好'}
texture:{hardness:3,sticky:1,crispy:4,smooth:3,elastic:2,textureScore:3.4,textureGrade:'良好'},
history:[
{type:'新增',title:'创建餐品',time:'2026-04-18 10:00',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'从系统内置餐品库导入「清炒时蔬」。'},
{type:'编辑',title:'扩充餐品规格',time:'2026-05-30 15:20',user:'李建国',role:'营养师',dept:'营养管理部',summary:'新增「小份」规格,满足小胃口就餐需求。',changes:[{field:'餐品规格',old:'1 项(标准份)',new:'2 项(标准份/小份)'}]},
{type:'价格调整',title:'更新餐品定价',time:'2026-07-12 09:45',user:'张明',role:'成本核算员',dept:'财务部',summary:'夏季时蔬成本下降,标准份售价下调。',changes:[{field:'标准份售价',old:'8 元',new:'6 元'}]}
]
},
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,cleanMargin:42.0,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',updateTime:'2026-08-08',
pricing:{basePrice:'1.67',ingredientCost:1.20,per100gCost:0.40,profitRate:150,suggestedPrice:'1.00'},
promoVideos:[{title:'番茄鸡蛋汤宣传片',duration:'1分钟',size:'24MB',enabled:true},{title:'家常做法教程',duration:'3分钟',size:'80MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
composition:[{name:'番茄',netType:'番茄',amount:150,pct:'50%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'鸡蛋',netType:'鸡蛋液',amount:60,pct:'20%',state:'原料',type:'主材',allergen:'含蛋类',note:'—'},{name:'食用油',netType:'净油',amount:5,pct:'1.7%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'0.7%',state:'原料',type:'调料',allergen:'无',note:'—'}],
composition:[{code:'YQ-006',name:'番茄',netName:'番茄块',amount:150,type:'主材'},{code:'YQ-005',name:'鸡蛋',netName:'鸡蛋液',amount:60,type:'主材'},{code:'YQ-014',name:'食用油',netName:'净油',amount:5,type:'调料'},{code:'YQ-018',name:'食盐',netName:'盐粒',amount:2,type:'调料'}],
nutri:{calorie:45,protein:3.8,carb:4.2,fat:2.1,vitC:18.2,mineral:32,fiber:1.1,sodium:280,calcium:18,iron:0.8,zinc:0.4,potassium:210,cholesterol:110,vitA:62,vitE:0.5,vitB1:0.04,vitB2:0.12,magnesium:12,phosphorus:80,selenium:8,copper:0.05,manganese:0.06,nutriGrade:'达标',note:'铁0.8mg、钾210mg'},
cook:{mainMethod:'煮',subMethod:'炒',heat:'中火',duration:12,temp:'100',steps:5},
taste:{sour:3,sweet:2,bitter:1,spicy:1,salty:2,smell:3.8,flavorScore:3.5,flavorGrade:'良好'},
color:{mainColor:'橙红色',uniformity:4,gloss:3,freshness:5,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:1,sticky:2,crispy:1,smooth:5,elastic:2,textureScore:3.8,textureGrade:'良好'}
texture:{hardness:1,sticky:2,crispy:1,smooth:5,elastic:2,textureScore:3.8,textureGrade:'良好'},
history:[
{type:'新增',title:'创建餐品',time:'2026-03-25 09:00',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'从系统内置餐品库导入「番茄鸡蛋汤」。'},
{type:'编辑',title:'补充宣传视频',time:'2026-05-14 14:30',user:'陈晓燕',role:'运营员',dept:'运营部',summary:'新增宣传片与家常做法教程两条视频。',changes:[{field:'宣传视频',old:'0 条',new:'2 条'}]},
{type:'编辑',title:'修订味道档案',time:'2026-07-20 11:10',user:'李建国',role:'营养师',dept:'营养管理部',summary:'根据试菜反馈调整酸味评分。',changes:[{field:'酸味(1-5)',old:'2',new:'3'},{field:'味道评分',old:'3.2',new:'3.5'}]}
]
},
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,cleanPrice:2,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,cleanPrice:1.5,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,cleanMargin:38.0,cleanPrice:2,cleanSale:false,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,cleanMargin:35.0,cleanPrice:1.5,cleanSale:false},{portion:'大份',baseGram:300,unit:'份',price:4,cleanMargin:40.0,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',updateTime:'2026-07-22',
pricing:{basePrice:'1.50',ingredientCost:1.00,per100gCost:0.50,profitRate:150,suggestedPrice:'1.25'},
promoVideos:[],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
composition:[{name:'大米',netType:'熟米饭',amount:100,pct:'100%',state:'原料',type:'主材',allergen:'无',note:'—'}],
composition:[{code:'YQ-011',name:'大米',netName:'熟米饭',amount:100,type:'主材'}],
nutri:{calorie:232,protein:4.8,carb:51.2,fat:0.6,vitC:0,mineral:12,fiber:0.4,sodium:3,calcium:10,iron:0.5,zinc:0.8,potassium:43,cholesterol:0,vitA:0,vitE:0.1,vitB1:0.02,vitB2:0.02,magnesium:12,phosphorus:43,selenium:1.5,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'—'},
cook:{mainMethod:'蒸',subMethod:'—',heat:'大火→中火',duration:30,temp:'100',steps:2},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:1,smell:3.0,flavorScore:2.8,flavorGrade:'一般'},
color:{mainColor:'乳白色',uniformity:5,gloss:4,freshness:4,appearance:4,colorScore:4.2,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:3,elastic:2,textureScore:3.2,textureGrade:'一般'}
texture:{hardness:2,sticky:3,crispy:1,smooth:3,elastic:2,textureScore:3.2,textureGrade:'一般'},
history:[
{type:'新增',title:'创建餐品',time:'2026-02-01 09:00',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'从系统内置餐品库导入「米饭」,作为基础主食。'},
{type:'编辑',title:'扩充餐品规格',time:'2026-04-10 14:00',user:'李建国',role:'营养师',dept:'营养管理部',summary:'新增「小份」「大份」规格,适应不同食量。',changes:[{field:'餐品规格',old:'1 项(标准份)',new:'3 项(小份/标准份/大份)'}]},
{type:'价格调整',title:'更新餐品定价',time:'2026-07-22 10:30',user:'张明',role:'成本核算员',dept:'财务部',summary:'粳米定价微调,标准份售价更新。',changes:[{field:'标准份售价',old:'2 元',new:'3 元'}]}
]
},
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,cleanMargin:62.0,cleanPrice:10,cleanSale:false,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,cleanMargin:65.0,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',updateTime:'2026-08-05',
pricing:{basePrice:'7.78',ingredientCost:5.20,per100gCost:2.89,profitRate:170,suggestedPrice:'7.80'},
promoVideos:[{title:'糖醋里脊宣传片',duration:'2分钟',size:'58MB',enabled:true},{title:'正宗做法揭秘',duration:'5分钟',size:'150MB',enabled:true},{title:'出品摆盘展示',duration:'30秒',size:'12MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
composition:[{name:'猪里脊肉',netType:'里脊',amount:120,pct:'66.7%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'淀粉',netType:'淀粉糊',amount:20,pct:'11.1%',state:'原料',type:'辅材',allergen:'无',note:'挂糊'},{name:'白糖',netType:'糖浆',amount:25,pct:'13.9%',state:'料',type:'调料',allergen:'无',note:'糖醋汁'},{name:'食醋',netType:'调味汁',amount:15,pct:'8.3%',state:'原料',type:'调料',allergen:'无',note:'糖醋汁'}],
composition:[{code:'YQ-002',name:'里脊',netName:'里脊丝',netSpecAlias:'细丝',amount:120,type:'主材'},{code:'YQ-022',name:'淀粉',netName:'淀粉糊',amount:20,type:'辅材'},{code:'YQ-019',name:'白糖',netName:'糖浆',amount:25,type:'料'},{code:'YQ-021',name:'食醋',netName:'调味汁',amount:15,type:'调料'}],
nutri:{calorie:420,protein:22.4,carb:38.2,fat:18.6,vitC:1.2,mineral:28,fiber:0.6,sodium:620,calcium:22,iron:1.8,zinc:1.6,potassium:340,cholesterol:75,vitA:8,vitE:0.4,vitB1:0.45,vitB2:0.18,magnesium:24,phosphorus:200,selenium:14,copper:0.1,manganese:0.08,nutriGrade:'未达标',note:'铁1.8mg'},
cook:{mainMethod:'炸',subMethod:'炒',heat:'大火',duration:15,temp:'180',steps:7},
taste:{sour:4,sweet:4,bitter:1,spicy:1,salty:3,smell:4.0,flavorScore:4.2,flavorGrade:'良好'},
color:{mainColor:'金黄色',uniformity:4,gloss:5,freshness:4,appearance:5,colorScore:4.5,colorGrade:'优秀'},
texture:{hardness:3,sticky:1,crispy:5,smooth:4,elastic:3,textureScore:4.0,textureGrade:'良好'}
texture:{hardness:3,sticky:1,crispy:5,smooth:4,elastic:3,textureScore:4.0,textureGrade:'良好'},
history:[
{type:'新增',title:'创建餐品',time:'2026-04-25 10:30',user:'味养创新',role:'AI 推荐',dept:'味养创新中心',summary:'味养创新模块推演生成「糖醋里脊」餐品并入库。'},
{type:'编辑',title:'完善口感档案',time:'2026-05-18 14:50',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'根据实际出品反馈调整脆度评分。',changes:[{field:'脆度(1-5)',old:'4',new:'5'},{field:'口感评分',old:'3.8',new:'4.0'}]},
{type:'价格调整',title:'更新餐品定价',time:'2026-06-20 11:15',user:'张明',role:'成本核算员',dept:'财务部',summary:'里脊肉成本上调,标准份售价同步调整。',changes:[{field:'标准份售价',old:'12 元',new:'14 元'},{field:'大份售价',old:'18 元',new:'20 元'}]},
{type:'编辑',title:'补充宣传视频',time:'2026-07-08 16:00',user:'陈晓燕',role:'运营员',dept:'运营部',summary:'新增三条宣传视频素材。',changes:[{field:'宣传视频',old:'0 条',new:'3 条'}]},
{type:'停用',title:'停用餐品',time:'2026-08-05 09:20',user:'李建国',role:'营养师',dept:'营养管理部',summary:'高糖高油,营养指标未达标,根据健康饮食指引暂停供应。',changes:[{field:'餐品状态',old:'启用',new:'停用'}]}
]
}
];
let currentDish = 0;
let currentTab = 0;
/* 列表渲染基于 filteredDishes(由快速搜索 / 高级搜索筛选产生),默认全量 */
let filteredDishes = [];
/* 高级搜索已应用条件(用于汇总条显示) */
let appliedAdvSearch = null;
function renderDishList(){
document.getElementById('dishList').innerHTML = DISHES.map((d,i)=>`
document.getElementById('dishList').innerHTML = filteredDishes.map((entry)=>{const d=entry.dish;const i=entry.idx;return `
<div class="food-item${i===currentDish?' active':''}" onclick="selectDish(${i})">
<div class="food-item__icon">${d.icon}</div>
<div class="food-item__info">
<div class="food-item__name">${d.name}</div>
<div class="food-item__meta"><span>${d.id}</span><span>${d.category}</span><span class="tag tag-${d.status==='启用'?'green':'gray'}" style="margin:0;padding:1px 6px;">${d.status}</span></div>
<div class="food-item__meta"><span>${d.updateTime||'—'}</span><span>${d.category}</span><span class="tag tag-${d.status==='启用'?'green':'gray'}" style="margin:0;padding:1px 6px;">${d.status}</span></div>
</div>
</div>`).join('');
</div>`;}).join('');
}
function selectDish(idx){ currentDish=idx; renderDishList(); renderDetail(); }
@@ -481,7 +621,7 @@ function renderDetail(){
const s = document.getElementById('detailStatus');
s.textContent = d.status; s.className='tag tag-'+(d.status==='启用'?'green':'gray');
const body = document.getElementById('detailBody');
const renderers = [renderBasic,renderComposition,renderPricing,renderCook,renderTaste,renderTexture,renderColor];
const renderers = [renderBasic,renderComposition,renderPricing,renderCook,renderTaste,renderTexture,renderColor,renderHistory];
body.innerHTML = renderers[currentTab](d);
}
@@ -530,16 +670,17 @@ function renderBasic(d){
function renderPricing(d){
var specRows = (d.specs||[]).map(function(s){
var statusHtml = s.isDefault ? '<span class="tag tag-blue" style="font-size:11px;">默认</span>' : '';
var cleanMargin = (s.cleanMargin!==undefined && s.cleanMargin!==null) ? parseFloat(s.cleanMargin).toFixed(1)+'%' : '—';
var cleanPrice = (s.cleanPrice!==undefined && s.cleanPrice!==null) ? parseFloat(s.cleanPrice).toFixed(2) : '—';
var cleanSaleActive = s.cleanSale === true;
var cleanSaleHtml = '<div class="sale-toggle'+(cleanSaleActive?' active':'')+'" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">'+(cleanSaleActive?'上架':'下架')+'</span></div>';
var gramText=s.baseGram?(parseFloat(s.baseGram)+'g'):'';
var specViewHtml='<div class="spec-cell-view"><span class="spec-cell-view__gram">'+gramText+'</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">'+(s.portion||'—')+'</span></div>';
return '<tr><td>'+specViewHtml+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
return '<tr><td>'+specViewHtml+'</td><td>'+s.price.toFixed(2)+'</td><td>'+cleanMargin+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
}).join('');
return renderPricingSection(d)
+ '<div class="info-section"><div class="info-section__title">📐 餐品规格(共 '+(d.specs||[]).length+' 项)</div>'
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>'+specRows+'</tbody></table></div>';
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>餐品售价(元/份)</th><th>净菜包毛利率</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>'+specRows+'</tbody></table></div>';
}
/* 餐品定价分区(普通 info-section 样式,详情页只展示字段,成本核算仅在编辑/新增页) */
@@ -580,7 +721,15 @@ function renderPromoVideosSection(d){
}
function renderComposition(d){
const rows = (d.composition||[]).map(c=>`<tr><td>${c.name}</td><td>${c.netType}</td><td>${c.amount}</td><td>${c.pct}</td><td><span class="tag tag-${c.type==='主材'?'blue':c.type==='调料'?'orange':'gray'}">${c.type||'辅材'}</span></td><td>${c.allergen}</td><td>${c.note}</td></tr>`).join('');
const rows = (d.composition||[]).map(c=>{
/* 净菜类型:主名称(已含-分隔符) + 净菜规格别名(弱化样式) */
var netName = c.netName || c.netType || '—';
var netTypeHtml = c.netSpecAlias
? '<span class="net-type">'+netName+'</span><span class="net-type-alias">'+c.netSpecAlias+'</span>'
: '<span class="net-type">'+netName+'</span>';
var typeTag = c.type==='主材'?'blue':c.type==='调料'?'orange':'gray';
return '<tr><td>'+(c.code||'—')+'</td><td>'+(c.name||'—')+'</td><td><span class="tag tag-'+typeTag+'">'+(c.type||'辅材')+'</span></td><td>'+netTypeHtml+'</td><td>'+(c.amount||0)+'g</td></tr>';
}).join('');
return `<div class="composition-head">
<div class="composition-head__title">🧩 餐品构成</div>
<a class="link" href="din-dish-add.html?mode=edit&tab=1">编辑</a>
@@ -590,7 +739,7 @@ function renderComposition(d){
<div class="info-cell"><div class="info-cell__label">熟重</div><div class="info-cell__value">${(d.cookedWeight!==undefined&&d.cookedWeight!==null)?(d.cookedWeight+'g'):'—'}</div></div>
</div></div>
<div class="info-section"><div class="info-section__title">食材构成(共 ${(d.composition||[]).length} 项)</div>
<table class="info-table"><thead><tr><th>食材名称</th><th>净菜类型</th><th>用量(g/份)</th><th>占比(%)</th><th>食材类型</th><th>过敏原</th><th>备注</th></tr></thead><tbody>${rows}</tbody></table></div>
<table class="info-table"><thead><tr><th>食材编码</th><th>食材名称</th><th>食材类型</th><th>净菜类型</th><th>用量(g/份)</th></tr></thead><tbody>${rows}</tbody></table></div>
${renderNutriSection(d)}`;
}
@@ -672,6 +821,74 @@ function renderColor(d){
</div></div>`;
}
/* === 操作历史 === */
// 操作类型标签配色
function historyOpTag(type){
const map={
'新增':{cls:'tag-green',icon:''},
'编辑':{cls:'tag-blue',icon:'✏️'},
'启用':{cls:'tag-green',icon:'✓'},
'停用':{cls:'tag-gray',icon:'⏸'},
'价格调整':{cls:'tag-orange',icon:'💰'},
'删除':{cls:'tag-red',icon:'🗑'},
'恢复':{cls:'',icon:'♻',style:'background:#F9F0FF;color:#722ED1;'}
};
const m = map[type] || {cls:'tag-gray',icon:'•'};
const styleAttr = m.style ? ` style="${m.style}"` : '';
return `<span class="tag ${m.cls}"${styleAttr}>${m.icon} ${type}</span>`;
}
// 渲染操作历史:筛选条 + 时间轴(只读视图,无编辑/删除入口)
function renderHistory(d){
const list = d.history || [];
let html = `<div class="info-section"><div class="info-section__title">操作历史</div>`;
// 筛选条
html += `<div class="history-filter">
<select><option>全部操作</option><option>新增</option><option>编辑</option><option>启用</option><option>停用</option><option>价格调整</option><option>删除</option><option>恢复</option></select>
<input type="text" placeholder="操作时间范围(YYYY-MM-DD ~ YYYY-MM-DD)" style="flex:1;min-width:260px;">
<input type="text" placeholder="操作人" style="width:140px;">
<button class="btn btn-primary">查询</button>
<button class="btn btn-default">重置</button>
</div>`;
// 时间轴
if(list.length===0){
html += `<div class="history-empty">该餐品暂无操作历史记录</div>`;
} else {
html += `<div class="history-timeline">`;
list.forEach(function(h){
const opClassMap={'新增':'create','编辑':'edit','启用':'edit','停用':'disable','价格调整':'price','删除':'delete','恢复':'restore'};
const opCls = opClassMap[h.type] || 'edit';
html += `<div class="history-item history-item--${opCls}">`;
html += `<div class="history-card">`;
html += `<div class="history-card__head">`;
html += historyOpTag(h.type);
html += `<span class="history-card__op">${h.title || ''}</span>`;
html += `<span class="history-card__time">${h.time}</span>`;
html += `</div>`;
html += `<div class="history-card__user">👤 ${h.user} · ${h.role || '操作员'}${h.dept?(' · '+h.dept):''}</div>`;
if(h.summary) html += `<div class="history-card__summary">${h.summary}</div>`;
// 变更明细
if(h.changes && h.changes.length>0){
html += `<div class="history-card__changes">`;
h.changes.forEach(function(c){
html += `<div class="history-change">`;
html += `<div class="history-change__field">${c.field}</div>`;
html += `<div class="history-change__old">${c.old || '—'}</div>`;
html += `<div class="history-change__new">${c.new || '—'}</div>`;
html += `</div>`;
});
html += `</div>`;
}
html += `</div></div>`;
});
html += `</div>`;
}
html += `</div>`;
return html;
}
function renderTexture(d){
const t = d.texture;
const gradeTag = t.textureGrade==='优秀'?'green':t.textureGrade==='良好'?'blue':t.textureGrade==='一般'?'orange':'red';
@@ -739,9 +956,172 @@ function togglePromoVideo(el, dishIdx, videoIdx){
(function(){var a=document.querySelector('.sidebar-item.active');if(!a)return;var s=a.closest('.sidebar-subgroup');if(s)s.classList.add('open');})();
// 初始化
renderDishList();
renderDetail();
// 初始化(先建立 filteredDishes 再渲染列表/详情)
applyFilters();
/* ============ 搜索与筛选 ============ */
/* 收集快速搜索 + 高级搜索条件,刷新 filteredDishes 与汇总条 */
function applyFilters(){
var kw = (document.getElementById('dishQuickSearchInput')||{}).value || '';
kw = kw.trim().toLowerCase();
var cat = (document.getElementById('dishCategoryFilter')||{}).value || '';
var stat = (document.getElementById('dishStatusFilter')||{}).value || '';
/* 应用快速搜索条件 */
var quickFiltered = DISHES.filter(function(d,i){
var matchKw = !kw || (d.name||'').toLowerCase().indexOf(kw)>=0 || (d.id||'').toLowerCase().indexOf(kw)>=0;
var matchCat = !cat || cat==='全部分类' || d.category===cat;
var matchStat = !stat || stat==='全部状态' || d.status===stat;
return matchKw && matchCat && matchStat;
});
/* 再叠加高级搜索条件 */
var list = quickFiltered;
if(appliedAdvSearch){
list = quickFiltered.filter(function(d){
return matchAdvanced(d, appliedAdvSearch);
});
}
filteredDishes = list.map(function(d){
var idx = DISHES.indexOf(d);
return {dish:d, idx:idx};
});
/* 若当前选中项被筛掉,则自动选第一条 */
var stillIn = filteredDishes.some(function(e){return e.idx===currentDish;});
if(!stillIn && filteredDishes.length>0){
currentDish = filteredDishes[0].idx;
currentTab = 0;
}
/* 更新汇总条 */
updateFilterSummary();
/* 重新渲染 */
renderDishList();
renderDetail();
/* 更新底部计数 */
var counter = document.getElementById('dishListCount');
if(counter) counter.textContent = '共 '+filteredDishes.length+' 条';
/* 空态提示 */
var emptyHint = document.getElementById('dishListEmpty');
if(emptyHint){
emptyHint.style.display = filteredDishes.length===0 ? 'block' : 'none';
}
}
/* 单条餐品是否匹配高级搜索条件 */
function matchAdvanced(d, q){
if(q.name && (d.name||'').toLowerCase().indexOf(q.name.toLowerCase())<0) return false;
if(q.code && (d.id||'').toLowerCase().indexOf(q.code.toLowerCase())<0) return false;
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.status && d.status!==q.status) return false;
/* 食材成本区间 */
var cost = (d.pricing && d.pricing.ingredientCost!==undefined) ? d.pricing.ingredientCost : null;
if(cost===null) cost = 0;
if(q.costMin!==null && cost < q.costMin) return false;
if(q.costMax!==null && cost > q.costMax) return false;
/* 餐品售价区间:取规格中最低价/最高价 */
var prices = (d.specs||[]).map(function(s){return s.price;});
var minPrice = prices.length ? Math.min.apply(null, prices) : 0;
var maxPrice = prices.length ? Math.max.apply(null, prices) : 0;
if(q.priceMin!==null && maxPrice < q.priceMin) return false;
if(q.priceMax!==null && minPrice > q.priceMax) return false;
/* 时间区间(字符串比较即可,演示数据格式 YYYY-MM-DD) */
if(q.updatedStart && (d.updateTime||'') < q.updatedStart) return false;
if(q.updatedEnd && (d.updateTime||'') > q.updatedEnd) return false;
/* 创建时间演示数据缺失,留作扩展(命中即跳过比较) */
return true;
}
/* 更新筛选汇总条 */
function updateFilterSummary(){
var sumEl = document.getElementById('filterSummary');
if(!sumEl) return;
var bits = [];
if(appliedAdvSearch){
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:'状态',
costMin:'成本≥',costMax:'成本≤',priceMin:'售价≥',priceMax:'售价≤',
updatedStart:'更新≥',updatedEnd:'更新≤',createdStart:'创建≥',createdEnd:'创建≤'};
var label = labelMap[k] || k;
var suffix = (k.indexOf('Min')>=0 || k.indexOf('Max')>=0) ? (k.indexOf('cost')>=0?'元':(k.indexOf('price')>=0?'元':'')) : '';
bits.push(label + v + suffix);
});
}
if(bits.length===0){
sumEl.style.display = 'none';
sumEl.innerHTML = '';
} else {
sumEl.style.display = 'flex';
sumEl.innerHTML = '<span class="panel-left__filter-summary__text">已应用筛选:' + bits.join(' · ') + '</span>'
+ '<button class="panel-left__filter-summary__clear" onclick="clearAdvancedSearch()">✕ 清空</button>';
}
}
/* 快速搜索(输入框 + 分类/状态下拉触发) */
function onQuickSearchInput(){ applyFilters(); }
function onQuickSearch(){ applyFilters(); }
/* 高级搜索弹窗 */
function openAdvancedSearchDialog(){
/* 把已应用条件回填到表单 */
var q = appliedAdvSearch || {};
setVal('advName', q.name||'');
setVal('advCode', q.code||'');
setVal('advCategory', q.category||'');
setVal('advMeal', q.meal||'');
setVal('advTag', q.tag||'');
setVal('advStatus', q.status||'');
setVal('advCostMin', q.costMin!==null && q.costMin!==undefined ? q.costMin : '');
setVal('advCostMax', q.costMax!==null && q.costMax!==undefined ? q.costMax : '');
setVal('advPriceMin', q.priceMin!==null && q.priceMin!==undefined ? q.priceMin : '');
setVal('advPriceMax', q.priceMax!==null && q.priceMax!==undefined ? q.priceMax : '');
setVal('advUpdatedStart', q.updatedStart||'');
setVal('advUpdatedEnd', q.updatedEnd||'');
setVal('advCreatedStart', q.createdStart||'');
setVal('advCreatedEnd', q.createdEnd||'');
document.getElementById('advancedSearchDialog').classList.add('open');
}
function closeAdvancedSearchDialog(){
document.getElementById('advancedSearchDialog').classList.remove('open');
}
function resetAdvancedSearch(){
['advName','advCode','advCategory','advMeal','advTag','advStatus','advCostMin','advCostMax','advPriceMin','advPriceMax','advUpdatedStart','advUpdatedEnd','advCreatedStart','advCreatedEnd'].forEach(function(id){ setVal(id,''); });
}
function applyAdvancedSearch(){
appliedAdvSearch = {
name: getVal('advName'),
code: getVal('advCode'),
category: getVal('advCategory'),
meal: getVal('advMeal'),
tag: getVal('advTag'),
status: getVal('advStatus'),
costMin: getNum('advCostMin'),
costMax: getNum('advCostMax'),
priceMin: getNum('advPriceMin'),
priceMax: getNum('advPriceMax'),
updatedStart: getVal('advUpdatedStart'),
updatedEnd: getVal('advUpdatedEnd'),
createdStart: getVal('advCreatedStart'),
createdEnd: getVal('advCreatedEnd')
};
/* 全部空字段视为未设置 */
var hasAny = Object.keys(appliedAdvSearch).some(function(k){
var v = appliedAdvSearch[k];
return v!==null && v!==undefined && v!=='';
});
if(!hasAny) appliedAdvSearch = null;
closeAdvancedSearchDialog();
applyFilters();
}
function clearAdvancedSearch(){
appliedAdvSearch = null;
applyFilters();
}
function setVal(id, v){ var el = document.getElementById(id); if(el) el.value = v; }
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; }
/* ========== 餐品分类管理 ========== */
var nextDishCatId = 100;
@@ -861,41 +1241,6 @@ function deleteDishCat(id){
}
</script>
<!-- 新增食材弹窗 -->
<div class="dialog-mask" id="ingredientDialog">
<div class="dialog">
<div class="dialog__header">
<div class="dialog__title">新增食材</div>
<button class="dialog__close" onclick="closeIngredientDialog()">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
<div class="dialog__body">
<div class="form-row">
<div class="form-group"><label>食材名称<span class="required">*</span></label><input class="form-input" type="text" placeholder="请输入食材名称"></div>
<div class="form-group"><label>净菜类型<span class="required">*</span></label><select class="form-select"><option>请选择</option><option>土豆丝</option><option>土豆块</option><option>土豆泥</option><option>番茄块</option><option>蒜末</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>用量(g/份)<span class="required">*</span></label><input class="form-input" type="number" placeholder="请输入用量"></div>
<div class="form-group"><label>加工状态</label><select class="form-select"><option>请选择</option><option>原料</option><option>净菜</option><option>半成品</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label>食材类型</label><select class="form-select"><option>辅材</option><option>主材</option><option>调料</option></select></div>
<div class="form-group"><label>过敏原</label><input class="form-input" type="text" placeholder="无则填无"></div>
</div>
<div class="form-group"><label>备注</label><input class="form-input" type="text" placeholder="请输入备注信息"></div>
</div>
<div class="dialog__footer">
<button class="btn btn-default" onclick="closeIngredientDialog()">取消</button>
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
<script>
function openIngredientDialog(){document.getElementById('ingredientDialog').classList.add('open');}
function closeIngredientDialog(){document.getElementById('ingredientDialog').classList.remove('open');}
</script>
<!-- 餐品分类管理弹窗 -->
<div class="dialog-mask" id="dishCategoryDialog">
<div class="dialog" style="width:820px;">
+268 -61
View File
@@ -111,6 +111,33 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.dynamic-row__del:hover{color:var(--danger);background:#FFF1F0;}
.dynamic-row-add{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border:1px dashed var(--primary);color:var(--primary);border-radius:var(--radius-sm);font-size:var(--font-xs);cursor:pointer;background:none;margin-top:8px;transition:all .2s;}
.dynamic-row-add:hover{background:var(--primary-light);}
/* 净菜表格(扁平 + rowspan 合并) */
.veg-table-wrap{overflow-x:auto;}
.veg-table{width:100%;border-collapse:collapse;font-size:var(--font-xs);border:1px solid #E8E8E8;border-radius:var(--radius-sm);}
.veg-table th{background:#FAFAFA;color:var(--text-secondary);font-weight:500;padding:8px 8px;text-align:left;border-bottom:1px solid #E8E8E8;white-space:nowrap;}
.veg-table td{padding:6px 8px;border-bottom:1px solid #F0F0F0;vertical-align:middle;}
.veg-table tr.veg-row:last-child td{border-bottom:none;}
.veg-table tr.veg-row--spec td{background:#FAFBFC;}
.veg-table .veg-cell--merged{background:#FAFBFC;vertical-align:middle;text-align:center;padding:8px;}
.veg-table .veg-name{display:block;font-size:var(--font-sm);font-weight:600;color:var(--primary);line-height:1.3;word-break:break-all;}
.veg-table .veg-name--placeholder{color:var(--text-placeholder);font-weight:400;font-size:var(--font-xs);}
.veg-table .veg-cell--merged .form-input,.veg-table .veg-cell--merged .form-select{height:26px;font-size:11px;}
.veg-table .form-input,.veg-table .form-select{height:28px;font-size:var(--font-xs);padding:0 6px;width:100%;}
.veg-table .col-name{min-width:130px;}
.veg-table .col-alias{min-width:100px;}
.veg-table .col-yield{min-width:70px;}
.veg-table .col-shape{min-width:90px;}
.veg-table .col-spec-alias{min-width:90px;width:90px;}
.veg-table .col-dim{min-width:62px;width:62px;}
.veg-table .col-tech{min-width:85px;width:85px;}
.veg-table .col-action{min-width:60px;white-space:nowrap;text-align:center;}
.veg-action-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid var(--border);background:#fff;color:var(--text-secondary);border-radius:var(--radius-sm);cursor:pointer;font-size:13px;line-height:1;transition:all .15s;vertical-align:middle;margin:0 1px;}
.veg-action-btn:hover{color:var(--primary);border-color:var(--primary);}
.veg-action-btn--add{color:var(--primary);border-color:var(--primary);}
.veg-action-btn--add:hover{background:var(--primary-light);}
.veg-action-btn--del:hover{color:var(--danger);border-color:var(--danger);background:#FFF1F0;}
.veg-action-btn[disabled]{opacity:0.35;cursor:not-allowed;pointer-events:none;}
.veg-table .veg-empty-row td{padding:24px;text-align:center;color:var(--text-placeholder);}
/* 复制食材弹窗 */
.copy-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:300;align-items:center;justify-content:center;}
.copy-overlay.show{display:flex;}
@@ -282,7 +309,7 @@ var STEP_HTML = [
`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;"><div class="form-card__title" style="margin-bottom:0;padding-bottom:0;border-bottom:none;flex-shrink:0;">📋 基本信息</div><button type="button" class="btn btn-default" onclick="openCopyDialog()" style="height:32px;font-size:var(--font-xs);">📋 从已有食材复制</button></div>
<div class="form-grid">
<div class="form-group"><label class="form-label">食物编码<span class="required">*</span></label><input class="form-input" id="foodCode" placeholder="系统自动生成(分类+自编码)" readonly style="background:#f5f5f5;cursor:not-allowed;"></div>
<div class="form-group"><label class="form-label">食物名称<span class="required">*</span></label><input class="form-input" id="foodName" placeholder="请输入食物名称"></div>
<div class="form-group"><label class="form-label">食物名称<span class="required">*</span></label><input class="form-input" id="foodName" placeholder="请输入食物名称" oninput="onFoodNameInput()"></div>
<div class="form-group"><label class="form-label">别名</label><input class="form-input" id="foodAlias" placeholder="请输入别名"></div>
<div class="form-group"><label class="form-label">食物分类<span class="required">*</span></label>
<div class="cascade-picker" id="addCategoryPicker">
@@ -302,9 +329,9 @@ var STEP_HTML = [
<span style="position:absolute;right:12px;top:0;bottom:0;display:flex;align-items:center;color:var(--text-placeholder);font-size:var(--font-sm);pointer-events:none;">元/100g</span>
</div>
</div>
<div class="form-group full"><label class="form-label">净菜类型</label>
<div class="dynamic-rows" id="vegTypeRows"></div>
<button type="button" class="dynamic-row-add" onclick="addVegType()">+ 添加净菜类型</button>
<div class="form-group full">
<div id="vegTypeRows"></div>
<button type="button" class="dynamic-row-add" onclick="addVegType()">+ 添加净菜</button>
</div>
<div class="form-group"><label class="form-label">食材图片</label>
<div class="upload-box" id="uploadBox" onclick="triggerUpload()">
@@ -517,76 +544,243 @@ function submitForm(){
window.location.href='din-ingredient.html';
}
/* === 净菜类型动态行 === */
/* === 净菜类型动态行(扁平结构 + rowspan 合并) === */
/* 切配形状选项(对应字典 spec_cut_shape) */
var VEG_SHAPE_OPTIONS=['','丝(细)','丝(中)','丝(粗)','片(薄)','片(中)','片(厚)','丁(小)','丁(中)','丁(大)','块(小)','块(中)','块(大)','段','条','泥',''];
var VEG_SHAPE_OPTIONS=['','丝','片','块','','泥','原状'];
/* 刀工选项 */
var VEG_TECH_OPTIONS=['','平刀','斜刀','直刀','花刀','其他'];
/* 净菜组 ID 自增 */
var vegGroupSeq = 1;
/* 取当前食物名称(从表单中读取) */
function getFoodName(){var el=document.getElementById('foodName');return el?el.value.trim():'';}
/* 由食物名称+切配形状拼接净菜标准名 */
function buildVegName(shape){
var fn=getFoodName();
if(!fn||!shape) return '';
return fn+'-'+shape;
}
/* 生成新净菜组 ID */
function genGroupId(){return 'g'+(vegGroupSeq++);}
function renderVegTypes(){
var container = document.getElementById('vegTypeRows');
if(!container) return;
if(vegTypes.length===0) vegTypes.push({name:'',yieldRate:''});
var html = '';
vegTypes.forEach(function(item,i){
var cs = item.cutSpec || {};
html += '<div class="dynamic-row">';
html += '<input class="form-input veg-name" placeholder="净菜名称" value="'+(item.name||'')+'" style="flex:1.2">';
html += '<input class="form-input veg-yield" type="number" min="0" max="100" placeholder="出净率(%)" value="'+(item.yieldRate||'')+'" style="flex:0.6">';
html += '<select class="form-select veg-shape" style="flex:0.9">';
VEG_SHAPE_OPTIONS.forEach(function(opt){
var sel = (opt && cs.shape===opt) ? ' selected' : '';
html += '<option value="'+opt+'"'+sel+'>'+(opt||'切配形状')+'</option>';
});
html += '</select>';
html += '<input class="form-input veg-length" type="number" min="0" step="0.1" placeholder="长(mm)" value="'+(cs.length||'')+'" style="flex:0.5">';
html += '<input class="form-input veg-width" type="number" min="0" step="0.1" placeholder="宽(mm)" value="'+(cs.width||'')+'" style="flex:0.5">';
html += '<input class="form-input veg-height" type="number" min="0" step="0.1" placeholder="高(mm)" value="'+(cs.height||'')+'" style="flex:0.5">';
html += '<select class="form-select veg-technique" style="flex:0.6">';
VEG_TECH_OPTIONS.forEach(function(opt){
var sel = (opt && cs.technique===opt) ? ' selected' : '';
html += '<option value="'+opt+'"'+sel+'>'+(opt||'刀工')+'</option>';
});
html += '</select>';
html += '<button type="button" class="dynamic-row__del" onclick="removeVegType('+i+')" title="删除">×</button>';
html += '</div>';
if(vegTypes.length===0){
vegTypes.push({groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',cutSpec:{}});
}
/* 按 groupId 分组(保持原顺序) */
var groupKeys = [];
var groupMap = {};
vegTypes.forEach(function(item){
var key = item.groupId;
if(!groupMap[key]){
groupKeys.push(key);
groupMap[key] = [];
}
groupMap[key].push(item);
});
var html = '<div class="veg-table-wrap"><table class="veg-table"><thead><tr>';
html += '<th class="col-name">净菜标准名</th>';
html += '<th class="col-alias">净菜俗名</th>';
html += '<th class="col-yield">出净率(%)</th>';
html += '<th class="col-shape">切配形状</th>';
html += '<th class="col-spec-alias">规格别名</th>';
html += '<th class="col-dim">长(mm)</th>';
html += '<th class="col-dim">宽(mm)</th>';
html += '<th class="col-dim">高(mm)</th>';
html += '<th class="col-tech">刀工</th>';
html += '<th class="col-action">操作</th>';
html += '</tr></thead><tbody>';
if(groupKeys.length===0){
html += '<tr class="veg-empty-row"><td colspan="10">暂无净菜类型,点击下方"+ 添加净菜"按钮新增</td></tr>';
}
groupKeys.forEach(function(key){
var items = groupMap[key];
var first = items[0];
var rs = items.length; /* rowspan */
/* 净菜标准名 = 食物名称 + 切配形状(取自 master 的 cutSpec.shape) */
var firstCs = first.cutSpec||{};
var nameText = buildVegName(firstCs.shape);
items.forEach(function(item,idx){
var isMaster = idx===0;
var rowClass = 'veg-row' + (isMaster?'':' veg-row--spec');
html += '<tr class="'+rowClass+'" data-row-id="'+idx+'" data-group-id="'+key+'">';
if(isMaster){
/* 合并列1:净菜标准名(只读显示) */
var nameClass = nameText ? 'veg-name' : 'veg-name veg-name--placeholder';
html += '<td class="veg-cell--merged col-name" rowspan="'+rs+'">';
html += '<span class="'+nameClass+'">'+(nameText||'—')+'</span>';
html += '</td>';
/* 合并列2:净菜俗名 */
html += '<td class="veg-cell--merged col-alias" rowspan="'+rs+'"><input class="form-input veg-group-alias" value="'+(first.alias||'')+'" placeholder="如:细丝组" oninput="onGroupField(\''+key+'\',\'alias\',this.value)"></td>';
/* 合并列3:出净率(整组共享,跟 master 一致) */
html += '<td class="veg-cell--merged col-yield" rowspan="'+rs+'"><input class="form-input veg-yield" type="number" min="0" max="100" value="'+(first.yieldRate||'')+'" oninput="onGroupField(\''+key+'\',\'yieldRate\',this.value)"></td>';
/* 合并列4:切配形状(整组共享,驱动净菜标准名拼接) */
html += '<td class="veg-cell--merged col-shape" rowspan="'+rs+'"><select class="form-select veg-shape" onchange="onGroupShapeChange(\''+key+'\',this.value)">';
VEG_SHAPE_OPTIONS.forEach(function(opt){
var sel = (opt && firstCs.shape===opt) ? ' selected' : '';
html += '<option value="'+opt+'"'+sel+'>'+(opt||'请选择')+'</option>';
});
html += '</select></td>';
}
/* 各规格独立列:规格别名/长宽高/刀工 */
html += '<td class="col-spec-alias"><input class="form-input veg-spec-alias" value="'+(item.specAlias||'')+'" placeholder="如:细丝" oninput="onRowField('+idx+',\''+key+'\',\'specAlias\',this.value)"></td>';
var cs = item.cutSpec||{};
html += '<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.length||'')+'" oninput="onRowField('+idx+',\''+key+'\',\'length\',this.value)"></td>';
html += '<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.width||'')+'" oninput="onRowField('+idx+',\''+key+'\',\'width\',this.value)"></td>';
html += '<td class="col-dim"><input class="form-input veg-dim" type="number" min="0" step="0.1" value="'+(cs.height||'')+'" oninput="onRowField('+idx+',\''+key+'\',\'height\',this.value)"></td>';
html += '<td class="col-tech"><select class="form-select veg-tech" onchange="onRowField('+idx+',\''+key+'\',\'technique\',this.value)">';
VEG_TECH_OPTIONS.forEach(function(opt){
var sel = (opt && cs.technique===opt) ? ' selected' : '';
html += '<option value="'+opt+'"'+sel+'>'+(opt||'请选择')+'</option>';
});
html += '</select></td>';
/* 操作列:master 行有 +规格 和 ×删除; 子规格行只有 ×删除 */
html += '<td class="col-action">';
if(isMaster){
html += '<button type="button" class="veg-action-btn veg-action-btn--add" onclick="addVegSpec(\''+key+'\')" title="新增一行规格"></button>';
}
html += '<button type="button" class="veg-action-btn veg-action-btn--del" onclick="removeVegRow('+idx+',\''+key+'\')" title="删除该行">×</button>';
html += '</td>';
html += '</tr>';
});
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
/* 添加净菜(新组) */
function addVegType(){
saveVegTypes();
vegTypes.push({name:'',yieldRate:''});
vegTypes.push({groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',cutSpec:{}});
renderVegTypes();
}
function removeVegType(idx){
/* 在指定净菜组下追加一行规格(共用 groupId) */
function addVegSpec(groupId){
saveVegTypes();
vegTypes.splice(idx,1);
if(vegTypes.length===0) vegTypes.push({name:'',yieldRate:''});
var master = vegTypes.find(function(it){return it.groupId===groupId;});
var newItem = {
groupId:groupId,
alias:'',
specAlias:'',
yieldRate:'',
cutSpec:{}
};
/* 紧跟该组最后一项插入 */
var lastIdx = -1;
for(var i=vegTypes.length-1;i>=0;i--){
if(vegTypes[i].groupId===groupId){lastIdx=i;break;}
}
if(lastIdx>=0) vegTypes.splice(lastIdx+1,0,newItem);
else vegTypes.push(newItem);
renderVegTypes();
}
function saveVegTypes(){
var rows = document.querySelectorAll('#vegTypeRows .dynamic-row');
vegTypes = [];
rows.forEach(function(row){
var name = row.querySelector('.veg-name');
var yieldRate = row.querySelector('.veg-yield');
if(!name || !yieldRate) return;
var shape = row.querySelector('.veg-shape');
var length = row.querySelector('.veg-length');
var width = row.querySelector('.veg-width');
var height = row.querySelector('.veg-height');
var technique = row.querySelector('.veg-technique');
var item = {name:name.value, yieldRate:yieldRate.value};
/* 任一切配字段有值则组装 cutSpec 对象 */
if((shape && shape.value) || (length && length.value) || (width && width.value) || (height && height.value) || (technique && technique.value)){
item.cutSpec = {
shape: shape ? shape.value : '',
length: length ? length.value : '',
width: width ? width.value : '',
height: height ? height.value : '',
technique: technique ? technique.value : ''
};
/* 删除单行:同组最后一行则整组消失 */
function removeVegRow(idx,groupId){
saveVegTypes();
/* 找到 DOM 中第 idx 行对应的 vegTypes 项(同组内的相对索引) */
var sameGroupItems = vegTypes.filter(function(it){return it.groupId===groupId;});
if(idx>=sameGroupItems.length) return;
var itemToRemove = sameGroupItems[idx];
var pos = vegTypes.indexOf(itemToRemove);
if(pos<0) return;
vegTypes.splice(pos,1);
if(vegTypes.length===0){
vegTypes.push({groupId:genGroupId(),shapeCat:'',alias:'',specAlias:'',yieldRate:'',cutSpec:{}});
}
renderVegTypes();
}
/* 切配形状变化:同步更新整组 cutSpec.shape(影响净菜标准名拼接) */
function onGroupShapeChange(groupId,val){
vegTypes.forEach(function(it){
if(it.groupId===groupId){
if(!it.cutSpec) it.cutSpec = {};
it.cutSpec.shape = val;
}
vegTypes.push(item);
});
/* 仅刷新净菜标准名文本,避免重绘输入框丢焦点 */
var masterName = document.querySelector('tr[data-group-id="'+groupId+'"][data-row-id="0"] td.col-name .veg-name');
if(masterName){
var txt = buildVegName(val);
masterName.textContent = txt || '—';
masterName.className = txt ? 'veg-name' : 'veg-name veg-name--placeholder';
}
}
/* 净菜组共享字段:净菜俗名/出净率 */
function onGroupField(groupId,field,val){
vegTypes.forEach(function(it){
if(it.groupId===groupId) it[field] = val;
});
}
/* 单行字段实时保存(仅规格独立字段:specAlias/长宽高/刀工) */
function onRowField(idx,groupId,field,val){
var sameGroupItems = vegTypes.filter(function(it){return it.groupId===groupId;});
if(idx>=sameGroupItems.length) return;
var item = sameGroupItems[idx];
if(field==='specAlias'){
item[field] = val;
} else if(field==='length' || field==='width' || field==='height' || field==='technique'){
if(!item.cutSpec) item.cutSpec = {};
item.cutSpec[field] = val;
}
}
/* 切换步骤时收集 DOM → vegTypes */
function saveVegTypes(){
var newVegs = [];
var rows = document.querySelectorAll('#vegTypeRows .veg-table tbody tr.veg-row');
/* 按 groupId 缓存 master 的共享字段(净菜俗名/出净率/切配形状) */
var groupShared = {};
rows.forEach(function(row){
var groupId = row.getAttribute('data-group-id');
var rowIdx = parseInt(row.getAttribute('data-row-id'),10);
var isMaster = rowIdx===0;
var item = {groupId:groupId};
if(isMaster){
var aliasInp = row.querySelector('.veg-group-alias');
var yieldInp = row.querySelector('.veg-yield');
var shapeSel = row.querySelector('.veg-shape');
item.alias = aliasInp ? aliasInp.value : '';
item.yieldRate = yieldInp ? yieldInp.value : '';
var shape = shapeSel ? shapeSel.value : '';
item.cutSpec = {shape:shape};
groupShared[groupId] = {alias:item.alias, yieldRate:item.yieldRate, shape:shape};
} else {
/* 子规格行:共享字段从 master 拷贝 */
var shared = groupShared[groupId] || {alias:'',yieldRate:'',shape:''};
item.alias = shared.alias;
item.yieldRate = shared.yieldRate;
item.cutSpec = {shape:shared.shape};
}
item.specAlias = row.querySelector('.veg-spec-alias') ? row.querySelector('.veg-spec-alias').value : '';
var dims = row.querySelectorAll('.veg-dim');
var techSel = row.querySelector('.veg-tech');
item.cutSpec.length = dims[0] ? dims[0].value : '';
item.cutSpec.width = dims[1] ? dims[1].value : '';
item.cutSpec.height = dims[2] ? dims[2].value : '';
item.cutSpec.technique = techSel ? techSel.value : '';
newVegs.push(item);
});
vegTypes = newVegs.length>0 ? newVegs : [{groupId:genGroupId(),alias:'',specAlias:'',yieldRate:'',cutSpec:{}}];
}
/* 食物名称输入时实时刷新所有净菜标准名 */
function onFoodNameInput(){
document.querySelectorAll('#vegTypeRows .veg-table tbody tr[data-row-id="0"] td.col-name .veg-name').forEach(function(el){
/* 找到同组 master 的切配形状 */
var row = el.closest('tr');
var groupId = row ? row.getAttribute('data-group-id') : '';
var master = vegTypes.find(function(it){return it.groupId===groupId;});
var shape = (master && master.cutSpec && master.cutSpec.shape) ? master.cutSpec.shape : '';
var txt = buildVegName(shape);
el.textContent = txt || '—';
el.className = txt ? 'veg-name' : 'veg-name veg-name--placeholder';
});
}
@@ -733,7 +927,10 @@ if(initTab >= 0 && initTab < totalSteps) currentStep = initTab;
/* 编辑模式 mock 数据(与 din-ingredient.html 的 FOODS 结构一致) */
var EDIT_FOODS = {
'0101001':{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',edible:'97',waterRate:'13.3',dataSource:'0',basePrice:'3.5',vegTypes:[{name:'洗净米',yieldRate:'97'},{name:'免淘米',yieldRate:'95'}],
'0101001':{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',edible:'97',waterRate:'13.3',dataSource:'0',basePrice:'3.5',vegTypes:[
{groupId:'g1',alias:'普洗组',specAlias:'洗净米',yieldRate:'97',cutSpec:{shape:'原状'}},
{groupId:'g1',alias:'普洗组',specAlias:'免淘米',yieldRate:'95',cutSpec:{shape:'原状'}}
],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],
nutri:{calorie:346,protein:7.4,fat:0.6,carbohydrate:77.2,dietary_fiber:0.7,
vitamin_a:0,vitamin_b1:0.11,vitamin_b2:0.02,vitamin_b6:0.12,vitamin_b12:0,vitamin_c:0,vitamin_d:0,vitamin_e:1.01,vitamin_k:0,niacin:1.6,folic_acid:3.8,biotin:0,pantothenic_acid:0.6,total_choline:0,
@@ -744,7 +941,11 @@ var EDIT_FOODS = {
color:{main:'乳白色',grade:'优',desc:'色泽洁白,半透明,颗粒饱满',cause:'碾磨去皮后淀粉体暴露'},
taste:{main:'淡味',sub:'微涩',intensity:'弱',desc:'生米无明显味道,煮熟后清香'},
texture:{main:'软糯',chew:'易',digest:'中',desc:'米粒饱满,质地坚硬,煮后软糯',cook:'水米比1:1.2,浸泡30分钟口感更佳'}},
'0301001':{name:'猪里脊(生)',code:'0301001',alias:'里脊肉',firstClass:'畜禽肉类',secondClass:'猪肉',edible:'100',waterRate:'73.3',dataSource:'0',basePrice:'18.8',vegTypes:[{name:'去筋膜',yieldRate:'92'},{name:'切丁',yieldRate:'88',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,technique:'直刀'}}],
'0301001':{name:'猪里脊(生)',code:'0301001',alias:'里脊肉',firstClass:'畜禽肉类',secondClass:'猪肉',edible:'100',waterRate:'73.3',dataSource:'0',basePrice:'18.8',vegTypes:[
{groupId:'g1',alias:'丝类组',specAlias:'细丝',yieldRate:'92',cutSpec:{shape:'丝',length:10,width:1,height:1,technique:'平刀'}},
{groupId:'g1',alias:'丝类组',specAlias:'粗丝',yieldRate:'90',cutSpec:{shape:'丝',length:10,width:2,height:2,technique:'直刀'}},
{groupId:'g2',alias:'块类组',specAlias:'方块',yieldRate:'88',cutSpec:{shape:'块',length:15,width:15,height:15,technique:'直刀'}}
],
crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'},{name:'高血脂人群',level:'适量'}],
nutri:{calorie:143,protein:20.2,fat:7.1,carbohydrate:0.7,dietary_fiber:0,
vitamin_a:6,vitamin_b1:0.54,vitamin_b2:0.1,vitamin_b6:0.27,vitamin_b12:0.6,vitamin_c:0,vitamin_d:0.2,vitamin_e:0.34,vitamin_k:0,niacin:5.3,folic_acid:5,biotin:0,pantothenic_acid:0.5,total_choline:0,
@@ -766,7 +967,10 @@ var EDIT_FOODS = {
color:{main:'金黄色',grade:'优',desc:'蛋壳浅褐,蛋黄金黄饱满',cause:'蛋黄富含叶黄素和类胡萝卜素'},
taste:{main:'鲜味',sub:'蛋香',intensity:'中',desc:'蛋白柔和,蛋黄浓郁'},
texture:{main:'嫩滑',chew:'易',digest:'易',desc:'水煮蛋弹嫩,炒蛋软嫩,蒸蛋细腻',cook:'蒸蛋口感最软,适合各年龄段'}},
'0201001':{name:'菠菜(生)',code:'0201001',alias:'波斯菜',firstClass:'蔬菜类',secondClass:'叶菜类',edible:'89',waterRate:'91.2',dataSource:'0',basePrice:'4.2',vegTypes:[{name:'去根',yieldRate:'90'},{name:'切段',yieldRate:'88',cutSpec:{shape:'段',length:50,width:'',height:'',technique:'直刀'}}],
'0201001':{name:'菠菜(生)',code:'0201001',alias:'波斯菜',firstClass:'蔬菜类',secondClass:'叶菜类',edible:'89',waterRate:'91.2',dataSource:'0',basePrice:'4.2',vegTypes:[
{groupId:'g1',alias:'段类组',specAlias:'净菜',yieldRate:'90',cutSpec:{shape:'原状'}},
{groupId:'g1',alias:'段类组',specAlias:'寸段',yieldRate:'88',cutSpec:{shape:'原状',length:50,width:'',height:'',technique:'直刀'}}
],
crowd:[{name:'全人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],
nutri:{calorie:28,protein:2.6,fat:0.3,carbohydrate:4.5,dietary_fiber:1.7,
vitamin_a:487,vitamin_b1:0.04,vitamin_b2:0.11,vitamin_b6:0.1,vitamin_b12:0,vitamin_c:32,vitamin_d:0,vitamin_e:1.74,vitamin_k:0,niacin:0.6,folic_acid:110,biotin:0,pantothenic_acid:0.2,total_choline:0,
@@ -777,7 +981,10 @@ var EDIT_FOODS = {
color:{main:'深绿色',grade:'优',desc:'叶片翠绿有光泽,根部紫红',cause:'叶绿素含量高'},
taste:{main:'清甜',sub:'微涩',intensity:'弱',desc:'清爽微甜带涩'},
texture:{main:'嫩脆',chew:'易',digest:'易',desc:'叶片嫩滑,茎部脆嫩',cook:'焯水去草酸后口感最佳'}},
'0601001':{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',edible:'100',waterRate:'82.8',dataSource:'1',basePrice:'5.0',vegTypes:[{name:'切块',yieldRate:'96',cutSpec:{shape:'块(中)',length:30,width:30,height:20,technique:'直刀'}},{name:'切丁',yieldRate:'94',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,technique:'直刀'}}],
'0601001':{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',edible:'100',waterRate:'82.8',dataSource:'1',basePrice:'5.0',vegTypes:[
{groupId:'g1',alias:'块类组',specAlias:'大方块',yieldRate:'96',cutSpec:{shape:'块',length:30,width:30,height:20,technique:'直刀'}},
{groupId:'g2',alias:'片类组',specAlias:'薄片',yieldRate:'94',cutSpec:{shape:'片',length:10,width:10,height:3,technique:'平刀'}}
],
crowd:[{name:'全人群',level:'推荐'},{name:'高血压人群',level:'推荐'},{name:'痛风人群',level:'适量'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],
nutri:{calorie:98,protein:12.2,fat:4.8,carbohydrate:1.5,dietary_fiber:0.4,
vitamin_a:0,vitamin_b1:0.07,vitamin_b2:0.04,vitamin_b6:0.05,vitamin_b12:0,vitamin_c:0,vitamin_d:0,vitamin_e:2.71,vitamin_k:0,niacin:0.1,folic_acid:37,biotin:0,pantothenic_acid:0.1,total_choline:0,
+214 -14
View File
@@ -122,9 +122,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
/* 净菜类型小表格 */
.veg-type-table{width:100%;border-collapse:collapse;border:1px solid #F0F0F0;border-radius:var(--radius-sm);overflow:hidden;margin-top:4px;}
.veg-type-table th{background:#FAFAFA;font-size:var(--font-xs);color:var(--text-secondary);font-weight:500;text-align:left;padding:8px 12px;border-bottom:1px solid #F0F0F0;}
.veg-type-table td{font-size:var(--font-sm);color:var(--text-primary);padding:8px 12px;border-bottom:1px solid #F5F5F5;}
.veg-type-table td{font-size:var(--font-sm);color:var(--text-primary);padding:8px 12px;border-bottom:1px solid #F5F5F5;vertical-align:middle;}
.veg-type-table tr:last-child td{border-bottom:none;}
.veg-type-table th+th,.veg-type-table td+td{border-left:1px solid #F0F0F0;}
.veg-type-table .veg-type__name{font-weight:500;color:var(--primary);background:#F8FBFF;}
.veg-type-table .veg-type__groupalias{color:var(--text-secondary);}
.veg-type-table .veg-type__specalias{color:var(--text-primary);}
.veg-type-table .veg-type__divider td{border-top:2px solid #E8E8E8;}
/* 级联选择器 */
.cascade-picker{position:relative;flex:1;}
.cascade-trigger{height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 10px;font-size:var(--font-xs);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;cursor:pointer;display:flex;align-items:center;white-space:nowrap;color:var(--text-placeholder);width:100%;}
@@ -177,6 +181,38 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.status-toggle.active .status-toggle__switch::after{transform:translateX(16px);}
.status-toggle__label{font-size:var(--font-xs);color:var(--text-placeholder);}
.status-toggle.active .status-toggle__label{color:var(--success);}
/* 操作历史 */
.history-filter{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;}
.history-filter select,.history-filter input[type="text"]{height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 10px;font-size:var(--font-xs);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;}
.history-filter select:focus,.history-filter input[type="text"]:focus{border-color:var(--primary);}
.history-filter input[type="text"]{padding-right:10px;background:#fff;appearance:auto;cursor:text;}
.history-filter .btn{height:32px;font-size:var(--font-xs);padding:0 12px;}
/* 时间轴 */
.history-timeline{position:relative;padding-left:20px;}
.history-timeline::before{content:'';position:absolute;left:5px;top:6px;bottom:6px;width:2px;background:#E8E8E8;}
.history-item{position:relative;padding:0 0 18px 24px;}
.history-item:last-child{padding-bottom:0;}
.history-item::before{content:'';position:absolute;left:-19px;top:4px;width:12px;height:12px;border-radius:50%;background:#fff;border:2px solid var(--primary);box-shadow:0 0 0 3px #E6F7FF;}
.history-item--create::before{border-color:var(--success);box-shadow:0 0 0 3px #F6FFED;}
.history-item--edit::before{border-color:var(--primary);box-shadow:0 0 0 3px #E6F7FF;}
.history-item--disable::before{border-color:var(--text-placeholder);box-shadow:0 0 0 3px #F5F5F5;}
.history-item--price::before{border-color:var(--warning);box-shadow:0 0 0 3px #FFF7E6;}
.history-item--delete::before{border-color:var(--danger);box-shadow:0 0 0 3px #FFF1F0;}
.history-item--restore::before{border-color:#722ED1;box-shadow:0 0 0 3px #F9F0FF;}
.history-card{background:#FAFBFC;border:1px solid #F0F0F0;border-radius:var(--radius-sm);padding:12px 16px;}
.history-card__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px;}
.history-card__time{font-size:var(--font-xs);color:var(--text-placeholder);margin-left:auto;}
.history-card__op{font-size:var(--font-sm);font-weight:500;color:var(--text-primary);}
.history-card__user{font-size:var(--font-xs);color:var(--text-secondary);display:flex;align-items:center;gap:4px;}
.history-card__summary{font-size:var(--font-sm);color:var(--text-secondary);line-height:1.6;margin-top:4px;}
.history-card__changes{margin-top:8px;background:#fff;border:1px solid #F0F0F0;border-radius:var(--radius-sm);overflow:hidden;}
.history-change{display:grid;grid-template-columns:120px 1fr 1fr;align-items:center;font-size:var(--font-xs);border-bottom:1px solid #F5F5F5;}
.history-change:last-child{border-bottom:none;}
.history-change__field{padding:6px 12px;color:var(--text-secondary);background:#FAFAFA;border-right:1px solid #F0F0F0;}
.history-change__old{padding:6px 12px;color:var(--text-placeholder);text-decoration:line-through;border-right:1px dashed #E8E8E8;}
.history-change__new{padding:6px 12px;color:var(--text-primary);font-weight:500;}
.history-empty{text-align:center;padding:40px 0;color:var(--text-placeholder);font-size:var(--font-sm);}
</style>
</head>
<body>
@@ -346,6 +382,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
<div class="detail-tab" onclick="switchDetailTab(2)">👅 味道档案</div>
<div class="detail-tab" onclick="switchDetailTab(3)">🫦 口感档案</div>
<div class="detail-tab" onclick="switchDetailTab(4)">🎨 色泽档案</div>
<div class="detail-tab" onclick="switchDetailTab(5)">🕘 操作历史</div>
</div>
</div>
<div class="panel-right__body" id="detail-body">
@@ -370,10 +407,24 @@ function formatCutSpec(cut){
var result = dimStr + shapeStr + techStr;
return result || '—';
}
/* 净菜类型列表专用:切配规格剥离 shape(丝/片/丁/块),因同表已有独立「切配形状」列,避免冗余 */
function formatCutSpecNoShape(cut){
if(!cut) return '—';
if(typeof cut === 'string') return cut;
var dims = [cut.length, cut.width, cut.height].filter(function(v){
return v !== undefined && v !== null && v !== '' && !isNaN(v);
});
var dimStr = dims.length ? dims.join('×') + 'mm' : '';
var techStr = cut.technique ? ' · ' + cut.technique : '';
return (dimStr || techStr) ? (dimStr + techStr) : '—';
}
// 食物数据(对齐 yx_mater_base_material 表结构)
const FOODS = [
{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',icon:'🌾',edible:'100',waterRate:'13.3',dataSource:0,basePrice:'3.5',vegTypes:[{name:'洗净米',yieldRate:'97'},{name:'免淘米',yieldRate:'95'}],
{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',icon:'🌾',edible:'100',waterRate:'13.3',dataSource:0,basePrice:'3.5',vegTypes:[
{groupId:'g1',alias:'普洗组',specAlias:'洗净米',yieldRate:'97',cutSpec:{shape:'原状'}},
{groupId:'g1',alias:'普洗组',specAlias:'免淘米',yieldRate:'95',cutSpec:{shape:'原状'}}
],
nutri:{calorie:346,protein:7.4,fat:0.8,carbohydrate:77.2,dietary_fiber:0.6,
vitamin_a:0,vitamin_b1:0.16,vitamin_b2:0.08,vitamin_b6:0.16,vitamin_b12:0,vitamin_c:0,vitamin_d:0,vitamin_e:0.46,vitamin_k:0,niacin:1.9,folic_acid:3.8,biotin:0,pantothenic_acid:0.6,total_choline:0,
ca:13,phosphorus:110,kalium:103,mg:34,na:3.8,fe:2.3,zn:1.7,se:2.23,cu:0.3,mn:1.29,iodine:0,
@@ -382,8 +433,20 @@ const FOODS = [
gi:83,purine:18.4,guanine:0,adenine:0,hypoxanthine:0,xanthine:0},
color:{main:'白色',desc:'乳白色,半透明,颗粒饱满',cause:'淀粉主体,含少量蛋白质',grade:'优',crowd:[{name:'成人',level:'推荐'},{name:'老年人',level:'推荐'},{name:'儿童',level:'推荐'}],note:'—'},
taste:{main:'淡味',sub:'—',intensity:'弱',desc:'清淡,煮熟后微甜,无异味',crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],note:'糖尿病人群注意控制量'},
texture:{main:'软糯',chew:'易',digest:'易',desc:'煮熟后软糯有嚼劲,易于消化',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],cook:'熬粥质地更软,适合老年人'}},
{name:'猪里脊肉',code:'0201001',alias:'里脊',firstClass:'畜禽肉类',secondClass:'猪肉',icon:'🥩',edible:'100',waterRate:'71.4',dataSource:0,basePrice:'18.8',vegTypes:[{name:'肉片',yieldRate:'95',cutSpec:{shape:'片(中)',length:3,width:1,height:5,technique:'平刀'}},{name:'肉丁',yieldRate:'92',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,technique:'直刀'}},{name:'肉块',yieldRate:'97',cutSpec:{shape:'块(中)',length:30,width:30,height:30,technique:'直刀'}}],
texture:{main:'软糯',chew:'易',digest:'易',desc:'煮熟后软糯有嚼劲,易于消化',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],cook:'熬粥质地更软,适合老年人'},
history:[
{type:'新增',title:'创建食材条目',time:'2026-01-12 09:24',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'从系统内置食物成分库导入,初始化基础档案。'},
{type:'编辑',title:'补充营养成分数据',time:'2026-02-08 14:10',user:'李建国',role:'营养师',dept:'营养管理部',summary:'完善 53 项营养素中的维生素 K 与生物素字段。',changes:[{field:'维生素K',old:'—',new:'0 μg'},{field:'生物素',old:'—',new:'0 μg'}]},
{type:'编辑',title:'调整净菜类型',time:'2026-03-15 10:42',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'新增「免淘米」净菜规格,出净率 95%。',changes:[{field:'净菜类型',old:'1 项(普洗组)',new:'2 项(普洗组/免淘米)'}]},
{type:'价格调整',title:'更新基础定价',time:'2026-06-05 16:30',user:'张明',role:'采购员',dept:'采购部',summary:'根据 6 月市场行情调整粳米基础定价。',changes:[{field:'基础定价',old:'3.2 元/100g',new:'3.5 元/100g'}]},
{type:'编辑',title:'修订色泽描述',time:'2026-07-21 11:05',user:'李建国',role:'营养师',dept:'营养管理部',summary:'色泽描述语义化,补充感官评级。',changes:[{field:'色泽描述',old:'乳白色,半透明',new:'乳白色,半透明,颗粒饱满'},{field:'感官评级',old:'—',new:'优'}]}
]},
{name:'猪里脊肉',code:'0201001',alias:'里脊',firstClass:'畜禽肉类',secondClass:'猪肉',icon:'🥩',edible:'100',waterRate:'71.4',dataSource:0,basePrice:'18.8',vegTypes:[
{groupId:'g1',alias:'里脊肉丝',specAlias:'细丝',yieldRate:'92',cutSpec:{shape:'丝',length:10,width:1,height:1,technique:'平刀'}},
{groupId:'g1',alias:'里脊肉丝',specAlias:'粗丝',yieldRate:'90',cutSpec:{shape:'丝',length:10,width:2,height:2,technique:'直刀'}},
{groupId:'g2',alias:'里脊肉片',specAlias:'薄片',yieldRate:'95',cutSpec:{shape:'片',length:3,width:1,height:5,technique:'平刀'}},
{groupId:'g3',alias:'里脊肉块',specAlias:'方块',yieldRate:'92',cutSpec:{shape:'块',length:20,width:20,height:20,technique:'直刀'}}
],
nutri:{calorie:155,protein:20.2,fat:7.9,carbohydrate:0.7,dietary_fiber:0,
vitamin_a:6,vitamin_b1:0.54,vitamin_b2:0.1,vitamin_b6:0.33,vitamin_b12:0.6,vitamin_c:0,vitamin_d:0,vitamin_e:0.34,vitamin_k:0,niacin:5.3,folic_acid:3,biotin:0,pantothenic_acid:0.8,total_choline:0,
ca:6,phosphorus:196,kalium:305,mg:25,na:43.2,fe:1.5,zn:2.3,se:11.97,cu:0.09,mn:0.01,iodine:0,
@@ -392,7 +455,15 @@ const FOODS = [
gi:0,purine:132.6,guanine:28.5,adenine:42.3,hypoxanthine:51.2,xanthine:10.6},
color:{main:'粉红色',desc:'鲜嫩粉红,有光泽,纹理细腻',cause:'肌红蛋白含量适中',grade:'优',crowd:[{name:'成人',level:'推荐'},{name:'老年人',level:'适量'}],note:'老年人消化负担稍重'},
taste:{main:'鲜味',sub:'淡咸',intensity:'中',desc:'肉香鲜美,脂肪少,口感纯正',crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'},{name:'肾病人群',level:'慎食'}],note:'肾病人群需控制蛋白质摄入'},
texture:{main:'嫩滑',chew:'中',digest:'中',desc:'肌纤维细腻,炒制后滑嫩,少筋膜',crowd:[{name:'成人',level:'推荐'},{name:'老年人',level:'适量'},{name:'儿童',level:'推荐'}],cook:'老年人建议炖煮至软烂,避免大块'}},
texture:{main:'嫩滑',chew:'中',digest:'中',desc:'肌纤维细腻,炒制后滑嫩,少筋膜',crowd:[{name:'成人',level:'推荐'},{name:'老年人',level:'适量'},{name:'儿童',level:'推荐'}],cook:'老年人建议炖煮至软烂,避免大块'},
history:[
{type:'新增',title:'创建食材条目',time:'2026-01-12 09:25',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'从系统内置食物成分库导入,初始化基础档案。'},
{type:'编辑',title:'扩充净菜切配规格',time:'2026-02-20 15:48',user:'李建国',role:'营养师',dept:'营养管理部',summary:'新增里脊肉片(薄片)、里脊肉块(方块)两个切配组。',changes:[{field:'净菜类型',old:'2 项(丝类)',new:'4 项(丝/片/块)'}]},
{type:'价格调整',title:'更新基础定价',time:'2026-04-02 09:10',user:'张明',role:'采购员',dept:'采购部',summary:'清明后猪肉批发价上调,同步调整食材定价。',changes:[{field:'基础定价',old:'16.5 元/100g',new:'18.8 元/100g'}]},
{type:'编辑',title:'修正嘌呤数据',time:'2026-05-18 13:20',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'对齐《中国食物成分表》标准,修正嘌呤相关指标。',changes:[{field:'总嘌呤',old:'118.5 mg',new:'132.6 mg'},{field:'次黄嘌呤',old:'42.1 mg',new:'51.2 mg'}]},
{type:'价格调整',title:'更新基础定价',time:'2026-07-09 17:00',user:'张明',role:'采购员',dept:'采购部',summary:'夏季猪肉行情稳定,微调定价。',changes:[{field:'基础定价',old:'19.2 元/100g',new:'18.8 元/100g'}]},
{type:'编辑',title:'调整慎食人群说明',time:'2026-08-01 10:15',user:'李建国',role:'营养师',dept:'营养管理部',summary:'细化肾病人群慎食提示。',changes:[{field:'人群备注',old:'肾病患者慎食',new:'肾病人群需控制蛋白质摄入'}]}
]},
{name:'菠菜',code:'0701001',alias:'菠菜',firstClass:'蔬菜类',secondClass:'叶菜类',icon:'🥬',edible:'89',waterRate:'91.2',dataSource:0,basePrice:'4.2',vegTypes:[],
nutri:{calorie:24,protein:2.6,fat:0.3,carbohydrate:3.1,dietary_fiber:1.7,
vitamin_a:487,vitamin_b1:0.04,vitamin_b2:0.11,vitamin_b6:0.1,vitamin_b12:0,vitamin_c:32,vitamin_d:0,vitamin_e:1.74,vitamin_k:483,niacin:0.6,folic_acid:110,biotin:0,pantothenic_acid:0.2,total_choline:0,
@@ -402,7 +473,13 @@ const FOODS = [
gi:15,purine:23.0,guanine:5.1,adenine:8.2,hypoxanthine:7.3,xanthine:2.4},
color:{main:'深绿色',desc:'翠绿鲜亮,叶片厚实,颜色均匀',cause:'叶绿素含量丰富,含胡萝卜素',grade:'优',crowd:[{name:'成人',level:'推荐'},{name:'孕妇',level:'推荐'},{name:'贫血人群',level:'推荐'}],note:'富含叶酸,孕妇适宜'},
taste:{main:'淡味',sub:'微涩',intensity:'弱',desc:'轻微涩感(草酸),汆烫后消失',crowd:[{name:'全人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],note:'肾结石人群慎食,草酸含量高'},
texture:{main:'嫩软',chew:'易',digest:'易',desc:'叶片薄嫩,焯水后柔软,汁水丰富',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'婴幼儿',level:'适量'}],cook:'婴幼儿建议打泥处理,去除纤维'}},
texture:{main:'嫩软',chew:'易',digest:'易',desc:'叶片薄嫩,焯水后柔软,汁水丰富',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'婴幼儿',level:'适量'}],cook:'婴幼儿建议打泥处理,去除纤维'},
history:[
{type:'新增',title:'创建食材条目',time:'2026-01-12 09:26',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'从系统内置食物成分库导入。'},
{type:'编辑',title:'完善叶酸数据',time:'2026-03-02 14:30',user:'李建国',role:'营养师',dept:'营养管理部',summary:'对齐最新检测数据,叶酸含量上调。',changes:[{field:'叶酸',old:'87 μg',new:'110 μg'}]},
{type:'价格调整',title:'更新基础定价',time:'2026-05-20 11:00',user:'张明',role:'采购员',dept:'采购部',summary:'夏季本地菠菜集中上市,定价下调。',changes:[{field:'基础定价',old:'5.5 元/100g',new:'4.2 元/100g'}]},
{type:'编辑',title:'调整慎食提示',time:'2026-06-12 09:40',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'明确草酸含量高的人群风险提示。',changes:[{field:'人群备注',old:'—',new:'肾结石人群慎食,草酸含量高'}]}
]},
{name:'鸡蛋',code:'0401001',alias:'鸡蛋',firstClass:'蛋类',secondClass:'鸡蛋',icon:'🥚',edible:'88',waterRate:'74.1',dataSource:0,basePrice:'6.5',vegTypes:[],
nutri:{calorie:144,protein:13.3,fat:8.8,carbohydrate:2.8,dietary_fiber:0,
vitamin_a:234,vitamin_b1:0.11,vitamin_b2:0.27,vitamin_b6:0.11,vitamin_b12:1.0,vitamin_c:0,vitamin_d:2.2,vitamin_e:1.06,vitamin_k:0,niacin:0.2,folic_acid:70,biotin:20,pantothenic_acid:1.3,total_choline:294,
@@ -412,8 +489,17 @@ const FOODS = [
gi:0,purine:3.7,guanine:0.8,adenine:1.2,hypoxanthine:1.1,xanthine:0.6},
color:{main:'米黄色',desc:'蛋黄橙黄,蛋白透明,整体色泽亮',cause:'蛋黄富含类胡萝卜素',grade:'优',crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'},{name:'高血脂人群',level:'慎食'}],note:'高血脂人群每日不超过1个'},
taste:{main:'鲜味',sub:'淡味',intensity:'中',desc:'蛋黄浓郁,蛋白清淡,整体鲜香',crowd:[{name:'成人',level:'推荐'},{name:'儿童',level:'推荐'}],note:'优质蛋白,味道适中'},
texture:{main:'嫩滑',chew:'易',digest:'易',desc:'水煮蛋弹嫩,炒蛋软嫩,蒸蛋细腻',crowd:[{name:'全人群',level:'推荐'},{name:'婴幼儿',level:'推荐'},{name:'老年人',level:'推荐'}],cook:'蒸蛋口感最软,适合各年龄段'}},
{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',icon:'🧈',edible:'100',waterRate:'82.8',dataSource:1,basePrice:'5.0',vegTypes:[{name:'豆腐块',yieldRate:'98',cutSpec:{shape:'块(中)',length:30,width:30,height:20,technique:'直刀'}}],
texture:{main:'嫩滑',chew:'易',digest:'易',desc:'水煮蛋弹嫩,炒蛋软嫩,蒸蛋细腻',crowd:[{name:'全人群',level:'推荐'},{name:'婴幼儿',level:'推荐'},{name:'老年人',level:'推荐'}],cook:'蒸蛋口感最软,适合各年龄段'},
history:[
{type:'新增',title:'创建食材条目',time:'2026-01-12 09:30',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'从系统内置食物成分库导入。'},
{type:'编辑',title:'完善生物素与胆碱',time:'2026-02-25 16:00',user:'李建国',role:'营养师',dept:'营养管理部',summary:'补充生物素、总胆碱等 4 项指标。',changes:[{field:'生物素',old:'—',new:'20 μg'},{field:'总胆碱',old:'—',new:'294 mg'}]},
{type:'价格调整',title:'更新基础定价',time:'2026-04-18 10:20',user:'张明',role:'采购员',dept:'采购部',summary:'蛋价季节性波动,微调。',changes:[{field:'基础定价',old:'7.0 元/100g',new:'6.5 元/100g'}]},
{type:'编辑',title:'补充碘含量',time:'2026-07-04 14:50',user:'王秀兰',role:'营养师',dept:'营养管理部',summary:'补充碘元素数据,完善矿物质维度。',changes:[{field:'碘',old:'—',new:'27.2 μg'}]}
]},
{name:'豆腐(北)',code:'0601001',alias:'北豆腐',firstClass:'豆及豆制品',secondClass:'豆制品',icon:'🧈',edible:'100',waterRate:'82.8',dataSource:1,basePrice:'5.0',vegTypes:[
{groupId:'g1',alias:'块类组',specAlias:'大方块',yieldRate:'96',cutSpec:{shape:'块',length:30,width:30,height:20,technique:'直刀'}},
{groupId:'g1',alias:'块类组',specAlias:'中方块',yieldRate:'98',cutSpec:{shape:'块',length:20,width:20,height:15,technique:'直刀'}}
],
nutri:{calorie:98,protein:12.2,fat:4.8,carbohydrate:1.5,dietary_fiber:0.4,
vitamin_a:0,vitamin_b1:0.07,vitamin_b2:0.04,vitamin_b6:0.05,vitamin_b12:0,vitamin_c:0,vitamin_d:0,vitamin_e:2.71,vitamin_k:0,niacin:0.1,folic_acid:37,biotin:0,pantothenic_acid:0.1,total_choline:0,
ca:138,phosphorus:97,kalium:106,mg:63,na:7.2,fe:2.5,zn:1.11,se:2.3,cu:0.27,mn:0.47,iodine:0,
@@ -422,7 +508,14 @@ const FOODS = [
gi:32,purine:68.6,guanine:15.2,adenine:22.1,hypoxanthine:24.8,xanthine:6.5},
color:{main:'乳白色',desc:'洁白细腻,质地均匀,有光泽',cause:'大豆蛋白凝固,含水量高',grade:'优',crowd:[{name:'全人群',level:'推荐'},{name:'高血压人群',level:'推荐'}],note:'低脂优质蛋白,适合多数人群'},
taste:{main:'淡味',sub:'豆香',intensity:'弱',desc:'清淡豆香,口感温和,无刺激性',crowd:[{name:'全人群',level:'推荐'},{name:'痛风人群',level:'适量'}],note:'痛风急性期适量,嘌呤含量偏高'},
texture:{main:'细腻',chew:'易',digest:'易',desc:'质地细腻滑嫩,入口即化,无渣感',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],cook:'细腻易消化,适合消化功能弱者'}}
texture:{main:'细腻',chew:'易',digest:'易',desc:'质地细腻滑嫩,入口即化,无渣感',crowd:[{name:'全人群',level:'推荐'},{name:'老年人',level:'推荐'},{name:'术后恢复',level:'推荐'}],cook:'细腻易消化,适合消化功能弱者'},
history:[
{type:'新增',title:'手动添加食材条目',time:'2026-02-15 10:00',user:'陈晓燕',role:'营养师',dept:'营养管理部',summary:'手动新增「豆腐(北)」食材档案。'},
{type:'编辑',title:'补充净菜规格',time:'2026-03-08 15:20',user:'陈晓燕',role:'营养师',dept:'营养管理部',summary:'新增块类组两个规格(大方块/中方块)。',changes:[{field:'净菜类型',old:'0 项',new:'2 项(大方块/中方块)'}]},
{type:'价格调整',title:'更新基础定价',time:'2026-05-12 09:30',user:'张明',role:'采购员',dept:'采购部',summary:'豆制品批发价上调。',changes:[{field:'基础定价',old:'4.5 元/100g',new:'5.0 元/100g'}]},
{type:'编辑',title:'补充痛风人群提示',time:'2026-06-28 11:45',user:'李建国',role:'营养师',dept:'营养管理部',summary:'新增痛风急性期适量提示。',changes:[{field:'人群备注',old:'—',new:'痛风急性期适量,嘌呤含量偏高'}]},
{type:'编辑',title:'修订矿物质数据',time:'2026-08-05 14:00',user:'陈晓燕',role:'营养师',dept:'营养管理部',summary:'对齐《中国食物成分表》修正钙、镁数据。',changes:[{field:'钙',old:'105 mg',new:'138 mg'},{field:'镁',old:'52 mg',new:'63 mg'}]}
]}
];
let currentFood = 1;
@@ -453,13 +546,15 @@ function levelTag(level){
function renderDetail(){
const f = FOODS[currentFood];
const body = document.getElementById('detail-body');
const editLink = `<div style="text-align:right;margin-bottom:12px;display:flex;justify-content:flex-end;gap:16px;"><a class="link" href="din-ingredient-add.html?mode=edit&code=${f.code}&tab=${currentTab}">编辑</a><a class="link" style="color:var(--danger);" onclick="openDeleteFoodDialog()">删除食材</a></div>`;
// 操作历史是只读视图,不显示编辑/删除入口
const editLink = currentTab===5 ? '' : `<div style="text-align:right;margin-bottom:12px;display:flex;justify-content:flex-end;gap:16px;"><a class="link" href="din-ingredient-add.html?mode=edit&code=${f.code}&tab=${currentTab}">编辑</a><a class="link" style="color:var(--danger);" onclick="openDeleteFoodDialog()">删除食材</a></div>`;
let content = '';
if(currentTab===0) content = renderBasicInfo(f);
else if(currentTab===1) content = renderNutri(f);
else if(currentTab===2) content = renderTaste(f);
else if(currentTab===3) content = renderTexture(f);
else content = renderColor(f);
else if(currentTab===4) content = renderColor(f);
else content = renderHistory(f);
body.innerHTML = editLink + content;
}
@@ -473,11 +568,50 @@ function renderBasicInfo(f){
html += `<div class="profile-field"><div class="profile-field__label">可食部分</div><div class="profile-field__value">${f.edible}%</div></div>`;
html += `<div class="profile-field"><div class="profile-field__label">水分</div><div class="profile-field__value">${f.waterRate}%</div></div>`;
html += `<div class="profile-field"><div class="profile-field__label">数据来源</div><div class="profile-field__value">${sourceLabel}</div></div>`;
html += `<div class="profile-field"><div class="profile-field__label">基础定价</div><div class="profile-field__value">${f.basePrice ? f.basePrice + ' 元/100g' : ''}</div></div>`;
const priceHint = f.basePrice ? `<span style="font-size:var(--font-xs);color:var(--text-placeholder);margin-left:8px;font-weight:400;">2026-06-05 手工录入)</span>` : '';
html += `<div class="profile-field"><div class="profile-field__label">基础定价</div><div class="profile-field__value">${f.basePrice ? f.basePrice + ' 元/100g' : '—'}${priceHint}</div></div>`;
let vegTypesValue;
if(f.vegTypes && f.vegTypes.length > 0){
let rows = f.vegTypes.map(t=>`<tr><td>${t.name||'—'}</td><td>${t.yieldRate!==undefined&&t.yieldRate!==''?t.yieldRate+'%':'—'}</td><td>${formatCutSpec(t.cutSpec)}</td></tr>`).join('');
vegTypesValue = `<table class="veg-type-table"><thead><tr><th>净菜名称</th><th>菜品出净率</th><th>切配规格</th></tr></thead><tbody>${rows}</tbody></table>`;
/* 按 groupId 分组,同组多规格合并前 4 列(净菜标准名/净菜俗名/出净率/切配形状) */
var groupKeys = [];
var groupMap = {};
f.vegTypes.forEach(function(it){
var k = it.groupId || ('auto_'+it.shapeCat);
if(!groupMap[k]){groupKeys.push(k);groupMap[k]=[];}
groupMap[k].push(it);
});
let rows = '';
groupKeys.forEach(function(k){
var items = groupMap[k];
var first = items[0];
var firstCs = first.cutSpec || {};
var vegName = f.name + (firstCs.shape ? '-'+firstCs.shape : '');
var vegAlias = first.alias || '—';
var rs = items.length;
items.forEach(function(item,si){
var yieldRate = (item.yieldRate!==undefined && item.yieldRate!=='') ? item.yieldRate+'%' : '—';
var cs = item.cutSpec || {};
var cutShape = cs.shape || '—';
var cutSpecTxt = formatCutSpecNoShape(item.cutSpec);
var specAlias = item.specAlias || '—';
if(si===0){
rows += `<tr>`;
rows += `<td class="veg-type__name" rowspan="${rs}">${vegName}</td>`;
rows += `<td class="veg-type__groupalias" rowspan="${rs}">${vegAlias}</td>`;
rows += `<td rowspan="${rs}">${yieldRate}</td>`;
rows += `<td rowspan="${rs}">${cutShape}</td>`;
rows += `<td>${cutSpecTxt}</td>`;
rows += `<td class="veg-type__specalias">${specAlias}</td>`;
rows += `</tr>`;
} else {
rows += `<tr>`;
rows += `<td>${cutSpecTxt}</td>`;
rows += `<td class="veg-type__specalias">${specAlias}</td>`;
rows += `</tr>`;
}
});
});
vegTypesValue = `<table class="veg-type-table"><thead><tr><th>净菜标准名</th><th>净菜俗名</th><th>出净率</th><th>切配形状</th><th>切配规格</th><th>规格别名</th></tr></thead><tbody>${rows}</tbody></table>`;
} else {
vegTypesValue = '—';
}
@@ -604,6 +738,72 @@ function renderCrowd(crowd, note){
return html;
}
/* === 操作历史 === */
// 操作类型标签配色
function historyOpTag(type){
const map={
'新增':{cls:'tag-green',icon:''},
'编辑':{cls:'tag-blue',icon:'✏️'},
'启用':{cls:'tag-green',icon:'✓'},
'禁用':{cls:'tag-gray',icon:'⏸'},
'价格调整':{cls:'tag-orange',icon:'💰'},
'删除':{cls:'tag-red',icon:'🗑'},
'恢复':{cls:'',icon:'♻',style:'background:#F9F0FF;color:#722ED1;'}
};
const m = map[type] || {cls:'tag-gray',icon:'•'};
const styleAttr = m.style ? ` style="${m.style}"` : '';
return `<span class="tag ${m.cls}"${styleAttr}>${m.icon} ${type}</span>`;
}
// 渲染操作历史:筛选条 + 时间轴
function renderHistory(f){
const list = f.history || [];
// 筛选条
let html = `<div class="history-filter">
<select id="historyOpType"><option>全部操作</option><option>新增</option><option>编辑</option><option>启用</option><option>禁用</option><option>价格调整</option><option>删除</option><option>恢复</option></select>
<input type="text" placeholder="操作时间范围(YYYY-MM-DD ~ YYYY-MM-DD)" style="flex:1;min-width:260px;">
<input type="text" placeholder="操作人" style="width:140px;">
<button class="btn btn-primary">查询</button>
<button class="btn btn-default">重置</button>
</div>`;
// 时间轴列表
if(list.length===0){
html += `<div class="history-empty">该食材暂无操作历史记录</div>`;
} else {
html += `<div class="history-timeline">`;
list.forEach(function(h){
const opClassMap={'新增':'create','编辑':'edit','启用':'edit','禁用':'disable','价格调整':'price','删除':'delete','恢复':'restore'};
const opCls = opClassMap[h.type] || 'edit';
html += `<div class="history-item history-item--${opCls}">`;
html += `<div class="history-card">`;
html += `<div class="history-card__head">`;
html += historyOpTag(h.type);
html += `<span class="history-card__op">${h.title || ''}</span>`;
html += `<span class="history-card__time">${h.time}</span>`;
html += `</div>`;
html += `<div class="history-card__user">👤 ${h.user} · ${h.role || '操作员'}${h.dept?(' · '+h.dept):''}</div>`;
if(h.summary) html += `<div class="history-card__summary">${h.summary}</div>`;
// 变更明细
if(h.changes && h.changes.length>0){
html += `<div class="history-card__changes">`;
h.changes.forEach(function(c){
html += `<div class="history-change">`;
html += `<div class="history-change__field">${c.field}</div>`;
html += `<div class="history-change__old">${c.old || '—'}</div>`;
html += `<div class="history-change__new">${c.new || '—'}</div>`;
html += `</div>`;
});
html += `</div>`;
}
html += `</div></div>`;
});
html += `</div>`;
}
return html;
}
/* 级联分类选择器 */
var CATEGORIES={'谷薯类':['稻米','小麦','玉米','薯类','杂粮'],'畜禽肉类':['猪肉','牛肉','羊肉','鸡肉','鸭肉'],'水产类':['淡水鱼','海水鱼','虾蟹','贝类'],'蛋类':['鸡蛋','鸭蛋','鹅蛋','鹌鹑蛋'],'奶及奶制品':['牛奶','酸奶','奶酪','奶粉'],'豆及豆制品':['大豆','豆制品','杂豆'],'蔬菜类':['叶菜类','根茎类','瓜果类','菌菇类','豆荚类'],'水果类':['仁果类','浆果类','柑橘类','瓜果类','热带水果'],'坚果种子类':['坚果','种子'],'油脂类':['植物油','动物油'],'调味品类':['酱料','香辛料','调味汁']};
var cascadeL1Active='';
+25
View File
@@ -1,5 +1,30 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-14] optimize: 餐品/食材档案检索与录入体系强化 + 进货明细金额补全
### 变更一:餐品库高级搜索与筛选汇总(`din-dish-all.html` / `din-dish-own.html`)
- 新增「⚙ 高级搜索」弹窗:支持餐品标签、上架状态等多条件组合筛选,与顶部快速搜索(名称/编号 + 分类/状态下拉)联动
- 新增筛选条件汇总条:展示已应用的筛选条件(名称 · 分类 · 标签 · 状态等),支持一键清空
- 列表渲染改为基于 `filteredDishes` 过滤结果,新增"未找到匹配的餐品"空状态提示
### 变更二:餐品详情操作历史(`din-dish-own.html`)
- 详情弹窗新增「🕘 操作历史」Tab:时间轴展示餐品的创建/停用等操作记录,含操作人、角色、部门、摘要与字段变更对比
### 变更三:食材录入交互重构(`din-dish-add.html`)
- 「新增食材」弹窗改为行内食材选择器:支持关键词搜索过滤、已选食材去重(不可重复添加)、选中即加行
- 食材行支持行内编辑:食材类型(主材/辅材/调料)、净菜类型、净菜规格联动、用量输入
- 规格表新增「净菜包毛利率」字段(0~100%,与净菜包售价并列);详情规格表同步增加毛利率列
### 变更四:食材档案详情增强(`din-ingredient.html`)
- 详情弹窗新增「🕘 操作历史」Tab:操作类型筛选条 + 时间轴,操作历史为只读视图(不显示编辑/删除入口)
- 净菜类型展示升级为 6 列结构表:净菜标准名 / 净菜俗名 / 出净率 / 切配形状 / 切配规格 / 规格别名
### 变更五:食材编辑蔬菜类型结构化(`din-ingredient-add.html`)
- 蔬菜净菜类型编辑区改为结构化表格(`veg-table`):同食材多规格行共享合并单元格的食材名列,行内编辑俗名/出净率/切配形状/规格尺寸/刀法,支持增删行
### 变更六:进货明细金额补全(`farm-purchase.html` / `sup-in-raw.html`)
- 列表新增「金额(元)」「单价(元/kg)」两列,补齐采购成本数据口径
## [2026-08-12] optimize: 净菜物流列简化 + 配送时间轴 + 收货码角色可见性
### 变更背景
+42 -39
View File
@@ -309,10 +309,10 @@ 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></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>散装(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>散装(kg)</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>¥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>
</tbody>
</table>
<div class="sub-table__footer">合计:2 种食材,200 kg,¥660</div>
@@ -335,11 +335,11 @@ 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></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>散装(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>散装(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>散装(kg)</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>¥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>
</tbody>
</table>
<div class="sub-table__footer">合计:3 种食材,250 kg,¥485</div>
@@ -362,10 +362,10 @@ 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></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>25kg/</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>25kg/</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>¥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>
</tbody>
</table>
<div class="sub-table__footer">合计:2 种食材,320 kg,¥8,480</div>
@@ -388,11 +388,11 @@ 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></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>散装(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>散装(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>散装(kg)</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>¥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>
</tbody>
</table>
<div class="sub-table__footer">合计:3 种食材,190 kg,¥3,860</div>
@@ -415,9 +415,9 @@ 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></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>20kg/</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-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>
</tbody>
</table>
@@ -441,10 +441,10 @@ 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></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>散装(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>散装(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-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>
</tbody>
</table>
<div class="sub-table__footer" style="color:var(--warning);">合计:2 种食材,200 kg,单价待补充</div>
@@ -498,7 +498,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
</div>
<table class="items-table" 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 style="width:70px;">小计金额</th><th>备注</th><th style="width:36px;"></th></tr>
</thead>
<tbody id="itemsBody"></tbody>
</table>
@@ -535,7 +535,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></tr></thead>
<tbody id="drawerItemsBody"></tbody>
</table>
</div>
@@ -650,7 +650,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<p style="font-size:var(--font-xs);color:var(--text-secondary);margin-bottom:16px;">请逐项填写验收数量,确认无误后再提交收货</p>
<table class="sub-table" style="width:100%;">
<thead>
<tr><th style="width:40px;">序号</th><th>食材名称</th><th>规格</th><th style="width:90px;">采购数量</th><th style="width:110px;">验收数量</th><th>备注</th></tr>
<tr><th style="width:40px;">序号</th><th>食材名称</th><th>包装规格</th><th style="width:90px;">采购数量</th><th style="width:110px;">验收数量</th><th>备注</th></tr>
</thead>
<tbody id="receiveItemsBody"></tbody>
</table>
@@ -686,12 +686,15 @@ function renderUnitOptions(selectEl,selected){
var html='<option value="">请选择</option>'+SPEC_UNIT_OPTIONS.map(function(u){return '<option'+(u===selected?' selected':'')+'>'+u+'</option>';}).join('');
selectEl.innerHTML=html;
}
/* 解析列表展示用的规格字符串(如 "25kg/" → {gram:25,unit:''}) */
/* 解析包装规格字符串(如 "500g/袋" → {gram:500,unit:'袋'}、"25kg/" → {gram:25000,unit:''}) */
function parseSpecText(specText){
if(!specText)return {gram:'',unit:''};
var m=specText.match(/^([\d.]+)kg\/(.+)$/);
if(m)return {gram:parseFloat(m[1]),unit:m[2]};
if(specText==='散装(kg)')return {gram:'',unit:'散装'};
/* 兼容 g 与 kg 两种克重单位,统一换算为克 */
var m=specText.match(/^([\d.]+)(g|kg)\/(.+)$/);
if(m){
var val=parseFloat(m[1]);
return {gram: m[2]==='kg'?val*1000:val, unit:m[3]};
}
return {gram:'',unit:''};
}
@@ -701,40 +704,40 @@ var orders = [
supplier:'绿源蔬菜配送', supplierContact:'李明', supplierPhone:'138-9912-3456',
date:'2026-04-16', arrival:'2026-04-17', source:'食谱搭配', status:'待接单', total:'¥660',
items:[
{trace:'ST-EX-LY01-VG-XLHU-080K-20260416-001',name:'西兰花',cat:'蔬菜类',qty:80,spec:'散装(kg)',price:4.5,subtotal:'¥360',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-QCAI-120K-20260416-002',name:'芹菜',cat:'蔬菜类',qty:120,spec:'散装(kg)',price:2.5,subtotal:'¥300',acceptedQty:'—',remark:'—'}
{trace:'ST-EX-LY01-VG-XLHU-080K-20260416-001',name:'西兰花',cat:'蔬菜类',qty:80,spec:'500g/袋',price:4.5,subtotal:'¥360',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-QCAI-120K-20260416-002',name:'芹菜',cat:'蔬菜类',qty:120,spec:'500g/袋',price:2.5,subtotal:'¥300',acceptedQty:'—',remark:'—'}
]},
{ no:'PO-20260415-001', canteen:'一号场所', canteenContact:'王建国', canteenPhone:'029-8812-3001',
supplier:'绿源蔬菜配送', supplierContact:'李明', supplierPhone:'138-9912-3456',
date:'2026-04-15', arrival:'2026-04-16', source:'食谱搭配', status:'待发货', total:'¥485',
items:[
{trace:'ST-EX-LY01-VG-BOCA-050K-20260415-002',name:'菠菜',cat:'蔬菜类',qty:50,spec:'散装(kg)',price:2.5,subtotal:'¥125',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-BCAI-080K-20260415-003',name:'白菜',cat:'蔬菜类',qty:80,spec:'散装(kg)',price:1.8,subtotal:'¥144',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-HLUB-120K-20260415-004',name:'胡萝卜',cat:'蔬菜类',qty:120,spec:'散装(kg)',price:1.8,subtotal:'¥216',acceptedQty:'—',remark:'—'}
{trace:'ST-EX-LY01-VG-BOCA-050K-20260415-002',name:'菠菜',cat:'蔬菜类',qty:50,spec:'500g/袋',price:2.5,subtotal:'¥125',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-BCAI-080K-20260415-003',name:'白菜',cat:'蔬菜类',qty:80,spec:'500g/袋',price:1.8,subtotal:'¥144',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-HLUB-120K-20260415-004',name:'胡萝卜',cat:'蔬菜类',qty:120,spec:'500g/袋',price:1.8,subtotal:'¥216',acceptedQty:'—',remark:'—'}
]},
{ no:'PO-20260415-002', canteen:'一号场所', canteenContact:'王建国', canteenPhone:'029-8812-3001',
supplier:'鲜肉直供公司', supplierContact:'张伟', supplierPhone:'139-0088-7654',
date:'2026-04-15', arrival:'2026-04-16', source:'食谱搭配', status:'待收货', total:'¥8,480',
deliveryMethod:'供应商自送', deliveryPerson:'张伟', deliveryPhone:'139-0088-7654', deliveryETA:'2026-04-16 14:00',
items:[
{trace:'ST-EX-XR01-MT-ZLRJ-080K-20260415-005',name:'猪里脊',cat:'肉禽类',qty:80,spec:'25kg/',price:28,subtotal:'¥2,240',acceptedQty:'—',remark:'需冷链运输'},
{trace:'ST-EX-XR01-MT-ZWUA-240K-20260415-006',name:'猪五花',cat:'肉禽类',qty:240,spec:'25kg/',price:26,subtotal:'¥6,240',acceptedQty:'—',remark:'—'}
{trace:'ST-EX-XR01-MT-ZLRJ-080K-20260415-005',name:'猪里脊',cat:'肉禽类',qty:80,spec:'500g/',price:28,subtotal:'¥2,240',acceptedQty:'—',remark:'需冷链运输'},
{trace:'ST-EX-XR01-MT-ZWUA-240K-20260415-006',name:'猪五花',cat:'肉禽类',qty:240,spec:'500g/',price:26,subtotal:'¥6,240',acceptedQty:'—',remark:'—'}
]},
{ no:'PO-20260414-003', canteen:'二号场所', canteenContact:'赵芳', canteenPhone:'029-8812-3002',
supplier:'海鲜水产配送', supplierContact:'陈浩', supplierPhone:'136-7766-5432',
date:'2026-04-14', arrival:'2026-04-15', source:'手动创建', status:'已完成', total:'¥3,860',
deliveryMethod:'第三方物流', deliveryPerson:'陈浩', deliveryPhone:'136-7766-5432', deliveryETA:'2026-04-15 10:30',
items:[
{trace:'ST-EX-HS01-SF-CAYO-060K-20260414-001',name:'草鱼',cat:'水产类',qty:60,spec:'散装(kg)',price:18,subtotal:'¥1,080',acceptedQty:'60',remark:'活鲜运输'},
{trace:'ST-EX-HS01-SF-JIYU-080K-20260414-002',name:'鲫鱼',cat:'水产类',qty:80,spec:'散装(kg)',price:16,subtotal:'¥1,280',acceptedQty:'80',remark:'—'},
{trace:'ST-EX-HS01-SF-XIAA-050K-20260414-003',name:'虾',cat:'水产类',qty:50,spec:'散装(kg)',price:30,subtotal:'¥1,500',acceptedQty:'—',remark:'冰鲜'}
{trace:'ST-EX-HS01-SF-CAYO-060K-20260414-001',name:'草鱼',cat:'水产类',qty:60,spec:'500g/袋',price:18,subtotal:'¥1,080',acceptedQty:'60',remark:'活鲜运输'},
{trace:'ST-EX-HS01-SF-JIYU-080K-20260414-002',name:'鲫鱼',cat:'水产类',qty:80,spec:'500g/袋',price:16,subtotal:'¥1,280',acceptedQty:'80',remark:'—'},
{trace:'ST-EX-HS01-SF-XIAA-050K-20260414-003',name:'虾',cat:'水产类',qty:50,spec:'500g/袋',price:30,subtotal:'¥1,500',acceptedQty:'—',remark:'冰鲜'}
]},
{ no:'PO-20260413-004', canteen:'三号场所', canteenContact:'刘洋', canteenPhone:'029-8812-3003',
supplier:'粮油批发商行', supplierContact:'孙强', supplierPhone:'137-5544-3210',
date:'2026-04-13', arrival:'2026-04-14', source:'手动创建', status:'已取消', total:'¥6,188',
cancelReason:'供应商无法供货',
items:[
{trace:'ST-EX-LY02-DG-DAMI-1000-20260413-001',name:'大米',cat:'粮油类',qty:1000,spec:'20kg/',price:3.5,subtotal:'¥3,500',acceptedQty:'—',remark:'东北五常大米'},
{trace:'ST-EX-LY02-DG-DAMI-1000-20260413-001',name:'大米',cat:'粮油类',qty:1000,spec:'25kg/',price:3.5,subtotal:'¥3,500',acceptedQty:'—',remark:'东北五常大米'},
{trace:'ST-EX-LY02-DG-HSYO-168K-20260413-002',name:'花生油',cat:'粮油类',qty:168,spec:'5kg/桶',price:16,subtotal:'¥2,688',acceptedQty:'—',remark:'—'}
]},
/* 待确认订单:系统自动生成,缺供应商与单价,需用户通过"立即确认"补全后流转为"待接单" */
@@ -742,8 +745,8 @@ var orders = [
supplier:'', supplierContact:'', supplierPhone:'',
date:'2026-04-16', arrival:'2026-04-17', source:'系统生成', status:'待确认', total:'待核算',
items:[
{trace:'ST-EX-LY01-VG-XLHU-080K-20260416-007',name:'西兰花',cat:'蔬菜类',qty:80,spec:'散装(kg)',price:null,subtotal:'—',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-QCAI-120K-20260416-008',name:'芹菜',cat:'蔬菜类',qty:120,spec:'散装(kg)',price:null,subtotal:'—',acceptedQty:'—',remark:'—'}
{trace:'ST-EX-LY01-VG-XLHU-080K-20260416-007',name:'西兰花',cat:'蔬菜类',qty:80,spec:'500g/袋',price:null,subtotal:'—',acceptedQty:'—',remark:'—'},
{trace:'ST-EX-LY01-VG-QCAI-120K-20260416-008',name:'芹菜',cat:'蔬菜类',qty:120,spec:'500g/袋',price:null,subtotal:'—',acceptedQty:'—',remark:'—'}
]}
];
+20 -7
View File
@@ -222,23 +222,23 @@ tbody tr:hover{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>入库重量(kg)</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>入库重量(kg)</th><th>入库总价(元)</th><th>入库单价(元/kg)</th><th>入库时间</th><th>存储方式</th><th>存放区域</th><th>操作人</th><th>操作</th>
</tr></thead>
<tbody>
<tr>
<td>IN-20260415-001</td><td><span class="tag tag-green">设备自动</span></td><td><div>毛菜验收秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">RW-SCALE-001</div></td><td>ST-FH-FA01-VG-BOCA-286K-20260415-001</td><td><span class="tag tag-blue">自产</span></td><td>菠菜</td><td>A区蔬菜基地</td><td>25kg/件</td><td>12件</td><td>286</td><td>2026-04-15 10:30</td><td><span class="tag tag-green">毛菜柜</span></td><td>8号柜-A01格口</td><td>张三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
<td>IN-20260415-001</td><td><span class="tag tag-green">设备自动</span></td><td><div>毛菜验收秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">RW-SCALE-001</div></td><td>ST-FH-FA01-VG-BOCA-286K-20260415-001</td><td><span class="tag tag-blue">自产</span></td><td>菠菜</td><td>A区蔬菜基地</td><td>25kg/件</td><td>12件</td><td>286</td><td>1001.00</td><td>3.50</td><td>2026-04-15 10:30</td><td><span class="tag tag-green">毛菜柜</span></td><td>8号柜-A01格口</td><td>张三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
</tr>
<tr>
<td>IN-20260415-002</td><td><span class="tag tag-green">设备自动</span></td><td><div>毛菜验收秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">RW-SCALE-002</div></td><td>ST-EX-LY01-VG-BOCA-050K-20260415-002</td><td><span class="tag tag-gray">外采</span></td><td>菠菜</td><td>绿源蔬菜配送</td><td>10kg/筐</td><td>5筐</td><td>50</td><td>2026-04-15 08:30</td><td><span class="tag tag-green">毛菜柜</span></td><td>8号柜-B03格口</td><td>李三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
<td>IN-20260415-002</td><td><span class="tag tag-green">设备自动</span></td><td><div>毛菜验收秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">RW-SCALE-002</div></td><td>ST-EX-LY01-VG-BOCA-050K-20260415-002</td><td><span class="tag tag-gray">外采</span></td><td>菠菜</td><td>绿源蔬菜配送</td><td>10kg/筐</td><td>5筐</td><td>50</td><td>200.00</td><td>4.00</td><td>2026-04-15 08:30</td><td><span class="tag tag-green">毛菜柜</span></td><td>8号柜-B03格口</td><td>李三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
</tr>
<tr>
<td>IN-20260415-003</td><td><span class="tag tag-orange">人工录入</span></td><td></td><td>ST-EX-XR01-MT-ZLRJ-080K-20260415-005</td><td><span class="tag tag-gray">外采</span></td><td>猪里脊</td><td>鲜肉直供公司</td><td>20kg/箱</td><td>4箱</td><td>80</td><td>2026-04-15 09:15</td><td><span class="tag tag-gray">仓库</span></td><td>B仓-肉类区-01</td><td>王三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
<td>IN-20260415-003</td><td><span class="tag tag-orange">人工录入</span></td><td></td><td>ST-EX-XR01-MT-ZLRJ-080K-20260415-005</td><td><span class="tag tag-gray">外采</span></td><td>猪里脊</td><td>鲜肉直供公司</td><td>20kg/箱</td><td>4箱</td><td>80</td><td>2800.00</td><td>35.00</td><td>2026-04-15 09:15</td><td><span class="tag tag-gray">仓库</span></td><td>B仓-肉类区-01</td><td>王三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
</tr>
<tr>
<td>IN-20260415-004</td><td><span class="tag tag-orange">人工录入</span></td><td></td><td>ST-EX-HS01-SF-CAYO-060K-20260414-001</td><td><span class="tag tag-gray">外采</span></td><td>草鱼</td><td>海鲜水产配送</td><td>散装(kg)</td><td>60kg</td><td>60</td><td>2026-04-15 09:45</td><td><span class="tag tag-gray">仓库</span></td><td>C仓-水产区-01</td><td>李三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
<td>IN-20260415-004</td><td><span class="tag tag-orange">人工录入</span></td><td></td><td>ST-EX-HS01-SF-CAYO-060K-20260414-001</td><td><span class="tag tag-gray">外采</span></td><td>草鱼</td><td>海鲜水产配送</td><td>散装(kg)</td><td>60kg</td><td>60</td><td>1080.00</td><td>18.00</td><td>2026-04-15 09:45</td><td><span class="tag tag-gray">仓库</span></td><td>C仓-水产区-01</td><td>李三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
</tr>
<tr>
<td>IN-20260416-005</td><td><span class="tag tag-green">设备自动</span></td><td><div>毛菜验收秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">RW-SCALE-003</div></td><td>ST-FH-FB01-VG-HLUB-420K-20260415-002</td><td><span class="tag tag-blue">自产</span></td><td>胡萝卜</td><td>B区根茎基地</td><td>25kg/件</td><td>17件</td><td>420</td><td>2026-04-16 11:00</td><td><span class="tag tag-gray">仓库</span></td><td>A仓-蔬菜区-03</td><td>张三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
<td>IN-20260416-005</td><td><span class="tag tag-green">设备自动</span></td><td><div>毛菜验收秤</div><div style="font-size:var(--font-xs);color:var(--text-secondary);">RW-SCALE-003</div></td><td>ST-FH-FB01-VG-HLUB-420K-20260415-002</td><td><span class="tag tag-blue">自产</span></td><td>胡萝卜</td><td>B区根茎基地</td><td>25kg/件</td><td>17件</td><td>420</td><td>1050.00</td><td>2.50</td><td>2026-04-16 11:00</td><td><span class="tag tag-gray">仓库</span></td><td>A仓-蔬菜区-03</td><td>张三丰</td><td class="td-actions"><span class="link" onclick="openVideo()">查看视频</span> <span class="link">编辑</span> <span class="link" onclick="showDetail(this)">详情</span> <span class="link" style="color:var(--danger)">删除</span></td>
</tr>
</tbody>
</table>
@@ -274,7 +274,9 @@ tbody tr:hover{background:#F8FBFF;}
</div>
</div>
<div class="form-item"><label>入库数量<span class="required">*</span></label><input type="text" placeholder="请输入入库数量"></div>
<div class="form-item"><label>入库重量(kg)<span class="required">*</span></label><input type="number" placeholder="请输入入库重量"></div>
<div class="form-item"><label>入库重量(kg)<span class="required">*</span></label><input id="f-weight" type="number" min="0" step="0.01" placeholder="请输入入库重量" oninput="calcUnitPrice()"></div>
<div class="form-item"><label>入库总价(元)<span class="required">*</span></label><input id="f-total-price" type="number" min="0" step="0.01" placeholder="请输入入库总价" oninput="calcUnitPrice()"></div>
<div class="form-item"><label>入库单价(元/kg)</label><input id="f-unit-price" type="text" placeholder="根据总价与重量自动计算" readonly style="background:#F5F5F5;color:#999;"></div>
<div class="form-item"><label>入库时间</label><input type="date"></div>
<div class="form-item"><label>存储方式<span class="required">*</span></label><select id="rawStorageTypeModal" onchange="updateRawAreaModalOptions()"><option>请选择</option><option>毛菜柜</option><option>仓库</option></select></div>
<div class="form-item"><label>所在区域<span class="required">*</span></label><select id="rawAreaModal"><option>请选择存储方式</option></select></div>
@@ -359,6 +361,17 @@ function updateRawAreaModalOptions(){
}
function openModal(){document.getElementById('modalOverlay').style.display='flex';}
function closeModal(){document.getElementById('modalOverlay').style.display='none';}
/* 入库单价 = 总价 / 重量,重量为 0 或为空时不计算 */
function calcUnitPrice(){
var weight=parseFloat(document.getElementById('f-weight').value);
var total=parseFloat(document.getElementById('f-total-price').value);
var unitEl=document.getElementById('f-unit-price');
if(!weight||weight<=0||isNaN(weight)||!total||isNaN(total)){
unitEl.value='';
return;
}
unitEl.value=(total/weight).toFixed(2);
}
document.getElementById('modalOverlay').addEventListener('click',function(e){if(e.target===this)closeModal();});
function openVideo(){document.getElementById('videoOverlay').style.display='flex';}
function closeVideo(){document.getElementById('videoOverlay').style.display='none';}