feat: 餐品定价规格拆分为餐品规格与餐品净菜包规格

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-09-14 15:45:34 +08:00
co-authored by Claude Opus 4.7
parent bf08a9614b
commit 39547a8bb6
4 changed files with 116 additions and 35 deletions
+64 -20
View File
@@ -607,9 +607,16 @@ 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>默认</th><th style="width:60px;">操作</th></tr></thead>
<thead><tr><th style="width:200px;">规格(克重/份型)</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="7" style="text-align:center;"> 添加规格</td></tr>
<tr class="spec-add-row" onclick="addSpecRow(this)"><td colspan="5" style="text-align:center;"> 添加规格</td></tr>
</tbody>
</table>
<div class="form-subtitle">🥬 餐品净菜包规格</div>
<table class="spec-table" id="cleanSpecTable">
<thead><tr><th style="width:200px;">规格(克重/份型)</th><th>净菜包毛利率(%)</th><th>净菜包售价(元/份)</th><th>上架状态</th><th>默认</th><th style="width:60px;">操作</th></tr></thead>
<tbody id="cleanSpecBody">
<tr class="spec-add-row" onclick="addCleanSpecRow(this)"><td colspan="6" style="text-align:center;"> 添加净菜包规格</td></tr>
</tbody>
</table>`,
@@ -733,10 +740,17 @@ function buildPortionOptions(selected){
return html;
}
function addSpecRow(addRow){
if(document.querySelector('.spec-edit-row')) return;
if(document.querySelector('#specBody .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="净菜包毛利率" 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>';
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><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 addCleanSpecRow(addRow){
if(document.querySelector('#cleanSpecBody .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="净菜包毛利率" 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="confirmCleanSpec(this)">确定</button> <button class="btn-text btn-text--danger" onclick="cancelSpec(this)">取消</button></td>';
addRow.parentNode.insertBefore(tr,addRow);
}
function confirmSpec(btn){
@@ -745,21 +759,35 @@ function confirmSpec(btn){
var gramInput=group.querySelector('input[type="number"]');
var sel=group.querySelector('select');
var priceInput=row.querySelectorAll('input[type="number"]')[1];
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>'+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>';
row.innerHTML='<td>'+specViewHtml+'</td><td>'+parseFloat(price).toFixed(2)+'</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 confirmCleanSpec(btn){
var row=btn.closest('tr');
var group=row.querySelector('.spec-input-group');
var gramInput=group.querySelector('input[type="number"]');
var sel=group.querySelector('select');
var cleanMarginInput=row.querySelectorAll('input[type="number"]')[1];
var cleanPriceInput=row.querySelectorAll('input[type="number"]')[2];
var portion=sel?sel.value:'';
var gram=gramInput?gramInput.value.trim():'';
var cleanMargin=cleanMarginInput?cleanMarginInput.value.trim():'';
var cleanPrice=cleanPriceInput?cleanPriceInput.value.trim():'';
if(!portion||portion==='请选择'||!gram||!cleanMargin||!cleanPrice){alert('请填写完整信息');return;}
var cleanMarginText = parseFloat(cleanMargin).toFixed(1)+'%';
var cleanPriceText = 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>'+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');
@@ -1011,13 +1039,13 @@ function recalcAfterIngredientChange(){
}
function cancelSpec(btn){btn.closest('tr').remove();}
function toggleDefault(el){
var tbody=document.getElementById('specBody');
var tbody=el.closest('tbody');
if(tbody){tbody.querySelectorAll('.spec-toggle').forEach(function(t){t.classList.remove('active');});}
el.classList.add('active');
}
function removeSpec(btn){
var row=btn.closest('tr');var wasDefault=row.querySelector('.spec-toggle.active');row.remove();
if(wasDefault){var first=document.querySelector('#specBody tr:not(.spec-add-row):not(.spec-edit-row) .spec-toggle');if(first)first.classList.add('active');}
var row=btn.closest('tr');var tbody=row.closest('tbody');var wasDefault=row.querySelector('.spec-toggle.active');row.remove();
if(wasDefault){var first=tbody.querySelector('tr:not(.spec-add-row):not(.spec-edit-row) .spec-toggle');if(first)first.classList.add('active');}
}
/* 食材清单数据:每个食材含 code + 多个净菜(netName 净菜形式,本身可含-分隔符) + 净菜规格别名(aliases 多规格时需先选净菜再选规格) */
@@ -1251,7 +1279,8 @@ 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,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}],
specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,sale:true,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,sale:true,isDefault:false},{portion:'大份',baseGram:300,unit:'份',price:18,sale:false,isDefault:false}],
cleanSpecs:[{portion:'标准份',baseGram:200,unit:'份',cleanMargin:55.0,cleanPrice:9,cleanSale:false,cleanDefault:true},{portion:'小份',baseGram:120,unit:'份',cleanMargin:50.0,cleanPrice:6,cleanSale:false,cleanDefault:false},{portion:'大份',baseGram:300,unit:'份',cleanMargin:58.0,cleanPrice:13,cleanSale:false,cleanDefault:false}],
cookedWeight:200,
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.大火收汁'},
@@ -1294,7 +1323,8 @@ if(pageMode==='convert' && INNOV){
EDIT_DATA.tag = INNOV.tag;
EDIT_DATA.status = '上架';
EDIT_DATA.crowd = [];
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.specs = [{portion:'标准份',baseGram:350,unit:'份',price:15,sale:true,isDefault:true},{portion:'小份',baseGram:200,unit:'份',price:10,sale:false,isDefault:false}];
EDIT_DATA.cleanSpecs = [{portion:'标准份',baseGram:350,unit:'份',cleanMargin:52.0,cleanPrice:10,cleanSale:false,cleanDefault:true},{portion:'小份',baseGram:200,unit:'份',cleanMargin:48.0,cleanPrice:7,cleanSale:false,cleanDefault:false}];
EDIT_DATA.cookedWeight = INNOV.cookedWeight || 200;
EDIT_DATA.composition = INNOV.composition;
EDIT_DATA.cook = INNOV.cook;
@@ -1422,19 +1452,33 @@ function fillEditData(){
if(inputs[1]) inputs[1].value = d.pricing.profitRate||'';
if(inputs[2]) inputs[2].value = (d.pricing.ingredientCost||0).toFixed(2);
}
// 预填规格表
// 预填餐品规格表
var specBody = container.querySelector('#specTable tbody');
if(specBody){
specBody.innerHTML = '';
d.specs.forEach(function(s){
(d.specs||[]).forEach(function(s){
var tr = document.createElement('tr');
var saleActive = s.sale === true;
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>'+saleHtml+'</td><td><input type="checkbox"'+(s.isDefault?' checked':'')+'></td><td><span class="link" onclick="this.closest(\'tr\').remove()">删除</span></td>';
specBody.appendChild(tr);
});
}
// 预填餐品净菜包规格表
var cleanSpecBody = container.querySelector('#cleanSpecTable tbody');
if(cleanSpecBody){
cleanSpecBody.innerHTML = '';
(d.cleanSpecs||[]).forEach(function(s){
var tr = document.createElement('tr');
var saleActive = s.cleanSale === true;
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:'');
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);
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" 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.cleanDefault?' checked':'')+'></td><td><span class="link" onclick="this.closest(\'tr\').remove()">删除</span></td>';
cleanSpecBody.appendChild(tr);
});
}
} else if(currentStep===3){
+22 -8
View File
@@ -454,7 +454,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<script>
// 餐品数据
const DISHES = [
{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',
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,sale:true,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,sale:true,isDefault:false},{portion:'大份',baseGram:300,unit:'份',price:18,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:200,unit:'份',cleanMargin:55.0,cleanPrice:9,cleanSale:false,cleanDefault:true},{portion:'小份',baseGram:120,unit:'份',cleanMargin:50.0,cleanPrice:6,cleanSale:false,cleanDefault:false},{portion:'大份',baseGram:300,unit:'份',cleanMargin:58.0,cleanPrice:13,cleanSale:false,cleanDefault: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,
@@ -465,7 +465,7 @@ const DISHES = [
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,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',
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,sale:true,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:150,unit:'份',cleanMargin:60.0,cleanPrice:4.5,cleanSale:false,cleanDefault:true},{portion:'小份',baseGram:100,unit:'份',cleanMargin:55.0,cleanPrice:3,cleanSale:false,cleanDefault: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,
@@ -476,7 +476,7 @@ const DISHES = [
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,cleanMargin:42.0,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',updateTime:'2026-08-08',
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,sale:true,isDefault:true}],cleanSpecs:[{portion:'标准份',baseGram:300,unit:'份',cleanMargin:42.0,cleanPrice:3.5,cleanSale:false,cleanDefault: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,
@@ -487,7 +487,7 @@ const DISHES = [
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,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',
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,sale:true,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,sale:true,isDefault:false},{portion:'份',baseGram:300,unit:'份',price:4,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:200,unit:'份',cleanMargin:38.0,cleanPrice:2,cleanSale:false,cleanDefault:true},{portion:'份',baseGram:100,unit:'份',cleanMargin:35.0,cleanPrice:1.5,cleanSale:false,cleanDefault:false},{portion:'大份',baseGram:300,unit:'份',cleanMargin:40.0,cleanPrice:3,cleanSale:false,cleanDefault: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,
@@ -498,7 +498,7 @@ const DISHES = [
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,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',
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,sale:true,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:180,unit:'份',cleanMargin:62.0,cleanPrice:10,cleanSale:false,cleanDefault:true},{portion:'大份',baseGram:280,unit:'份',cleanMargin:65.0,cleanPrice:14,cleanSale:false,cleanDefault: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,
@@ -595,17 +595,27 @@ 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 saleActive = s.sale === true;
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 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!==undefined?parseFloat(s.price).toFixed(2):'—')+'</td><td>'+saleHtml+'</td><td>'+statusHtml+'</td></tr>';
}).join('');
var cleanSpecRows = (d.cleanSpecs||[]).map(function(s){
var statusHtml = s.cleanDefault ? '<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>'+cleanMargin+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
return '<tr><td>'+specViewHtml+'</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><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></tr></thead><tbody>'+specRows+'</tbody></table></div>'
+ '<div class="info-section"><div class="info-section__title">🥬 餐品净菜包规格(共 '+(d.cleanSpecs||[]).length+' 项)</div>'
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>净菜包毛利率</th><th>净菜包售价(元/份)</th><th>上架状态</th><th>是否默认</th></tr></thead><tbody>'+cleanSpecRows+'</tbody></table></div>';
}
/* 餐品定价分区(普通 info-section 样式,详情页只展示字段,成本核算仅在编辑/新增页) */
@@ -995,7 +1005,11 @@ function persistDishSpecs(){
yieldRate: d.yieldRate,
specs: (d.specs||[]).map(function(s){
return {portion:s.portion, baseGram:s.baseGram, unit:s.unit||'份',
price:s.price, cleanPrice:s.cleanPrice, cleanSale:s.cleanSale===true, isDefault:s.isDefault===true};
price:s.price, sale:s.sale===true, isDefault:s.isDefault===true};
}),
cleanSpecs: (d.cleanSpecs||[]).map(function(s){
return {portion:s.portion, baseGram:s.baseGram, unit:s.unit||'份',
cleanMargin:s.cleanMargin, cleanPrice:s.cleanPrice, cleanSale:s.cleanSale===true, cleanDefault:s.cleanDefault===true};
})
};
});
+17 -7
View File
@@ -551,7 +551,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<script>
// 餐品数据
const DISHES = [
{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',
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:12,sale:true,isDefault:true},{portion:'小份',baseGram:120,unit:'份',price:8,sale:true,isDefault:false},{portion:'大份',baseGram:300,unit:'份',price:18,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:200,unit:'份',cleanMargin:55.0,cleanPrice:9,cleanSale:false,cleanDefault:true},{portion:'小份',baseGram:120,unit:'份',cleanMargin:50.0,cleanPrice:6,cleanSale:false,cleanDefault:false},{portion:'大份',baseGram:300,unit:'份',cleanMargin:58.0,cleanPrice:13,cleanSale:false,cleanDefault: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,
@@ -569,7 +569,7 @@ const DISHES = [
{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,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',
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{portion:'标准份',baseGram:150,unit:'份',price:6,sale:true,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:4,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:150,unit:'份',cleanMargin:60.0,cleanPrice:4.5,cleanSale:false,cleanDefault:true},{portion:'小份',baseGram:100,unit:'份',cleanMargin:55.0,cleanPrice:3,cleanSale:false,cleanDefault: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,
@@ -585,7 +585,7 @@ const DISHES = [
{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,cleanMargin:42.0,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',updateTime:'2026-08-08',
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{portion:'标准份',baseGram:300,unit:'份',price:5,sale:true,isDefault:true}],cleanSpecs:[{portion:'标准份',baseGram:300,unit:'份',cleanMargin:42.0,cleanPrice:3.5,cleanSale:false,cleanDefault: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,
@@ -601,7 +601,7 @@ const DISHES = [
{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,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',
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{portion:'标准份',baseGram:200,unit:'份',price:3,sale:true,isDefault:true},{portion:'小份',baseGram:100,unit:'份',price:2,sale:true,isDefault:false},{portion:'份',baseGram:300,unit:'份',price:4,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:200,unit:'份',cleanMargin:38.0,cleanPrice:2,cleanSale:false,cleanDefault:true},{portion:'份',baseGram:100,unit:'份',cleanMargin:35.0,cleanPrice:1.5,cleanSale:false,cleanDefault:false},{portion:'大份',baseGram:300,unit:'份',cleanMargin:40.0,cleanPrice:3,cleanSale:false,cleanDefault: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,
@@ -617,7 +617,7 @@ const DISHES = [
{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,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',
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{portion:'标准份',baseGram:180,unit:'份',price:14,sale:true,isDefault:true},{portion:'大份',baseGram:280,unit:'份',price:20,sale:false,isDefault:false}],cleanSpecs:[{portion:'标准份',baseGram:180,unit:'份',cleanMargin:62.0,cleanPrice:10,cleanSale:false,cleanDefault:true},{portion:'大份',baseGram:280,unit:'份',cleanMargin:65.0,cleanPrice:14,cleanSale:false,cleanDefault: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,
@@ -737,17 +737,27 @@ 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 saleActive = s.sale === true;
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 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!==undefined?parseFloat(s.price).toFixed(2):'—')+'</td><td>'+saleHtml+'</td><td>'+statusHtml+'</td></tr>';
}).join('');
var cleanSpecRows = (d.cleanSpecs||[]).map(function(s){
var statusHtml = s.cleanDefault ? '<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>'+cleanMargin+'</td><td>'+cleanPrice+'</td><td>'+cleanSaleHtml+'</td><td>'+statusHtml+'</td></tr>';
return '<tr><td>'+specViewHtml+'</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><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></tr></thead><tbody>'+specRows+'</tbody></table></div>'
+ '<div class="info-section"><div class="info-section__title">🥬 餐品净菜包规格(共 '+(d.cleanSpecs||[]).length+' 项)</div>'
+ '<table class="info-table spec-detail-table"><thead><tr><th>规格</th><th>净菜包毛利率</th><th>净菜包售价(元/份)</th><th>上架状态</th><th>是否默认</th></tr></thead><tbody>'+cleanSpecRows+'</tbody></table></div>';
}
/* 餐品定价分区(普通 info-section 样式,详情页只展示字段,成本核算仅在编辑/新增页) */
+13
View File
@@ -1,5 +1,18 @@
# 营养用餐管理端 CHANGELOG
## [2026-09-14] feat: 餐品定价规格拆分为餐品规格与餐品净菜包规格
- `din-dish-add.html` 新增/编辑餐品:
- Step 2 餐品定价下将原单一「餐品规格」表拆分为「餐品规格」「餐品净菜包规格」两个独立维护区域
- 餐品规格列:规格(克重/份型)·餐品售价(元/份)·上架状态·默认;净菜包规格列:规格(克重/份型)·净菜包毛利率(%)·净菜包售价(元/份)·上架状态·默认
- 规格管理 JS 拆分 addSpecRow/confirmSpec(餐品规格) 与 addCleanSpecRow/confirmCleanSpec(净菜包规格),toggleDefault/removeSpec 改为按所在 tbody 生效
- 数据模型 specs 拆分为 specs(新增 sale 上架状态) 与 cleanSpecs(新增 cleanDefault 默认),fillEditData 分别预填两表
- `din-dish-all.html` 餐品总库:
- 详情「餐品定价」Tab 拆分为「餐品规格」「餐品净菜包规格」两个表格区域,DISHES 数据拆分 specs/cleanSpecs
- persistDishSpecs 序列化同步输出 cleanSpecs(specs 去掉净菜包字段、新增 sale)
- `din-dish-own.html` 自有餐品库:
- 详情「餐品定价」Tab 拆分为「餐品规格」「餐品净菜包规格」两个表格区域,DISHES 数据拆分 specs/cleanSpecs
## [2026-09-07] feat: 净菜类型拆分独立管理(新增净菜管理页 + 规格行级可食部字段)
- `din-ingredient.html` 食物成分: