feat: 食材档案新增基础定价 + 净菜订单按餐次核算人数与生成中状态

- 食材档案(din-ingredient/din-ingredient-add):新增「基础定价」字段(元/100g),列表与编辑/复制形态同步
- 菜品页(din-dish-add/all/own):定价区交互与展示优化
- 净菜订单(prod-clean-order):由食谱快速创建订单支持餐次级就餐人数(4 个 input 独立填写),新增「生成中」订单状态(青色脉冲动效 + 聚合规则 + 状态机流转 + 统计卡片),新增预定配送时间字段(默认次日 10:00),移除原全局人数输入与汇总预览区
- 同步更新 CHANGELOG 与项目记忆文件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-06 10:39:36 +08:00
co-authored by Claude Opus 4.7
parent 6fc7b12d65
commit 445d824848
12 changed files with 488 additions and 531 deletions
+236 -158
View File
@@ -208,21 +208,41 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.input-with-btn{display:flex;gap:8px;align-items:center;}
.input-with-btn .form-input{flex:1;min-width:0;}
.input-with-btn__btn{flex-shrink:0;height:36px;font-size:var(--font-sm);white-space:nowrap;}
/* 食材成本后的小问号图标 */
.cost-help-icon{flex-shrink:0;width:22px;height:22px;border-radius:50%;border:1px solid var(--border);color:var(--text-secondary);font-size:var(--font-xs);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;background:#fff;transition:all .15s;user-select:none;}
.cost-help-icon:hover{border-color:var(--primary);color:var(--primary);}
/* 食材成本缺失提示 */
.cost-missing-hint{margin-top:6px;font-size:var(--font-xs);color:#FA541C;line-height:1.4;display:none;}
.cost-missing-hint.is-visible{display:block;}
/* 成本核算:成本价行 + 小问号弹窗 */
.cost-result{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:var(--font-sm);color:var(--text-secondary);}
.cost-result__value{font-size:var(--font-base);font-weight:600;color:var(--primary);}
.cost-result__hint{font-size:var(--font-xs);color:var(--text-placeholder);margin-left:6px;}
.cost-help{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--primary);color:#fff;font-size:10px;cursor:pointer;user-select:none;margin-left:4px;}
.cost-detail-pop{position:absolute;display:none;background:#fff;border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:0 4px 16px rgba(0,0,0,.12);padding:14px 16px;z-index:50;min-width:340px;font-size:var(--font-sm);top:100%;left:0;margin-top:6px;}
.cost-detail-pop.open{display:block;}
.cost-detail-pop__title{font-weight:600;color:var(--text-primary);margin-bottom:10px;font-size:var(--font-sm);}
.cost-detail-pop__table{width:100%;border-collapse:collapse;}
.cost-detail-pop__table th,.cost-detail-pop__table td{padding:6px 8px;font-size:var(--font-xs);text-align:left;border-bottom:1px solid #F0F0F0;}
.cost-detail-pop__table th{color:var(--text-secondary);font-weight:500;background:#FAFBFC;}
.cost-detail-pop__table tr:last-child td{border-bottom:none;}
.cost-detail-pop__total{margin-top:10px;padding-top:8px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:var(--font-sm);}
.cost-detail-pop__total strong{color:var(--primary);}
/* 成本核算:左右布局(左食材明细+总成本,右成本价+状态+应用按钮) */
.cost-result{display:flex;gap:16px;margin-top:12px;padding:14px 16px;background:#FAFBFC;border:1px solid var(--border);border-radius:var(--radius-md);font-size:var(--font-sm);color:var(--text-secondary);align-items:stretch;}
.cost-result__left{flex:1;min-width:0;display:flex;flex-direction:column;gap:10px;}
.cost-result__right{flex-shrink:0;width:240px;display:flex;flex-direction:column;gap:14px;padding-left:16px;border-left:1px dashed var(--border);}
.cost-result__head{display:flex;flex-direction:column;gap:8px;}
.cost-result__price-line{display:flex;align-items:baseline;gap:4px;flex-wrap:wrap;}
.cost-result__value{font-size:22px;font-weight:600;color:var(--primary);line-height:1.2;}
.cost-result__hint{font-size:var(--font-xs);color:var(--text-placeholder);}
.cost-result__apply{width:100%;height:32px;font-size:var(--font-sm);margin-top:auto;}
/* 核算状态徽标 */
.cost-status{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:11px;font-size:var(--font-xs);font-weight:500;line-height:1.4;width:fit-content;}
.cost-status::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;}
.cost-status--missing{background:#FFF1F0;color:#FA541C;}
.cost-status--success{background:#F6FFED;color:#52C41A;}
/* 食材成本明细块(直接展示,非弹窗) */
.cost-detail-block{display:flex;flex-direction:column;gap:8px;}
.cost-detail-block__title{font-weight:600;color:var(--text-primary);font-size:var(--font-sm);}
.cost-detail-block__table{width:100%;border-collapse:collapse;background:#fff;border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;}
.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;}
.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);}
.cost-detail-block__total strong{color:var(--primary);font-size:var(--font-base);font-weight:600;}
/* 宣传视频列表(多视频) */
.promo-video-list{display:flex;flex-direction:column;gap:10px;margin-top:10px;}
@@ -337,10 +357,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
<div class="steps">
<div class="step active" onclick="switchTab(0)"><span class="step__num">1</span><span class="step__label">基本档案</span></div>
<div class="step" onclick="switchTab(1)"><span class="step__num">2</span><span class="step__label">餐品构成</span></div>
<div class="step" onclick="switchTab(2)"><span class="step__num">3</span><span class="step__label">烹饪工艺</span></div>
<div class="step" onclick="switchTab(3)"><span class="step__num">4</span><span class="step__label">味道档案</span></div>
<div class="step" onclick="switchTab(4)"><span class="step__num">5</span><span class="step__label">口感档案</span></div>
<div class="step" onclick="switchTab(5)"><span class="step__num">6</span><span class="step__label">色泽档案</span></div>
<div class="step" onclick="switchTab(2)"><span class="step__num">3</span><span class="step__label">餐品定价</span></div>
<div class="step" onclick="switchTab(3)"><span class="step__num">4</span><span class="step__label">烹饪工艺</span></div>
<div class="step" onclick="switchTab(4)"><span class="step__num">5</span><span class="step__label">味道档案</span></div>
<div class="step" onclick="switchTab(5)"><span class="step__num">6</span><span class="step__label">口感档案</span></div>
<div class="step" onclick="switchTab(6)"><span class="step__num">7</span><span class="step__label">色泽档案</span></div>
</div>
<!-- 表单区域 -->
@@ -431,7 +452,7 @@ var INNOV = innovId ? INNOV_MAP[innovId] : null;
var initTab = parseInt(urlParams.get('tab')) || 0;
var currentStep = 0;
var totalSteps = 6;
var totalSteps = 7;
if(initTab >= 0 && initTab < totalSteps) currentStep = initTab;
/* 适配人群数据 */
var crowdList = [];
@@ -452,42 +473,6 @@ var STEP_HTML = [
<div class="form-subtitle">👥 适配人群</div>
<div class="dynamic-rows" id="crowdRows"></div>
<button type="button" class="dynamic-row-add" onclick="addCrowdRow()">+ 添加人群</button>
<div class="form-subtitle">💰 餐品定价</div>
<div class="form-grid">
<div class="form-group"><label class="form-label">称重基础价(元/100g)<span class="required">*</span></label><input class="form-input" id="basePriceInput" type="number" step="0.01" placeholder="请输入称重基础价"></div>
<div class="form-group"><label class="form-label">利润率(%)</label><input class="form-input" id="profitRateInput" type="number" min="0" step="1" placeholder="如:150" value="150"></div>
<div class="form-group">
<label class="form-label">食材成本(元/份)</label>
<div class="input-with-btn">
<input class="form-input" id="ingredientCostInput" type="number" step="0.01" placeholder="根据食材自动计算" value="0.00" readonly>
<button type="button" class="btn btn-default input-with-btn__btn" onclick="calcCostPrice()">🧮 成本核算</button>
</div>
</div>
</div>
<div class="cost-result" id="costResultRow" style="display:none;position:relative;">
<span>成本价(每百克):</span><span class="cost-result__value">¥<span id="costPriceDisplay">0.00</span></span><span class="cost-result__hint">/ 100g</span>
<span class="cost-help" onclick="toggleCostDetailPop(event)">?</span>
<button type="button" class="btn btn-primary cost-result__apply" style="height:28px;font-size:var(--font-xs);" onclick="applyCostToBasePrice()">应用到称重基础价</button>
<div class="cost-detail-pop" id="costDetailPop">
<div class="cost-detail-pop__title">🧾 食材成本明细</div>
<table class="cost-detail-pop__table">
<thead><tr><th>食材</th><th style="text-align:right;">用量(g)</th><th style="text-align:right;">单价(元/kg)</th><th style="text-align:right;">小计(元)</th></tr></thead>
<tbody id="costDetailTbody"><tr><td colspan="4" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr></tbody>
</table>
<div class="cost-detail-pop__total"><span>食材总成本</span><strong>¥<span id="costDetailTotal">0.00</span></strong></div>
<div class="cost-detail-pop__total"><span>熟重</span><strong><span id="costDetailWeight">0</span> g</strong></div>
<div class="cost-detail-pop__total"><span>每百克成本</span><strong>¥<span id="costDetailPer100g">0.00</span></strong></div>
</div>
</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>
<tbody id="specBody">
<tr><td><div class="spec-cell-view"><span class="spec-cell-view__gram">350g</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">标准份</span></div></td><td>15.00</td><td>10.00</td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></td><td><div class="spec-toggle active" 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></tr>
<tr><td><div class="spec-cell-view"><span class="spec-cell-view__gram">200g</span><span class="spec-cell-view__sep">/</span><span class="spec-cell-view__name">小份</span></div></td><td>10.00</td><td>7.00</td><td><div class="sale-toggle" onclick="toggleCleanSale(this)"><span class="sale-toggle__switch"></span><span class="sale-toggle__label">下架</span></div></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></tr>
<tr class="spec-add-row" onclick="addSpecRow(this)"><td colspan="6" style="text-align:center;"> 添加规格</td></tr>
</tbody>
</table>
<div class="form-subtitle">📷 菜品展示</div>
<div class="upload-grid" id="imageGrid">
<div class="upload-box has-img">
@@ -556,7 +541,53 @@ var STEP_HTML = [
</div>
</div>`,
// Step 2: 烹饪工艺
// Step 2: 餐品定价(含规格表 + 成本核算,从原 Step 0 拆出,2026-08-05
`<div class="form-card__title">💰 餐品定价</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">食材成本(元/100g)</label>
<div class="input-with-btn">
<input class="form-input" id="ingredientCostInput" type="number" step="0.01" placeholder="点击右侧核算按钮自动计算" value="0.00" readonly>
<button type="button" class="btn btn-default input-with-btn__btn" onclick="calcCostPrice()">🧮 成本核算</button>
<span class="cost-help-icon" title="点击查看食材成本明细" onclick="calcCostPrice()">?</span>
</div>
<div class="cost-missing-hint is-visible" id="ingredientCostHint">⚠ 食材价格缺失,请补全食材基础定价</div>
</div>
<div class="form-group"><label class="form-label">毛利润(%)</label><input class="form-input" id="profitRateInput" type="number" min="0" step="1" placeholder="如:150"></div>
<div class="form-group"><label class="form-label">称重基础价(元/100g)<span class="required">*</span></label><input class="form-input" id="basePriceInput" type="number" step="0.01" placeholder="请输入称重基础价"></div>
</div>
<div class="cost-result" id="costResultRow" style="display:none;">
<div class="cost-result__left">
<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>
</table>
<div class="cost-detail-block__total"><span>食材总成本(100g):</span><strong>¥<span id="costDetailTotal">0.00</span></strong></div>
</div>
</div>
<div class="cost-result__right">
<div class="cost-result__head">
<span style="color:var(--text-secondary);">成本价(每百克)</span>
<div class="cost-result__price-line">
<span class="cost-result__value">¥<span id="costPriceDisplay">—</span></span>
<span class="cost-result__hint">/ 100g</span>
</div>
<span class="cost-status cost-status--missing" id="costStatusBadge">数据缺失</span>
</div>
<button type="button" class="btn btn-primary cost-result__apply" onclick="applyCostToBasePrice()">应用到食材成本</button>
</div>
</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>
<tbody id="specBody">
<tr class="spec-add-row" onclick="addSpecRow(this)"><td colspan="6" style="text-align:center;"> 添加规格</td></tr>
</tbody>
</table>`,
// Step 3: 烹饪工艺
`<div class="form-card__title">🍳 烹饪工艺</div>
<div class="form-grid">
<div class="form-group"><label class="form-label">主烹饪法<span class="required">*</span></label><select class="form-select"><option>请选择</option><option>炒</option><option>蒸</option><option>炖</option><option>煮</option><option>炸</option><option>烤</option><option>凉拌</option></select></div>
@@ -568,7 +599,7 @@ var STEP_HTML = [
</div>
<div class="form-group full" style="margin-top:16px;"><label class="form-label">烹饪工艺描述</label><textarea class="form-textarea" placeholder="请按步骤描述烹饪工艺,例如:1.食材预处理;2.热锅冷油;3.主料入锅翻炒;4.加入调料;5.出锅装盘"></textarea></div>`,
// Step 3: 味道档案
// Step 4: 味道档案
`<div class="form-card__title">👅 味道档案</div>
<div class="form-grid">
<div class="form-group"><label class="form-label">酸度 (1-5)<span class="required">*</span></label><input class="form-input" type="number" min="1" max="5" placeholder="1-5"></div>
@@ -581,7 +612,7 @@ var STEP_HTML = [
<div class="form-group"><label class="form-label">综合评级<span class="required">*</span></label><select class="form-select"><option value="">请选择</option><option value="excellent">优秀</option><option value="good">良好</option><option value="average">一般</option><option value="poor">较差</option><option value="bad">不合格</option></select></div>
</div>`,
// Step 4: 口感档案
// Step 5: 口感档案
`<div class="form-card__title">🫦 口感档案</div>
<div class="form-grid">
<div class="form-group"><label class="form-label">软硬度 (1-5)<span class="required">*</span></label><input class="form-input" type="number" min="1" max="5" placeholder="1-5"></div>
@@ -593,7 +624,7 @@ var STEP_HTML = [
<div class="form-group"><label class="form-label">综合评级<span class="required">*</span></label><select class="form-select"><option value="">请选择</option><option value="excellent">优秀</option><option value="good">良好</option><option value="average">一般</option><option value="poor">较差</option><option value="bad">不合格</option></select></div>
</div>`,
// Step 5: 色泽档案
// Step 6: 色泽档案
`<div class="form-card__title">🎨 色泽档案</div>
<div class="form-grid">
<div class="form-group"><label class="form-label">主色调<span class="required">*</span></label><input class="form-input" type="text" placeholder="如:深红褐色"></div>
@@ -623,7 +654,7 @@ function renderStep(){
function lockConvertStep(){
var c=document.getElementById('stepContent');
c.classList.remove('step-readonly');
var LOCKED=[1,3,4,5];
var LOCKED=[1,4,5,6];
if(LOCKED.indexOf(currentStep)===-1) return;
c.classList.add('step-readonly');
var tip=document.createElement('div');
@@ -760,97 +791,142 @@ function triggerPromoVideoUpload(idx){
}
}
/* 餐品定价:成本核算 + 应用到称重基础价 */
/* 餐品定价:成本核算 + 应用到食材成本 */
function calcCostPrice(){
var ingredientCost = parseFloat(document.getElementById('ingredientCostInput') && document.getElementById('ingredientCostInput').value) || 0;
var cookedWeight = parseFloat(document.getElementById('cookedWeightInput') && document.getElementById('cookedWeightInput').value) || 0;
if(cookedWeight <= 0){
cookedWeight = 350;
}
if(ingredientCost <= 0){
ingredientCost = 12.00;
}
/* 每百克成本 */
var per100gCost = ingredientCost / cookedWeight * 100;
var row = document.getElementById('costResultRow');
/* toggle:已显示则隐藏,再次点击才重新核算 */
if(row && row.style.display !== 'none'){
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]'));
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'}
];
} 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;
/* 简化原型:单价按食材类型粗略给(主材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'});
});
}
/* 按 100g 餐品等比缩放:scale = 100 / Σ原用量,缩放后所有用量合计 = 100g */
var sumAmount = 0;
items.forEach(function(d){ sumAmount += d.amount || 0; });
var scale = sumAmount > 0 ? 100 / sumAmount : 0;
/* 渲染明细表 */
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>';
} else {
items.forEach(function(d, idx){
var scaledAmount = d.amount * scale;
var subtotal = scaledAmount * d.unitPrice / 1000;
total += subtotal;
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__subtotal" data-row-idx="'+idx+'">'+subtotal.toFixed(2)+'</td>'
+ '</tr>';
});
}
/* 显示成本核算结果区 */
if(row) row.style.display = 'flex';
var display = document.getElementById('costPriceDisplay');
if(display) display.textContent = per100gCost.toFixed(2);
/* 同步弹窗明细 */
/* 食材明细表 */
var detailTbody = document.getElementById('costDetailTbody');
if(detailTbody){
var rows = document.querySelectorAll('#ingredientTbody tr');
var hasRealIngredient = rows.length > 0 && !(rows.length === 1 && rows[0].querySelector('td[colspan]'));
var html = '';
var total = 0;
if(!hasRealIngredient){
/* 演示食材明细 */
var demo = [
{name:'鸡胸肉', amount:120, unitPrice:25.00},
{name:'西兰花', amount:80, unitPrice:10.00},
{name:'橄榄油', amount:10, unitPrice:35.00},
{name:'食盐', amount:3, unitPrice:6.00}
];
demo.forEach(function(d){
var subtotal = d.amount * d.unitPrice / 1000;
total += subtotal;
html += '<tr><td>'+d.name+'</td><td style="text-align:right;">'+d.amount+'</td><td style="text-align:right;">'+d.unitPrice.toFixed(2)+'</td><td style="text-align:right;">'+subtotal.toFixed(2)+'</td></tr>';
});
} else {
rows.forEach(function(r){
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;
/* 简化原型:单价按食材类型粗略给(主材18元/kg / 辅材12元/kg / 调料8元/kg */
var typeText = tds[3] ? tds[3].textContent : '';
var unitPrice = 18;
if(typeText.indexOf('辅材')>=0) unitPrice = 12;
else if(typeText.indexOf('调料')>=0) unitPrice = 8;
var subtotal = amount * unitPrice / 1000;
total += subtotal;
html += '<tr><td>'+name+'</td><td style="text-align:right;">'+amount+'</td><td style="text-align:right;">'+unitPrice.toFixed(2)+'</td><td style="text-align:right;">'+subtotal.toFixed(2)+'</td></tr>';
});
if(detailTbody) detailTbody.innerHTML = html;
/* 食材总成本 */
var totalEl = document.getElementById('costDetailTotal');
if(totalEl) totalEl.textContent = total.toFixed(2);
/* 隐藏食材成本缺失提示 */
var hintEl = document.getElementById('ingredientCostHint');
if(hintEl) hintEl.classList.remove('is-visible');
/* 重新判断核算状态(任一单价<=0 视为数据缺失) */
updateCostStatus();
}
/* 根据食材明细表中的单价判断核算状态,并更新成本价和徽标 */
function updateCostStatus(){
var inputs = document.querySelectorAll('#costDetailTbody .cost-detail__unit-input');
var hasMissing = false;
var total = 0;
inputs.forEach(function(inp){
var price = parseFloat(inp.value) || 0;
if(price <= 0) hasMissing = true;
var tr = inp.closest('tr');
if(tr){
var amountCell = tr.querySelector('td.cost-detail__amount');
var amount = parseFloat(amountCell && amountCell.textContent) || 0;
total += amount * price / 1000;
}
detailTbody.innerHTML = html || '<tr><td colspan="4" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr>';
var totalEl = document.getElementById('costDetailTotal');
if(totalEl) totalEl.textContent = total.toFixed(2);
var weightEl = document.getElementById('costDetailWeight');
if(weightEl) weightEl.textContent = cookedWeight;
var per100gEl = document.getElementById('costDetailPer100g');
if(per100gEl) per100gEl.textContent = per100gCost.toFixed(2);
});
/* 无食材时也视为数据缺失 */
if(inputs.length === 0) hasMissing = true;
/* 更新食材总成本 */
var totalEl = document.getElementById('costDetailTotal');
if(totalEl) totalEl.textContent = total.toFixed(2);
/* 更新成本价(数据缺失时显示 —) */
var display = document.getElementById('costPriceDisplay');
if(display) display.textContent = hasMissing ? '—' : total.toFixed(2);
/* 更新状态徽标 */
var badge = document.getElementById('costStatusBadge');
if(badge){
badge.textContent = hasMissing ? '数据缺失' : '核算成功';
badge.className = 'cost-status ' + (hasMissing ? 'cost-status--missing' : 'cost-status--success');
}
}
/* 小问号弹窗显示/切换 */
function toggleCostDetailPop(e){
if(e) e.stopPropagation();
var pop = document.getElementById('costDetailPop');
if(pop) pop.classList.toggle('open');
/* 食材成本明细:单价修改后联动重算小计、总成本、成本价、状态徽标 */
function recalcCostRow(inputEl){
var newPrice = parseFloat(inputEl.value) || 0;
/* 重算该行小计 */
var tr = inputEl.closest('tr');
if(!tr) return;
var amountCell = tr.querySelector('td.cost-detail__amount');
var subtotalCell = tr.querySelector('td.cost-detail__subtotal');
if(!amountCell || !subtotalCell) return;
var amount = parseFloat(amountCell.textContent) || 0;
var subtotal = amount * newPrice / 1000;
subtotalCell.textContent = subtotal.toFixed(2);
/* 统一交给 updateCostStatus 重算总成本、成本价、状态徽标 */
updateCostStatus();
}
/* 应用成本价到称重基础价(叠加利润率 */
/* 小问号弹窗显示/切换 — 已移除(成本明细改为直接展示 */
/* 应用食材总成本到食材成本 input,并根据毛利润自动算出称重基础价 */
function applyCostToBasePrice(){
var cost = parseFloat(document.getElementById('costPriceDisplay') && document.getElementById('costPriceDisplay').textContent) || 0;
if(cost <= 0){ return; }
var total = parseFloat(document.getElementById('costDetailTotal') && document.getElementById('costDetailTotal').textContent) || 0;
if(total <= 0){ return; }
/* 1. 回填食材成本 input */
var costInput = document.getElementById('ingredientCostInput');
if(costInput) costInput.value = total.toFixed(2);
/* 2. 根据毛利润自动算出称重基础价 */
var profitInput = document.getElementById('profitRateInput');
var rate = profitInput ? (parseFloat(profitInput.value)||0) : 0;
var base = cost * (1 + rate/100);
var base = total * (1 + rate/100);
var baseInput = document.getElementById('basePriceInput');
if(baseInput){
baseInput.value = base.toFixed(2);
}
if(baseInput) baseInput.value = base.toFixed(2);
}
/* 文档点击关闭弹窗 */
document.addEventListener('click', function(e){
var pop = document.getElementById('costDetailPop');
if(!pop || !pop.classList.contains('open')) return;
var help = e.target.closest('.cost-help');
var inPop = e.target.closest('.cost-detail-pop');
if(!help && !inPop) pop.classList.remove('open');
});
/* 营养指标自动计算(根据食材汇总) */
function recalcAddNutri(){
/* 简化原型:根据食材数量生成示例值 */
@@ -1121,14 +1197,9 @@ function fillEditData(){
var d = EDIT_DATA;
if(currentStep===0){
// 基本档案 - 输入顺序:餐品名称(0), 过敏原(1), [定价: basePrice(2), profitRate(3), ingredientCost(4)]
// 基本档案 - 输入顺序:餐品名称(0), 过敏原(1)(定价与规格已拆到 Step 2
if(inputs[0]) inputs[0].value = d.name;
if(inputs[1]) inputs[1].value = d.allergen;
if(d.pricing){
if(inputs[2]) inputs[2].value = d.pricing.basePrice||'';
if(inputs[3]) inputs[3].value = d.pricing.profitRate||150;
if(inputs[4]) inputs[4].value = (d.pricing.ingredientCost||0).toFixed(2);
}
// selects 顺序:分类、餐次、宝塔、标签、上架状态
setSelectByText(selects[0], d.category);
setSelectByText(selects[1], d.meal);
@@ -1140,20 +1211,6 @@ function fillEditData(){
crowdList = d.crowd.map(function(c){return {name:c.name,level:c.level};});
renderCrowdRows();
}
// 预填规格表
var specBody = container.querySelector('#specTable tbody');
if(specBody){
specBody.innerHTML = '';
d.specs.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:'');
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>';
specBody.appendChild(tr);
});
}
// 预填宣传视频
if(d.promoVideos && d.promoVideos.length>0){
promoVideoList = d.promoVideos.map(function(v, i){
@@ -1186,25 +1243,46 @@ function fillEditData(){
});
}
} else if(currentStep===2){
// 餐品定价 - 输入顺序:称重基础价(0), 毛利润(1), 食材成本(2)
if(d.pricing){
if(inputs[0]) inputs[0].value = d.pricing.basePrice||'';
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){
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:'');
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>';
specBody.appendChild(tr);
});
}
} else if(currentStep===3){
// 烹饪工艺 - 字段顺序:主烹饪法、辅烹饪法、火候、时长、温度、步骤数
var c = d.cook;
var vals = [c.mainMethod, c.subMethod, c.heat, c.duration, c.temp, c.steps];
inputs.forEach(function(inp, i){ if(vals[i]!==undefined) inp.value = vals[i]; });
var ta = container.querySelector('textarea');
if(ta) ta.value = c.desc||'';
} else if(currentStep===3){
} else if(currentStep===4){
// 味道档案
var t = d.taste;
var tvals = [t.sour, t.sweet, t.bitter, t.spicy, t.salty, t.smell, t.flavorScore];
inputs.forEach(function(inp, i){ if(tvals[i]!==undefined) inp.value = tvals[i]; });
if(selects[0]) setSelectByValue(selects[0], t.flavorGrade);
} else if(currentStep===4){
} else if(currentStep===5){
// 口感档案
var tx = d.texture;
var txvals = [tx.hardness, tx.sticky, tx.crispy, tx.smooth, tx.elastic, tx.textureScore];
inputs.forEach(function(inp, i){ if(txvals[i]!==undefined) inp.value = txvals[i]; });
if(selects[0]) setSelectByValue(selects[0], tx.textureGrade);
} else if(currentStep===5){
} else if(currentStep===6){
// 色泽档案
var cl = d.color;
var cvals = [cl.mainColor, cl.uniformity, cl.gloss, cl.freshness, cl.appearance, cl.colorScore];
+48 -129
View File
@@ -210,21 +210,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
/* 营养指标 - 自动计算提示 */
.nutri-tip{font-size:var(--font-xs);color:#D46B08;background:#FFF7E6;border:1px solid #FFD591;border-radius:var(--radius-sm);padding:6px 12px;margin-bottom:12px;display:inline-flex;align-items:center;gap:4px;}
/* 成本核算:成本价行 + 小问号弹窗 */
.cost-result{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:var(--font-sm);color:var(--text-secondary);position:relative;}
.cost-result__value{font-size:var(--font-base);font-weight:600;color:var(--primary);}
.cost-result__hint{font-size:var(--font-xs);color:var(--text-placeholder);margin-left:6px;}
.cost-result__apply{margin-left:auto;}
.cost-help{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--primary);color:#fff;font-size:10px;cursor:pointer;user-select:none;margin-left:4px;}
.cost-detail-pop{position:absolute;left:0;top:24px;display:none;background:#fff;border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:0 4px 16px rgba(0,0,0,.12);padding:14px 16px;z-index:50;min-width:340px;font-size:var(--font-sm);}
.cost-detail-pop.open{display:block;}
.cost-detail-pop__title{font-weight:600;color:var(--text-primary);margin-bottom:10px;font-size:var(--font-sm);}
.cost-detail-pop__table{width:100%;border-collapse:collapse;}
.cost-detail-pop__table th,.cost-detail-pop__table td{padding:6px 8px;font-size:var(--font-xs);text-align:left;border-bottom:1px solid #F0F0F0;}
.cost-detail-pop__table th{color:var(--text-secondary);font-weight:500;background:#FAFBFC;}
.cost-detail-pop__table tr:last-child td{border-bottom:none;}
.cost-detail-pop__total{margin-top:10px;padding-top:8px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:var(--font-sm);}
.cost-detail-pop__total strong{color:var(--primary);}
/* 成本核算样式已移除(详情页不再展示成本核算,编辑/新增页 din-dish-add.html 自带样式) */
/* 宣传视频多卡片 */
.promo-video-card{background:#FAFBFC;border:1px solid #F0F0F0;border-radius:var(--radius-md);overflow:hidden;display:flex;flex-direction:column;}
@@ -369,10 +355,11 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<div class="detail-tabs">
<div class="detail-tab active" onclick="switchDetailTab(0)">🍽️ 基本档案</div>
<div class="detail-tab" onclick="switchDetailTab(1)">🧩 餐品构成</div>
<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 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 class="detail-tab" onclick="switchDetailTab(6)">🎨 色泽档案</div>
</div>
</div>
<div class="panel-right__body" id="detailBody"></div>
@@ -470,21 +457,27 @@ 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,renderCook,renderTaste,renderTexture,renderColor];
const renderers = [renderBasic,renderComposition,renderPricing,renderCook,renderTaste,renderTexture,renderColor];
body.innerHTML = renderers[currentTab](d);
}
/* 删除餐品:二次确认 */
function deleteDish(idx){
var d = DISHES[idx];
if(!d) return;
var msg = '确定删除餐品「'+d.name+'」('+d.id+')吗?\n删除后不可恢复,请谨慎操作。';
if(!confirm(msg)) return;
/* 原型演示:仅从内存数据中移除并刷新列表 */
DISHES.splice(idx,1);
if(currentDish >= DISHES.length) currentDish = DISHES.length - 1;
if(currentDish < 0) currentDish = 0;
currentTab = 0;
renderDishList();
renderDetail();
}
function renderBasic(d){
var specRows = (d.specs||[]).map(function(s){
var statusHtml = s.isDefault ? '<span class="tag tag-blue" style="font-size:11px;">默认</span>' : '';
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>';
}).join('');
return `<div class="info-section"><div class="info-section__title">基本信息 <span><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>
return `<div class="info-section"><div class="info-section__title">基本信息 <span><a class="link" style="color:var(--danger);margin-right:12px;" onclick="deleteDish(${currentDish})">🗑 删除餐品</a><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">餐品编号</div><div class="info-cell__value">${d.id}</div></div>
<div class="info-cell"><div class="info-cell__label">餐品分类</div><div class="info-cell__value">${d.category}</div></div>
@@ -497,9 +490,6 @@ function renderBasic(d){
<div class="info-section"><div class="info-section__title">👥 适配人群</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;">${(d.crowd||[]).map(function(c){return '<div style="display:flex;align-items:center;gap:4px;"><span style="font-size:var(--font-sm);">'+c.name+'</span>'+levelTag(c.level)+'</div>';}).join('')}</div>
${d.crowdNote?'<div style="font-size:var(--font-sm);color:var(--text-secondary);background:#FFFBE6;padding:8px 12px;border-radius:var(--radius-sm);border:1px solid #FFE58F;">⚠️ '+d.crowdNote+'</div>':''}</div>
<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>
${renderPricingSection(d)}
<div class="info-section"><div class="info-section__title">📷 菜品展示</div>
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">图片</div></div>
@@ -513,32 +503,32 @@ function renderBasic(d){
${renderPromoVideosSection(d)}`;
}
/* 餐品定价分区(普通 info-section 样式,含成本核算 */
/* 餐品定价 Tab:餐品规格 + 餐品定价(从基本档案拆出,2026-08-05 */
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 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>';
}).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>';
}
/* 餐品定价分区(普通 info-section 样式,详情页只展示字段,成本核算仅在编辑/新增页) */
function renderPricingSection(d){
var p = d.pricing || {};
var ingredientCost = (p.ingredientCost||0).toFixed(2);
var cookedWeight = d.cookedWeight||0;
var per100g = cookedWeight>0 ? (p.ingredientCost/cookedWeight*100) : (p.per100gCost||0);
var per100gStr = per100g.toFixed(2);
var profitRate = p.profitRate||0;
return '<div class="info-section"><div class="info-section__title">💰 餐品定价 <span><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>'
return '<div class="info-section"><div class="info-section__title">💰 餐品定价 <span><a class="link" href="din-dish-add.html?mode=edit&tab=2">编辑</a></span></div>'
+ '<div class="info-grid">'
+ '<div class="info-cell"><div class="info-cell__label">称重基础价(元/100g</div><div class="info-cell__value">¥ '+(p.basePrice||'—')+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">利润</div><div class="info-cell__value">'+profitRate+'%</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">食材成本(元/</div><div class="info-cell__value">¥ '+ingredientCost+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">成本核算</div><div class="info-cell__value"><button class="btn btn-default" style="height:28px;font-size:var(--font-xs);padding:0 12px;" onclick="calcDetailCost('+currentDish+')">🧮 成本核算</button></div></div>'
+ '</div>'
+ '<div class="cost-result" id="costResultRow" style="display:none;">'
+ '<span>成本价(每百克):</span><span class="cost-result__value">¥<span id="costPriceDisplay">0.00</span></span><span class="cost-result__hint">/ 100g</span>'
+ '<span class="cost-help" onclick="toggleCostDetailPop(event)">?</span>'
+ '<div class="cost-detail-pop" id="costDetailPop">'
+ '<div class="cost-detail-pop__title">🧾 食材成本明细</div>'
+ '<table class="cost-detail-pop__table"><thead><tr><th>食材</th><th style="text-align:right;">用量(g)</th><th style="text-align:right;">单价(元/kg)</th><th style="text-align:right;">小计(元)</th></tr></thead><tbody id="costDetailTbody"></tbody></table>'
+ '<div class="cost-detail-pop__total"><span>食材总成本</span><strong>¥<span id="costDetailTotal">0.00</span></strong></div>'
+ '<div class="cost-detail-pop__total"><span>熟重</span><strong><span id="costDetailWeight">0</span> g</strong></div>'
+ '<div class="cost-detail-pop__total"><span>每百克成本</span><strong>¥<span id="costDetailPer100g">0.00</span></strong></div>'
+ '</div>'
+ '<button type="button" class="btn btn-primary cost-result__apply" style="height:28px;font-size:var(--font-xs);" onclick="applyDetailCost('+currentDish+')">应用到称重基础价</button>'
+ '<div class="info-cell"><div class="info-cell__label">利润</div><div class="info-cell__value">'+profitRate+'%</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">食材成本(元/100g</div><div class="info-cell__value">¥ '+ingredientCost+'</div></div>'
+ '</div>'
+ '</div>';
}
@@ -617,7 +607,7 @@ function renderNutriSection(d){
function renderCook(d){
var c = d.cook;
return '<div class="info-section"><div class="info-section__title">烹饪工艺 <span><a class="link" href="din-dish-add.html?mode=edit&tab=2">编辑</a></span></div>'
return '<div class="info-section"><div class="info-section__title">烹饪工艺 <span><a class="link" href="din-dish-add.html?mode=edit&tab=3">编辑</a></span></div>'
+ '<div class="info-grid">'
+ '<div class="info-cell"><div class="info-cell__label">主烹饪法</div><div class="info-cell__value">'+c.mainMethod+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">辅烹饪法</div><div class="info-cell__value">'+c.subMethod+'</div></div>'
@@ -631,7 +621,7 @@ function renderCook(d){
function renderTaste(d){
const t = d.taste;
const gradeTag = t.flavorGrade==='优秀'?'green':t.flavorGrade==='良好'?'blue':t.flavorGrade==='一般'?'orange':'red';
return `<div class="info-section"><div class="info-section__title">味道档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=3">编辑</a></span></div>
return `<div class="info-section"><div class="info-section__title">味道档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=4">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">酸度 (1-5)</div><div class="info-cell__value">${t.sour}</div></div>
<div class="info-cell"><div class="info-cell__label">甜度 (1-5)</div><div class="info-cell__value">${t.sweet}</div></div>
@@ -647,7 +637,7 @@ function renderTaste(d){
function renderColor(d){
const c = d.color;
const gradeTag = c.colorGrade==='优秀'?'green':c.colorGrade==='良好'?'blue':c.colorGrade==='一般'?'orange':'red';
return `<div class="info-section"><div class="info-section__title">色泽档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=5">编辑</a></span></div>
return `<div class="info-section"><div class="info-section__title">色泽档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=6">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">主色调</div><div class="info-cell__value">${c.mainColor}</div></div>
<div class="info-cell"><div class="info-cell__label">色泽均匀度 (1-5)</div><div class="info-cell__value">${c.uniformity}</div></div>
@@ -662,7 +652,7 @@ function renderColor(d){
function renderTexture(d){
const t = d.texture;
const gradeTag = t.textureGrade==='优秀'?'green':t.textureGrade==='良好'?'blue':t.textureGrade==='一般'?'orange':'red';
return `<div class="info-section"><div class="info-section__title">口感档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=4">编辑</a></span></div>
return `<div class="info-section"><div class="info-section__title">口感档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=5">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">软硬度 (1-5)</div><div class="info-cell__value">${t.hardness}</div></div>
<div class="info-cell"><div class="info-cell__label">黏糯度 (1-5)</div><div class="info-cell__value">${t.sticky}</div></div>
@@ -723,79 +713,8 @@ function togglePromoVideo(el, dishIdx, videoIdx){
}
}
/* 成本核算:计算每百克成本并展示明细 */
function calcDetailCost(dishIdx){
var dish = DISHES[dishIdx];
if(!dish) return;
var ingredientCost = (dish.pricing && dish.pricing.ingredientCost) || 0;
var cookedWeight = dish.cookedWeight || 0;
if(cookedWeight <= 0){ alert('请先在「餐品构成」中填写熟重'); return; }
if(ingredientCost <= 0){ alert('当前食材成本为 0,请先录入食材'); return; }
var per100gCost = ingredientCost / cookedWeight * 100;
var row = document.getElementById('costResultRow');
if(row) row.style.display = 'flex';
var display = document.getElementById('costPriceDisplay');
if(display) display.textContent = per100gCost.toFixed(2);
/* 同步弹窗明细 */
var detailTbody = document.getElementById('costDetailTbody');
if(detailTbody){
var rows = dish.composition || [];
if(rows.length === 0){
detailTbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr>';
} else {
var html = '';
var total = 0;
rows.forEach(function(c){
var amount = parseFloat(c.amount) || 0;
var unitPrice = 18;
if((c.type||'').indexOf('辅材')>=0) unitPrice = 12;
else if((c.type||'').indexOf('调料')>=0) unitPrice = 8;
var subtotal = amount * unitPrice / 1000;
total += subtotal;
html += '<tr><td>'+c.name+'</td><td style="text-align:right;">'+amount+'</td><td style="text-align:right;">'+unitPrice.toFixed(2)+'</td><td style="text-align:right;">'+subtotal.toFixed(2)+'</td></tr>';
});
detailTbody.innerHTML = html;
var totalEl = document.getElementById('costDetailTotal');
if(totalEl) totalEl.textContent = total.toFixed(2);
var weightEl = document.getElementById('costDetailWeight');
if(weightEl) weightEl.textContent = cookedWeight;
var per100gEl = document.getElementById('costDetailPer100g');
if(per100gEl) per100gEl.textContent = per100gCost.toFixed(2);
}
}
}
/* 成本核算相关函数已移除(详情页不再展示成本核算,编辑/新增页 din-dish-add.html 自带实现) */
/* 小问号弹窗显示/切换 */
function toggleCostDetailPop(e){
if(e) e.stopPropagation();
var pop = document.getElementById('costDetailPop');
if(pop) pop.classList.toggle('open');
}
/* 应用成本价到称重基础价(叠加利润率) */
function applyDetailCost(dishIdx){
var dish = DISHES[dishIdx];
if(!dish) return;
var display = document.getElementById('costPriceDisplay');
var cost = parseFloat(display && display.textContent) || 0;
if(cost <= 0){ alert('请先点击成本核算'); return; }
var rate = (dish.pricing && dish.pricing.profitRate) || 0;
var base = cost * (1 + rate/100);
if(!dish.pricing) dish.pricing = {};
dish.pricing.basePrice = base.toFixed(2);
dish.pricing.per100gCost = cost.toFixed(2);
alert('已应用到称重基础价:¥'+base.toFixed(2)+'\n(成本 ¥'+cost.toFixed(2)+' × (1 + '+rate+'%)');
renderDetail();
}
/* 文档点击关闭弹窗 */
document.addEventListener('click', function(e){
var pop = document.getElementById('costDetailPop');
if(!pop || !pop.classList.contains('open')) return;
var help = e.target.closest('.cost-help');
var inPop = e.target.closest('.cost-detail-pop');
if(!help && !inPop) pop.classList.remove('open');
});
(function(){var a=document.querySelector('.sidebar-item.active');if(!a)return;var s=a.closest('.sidebar-subgroup');if(s)s.classList.add('open');})();
// 初始化
+48 -129
View File
@@ -210,21 +210,7 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
/* 营养指标 - 自动计算提示 */
.nutri-tip{font-size:var(--font-xs);color:#D46B08;background:#FFF7E6;border:1px solid #FFD591;border-radius:var(--radius-sm);padding:6px 12px;margin-bottom:12px;display:inline-flex;align-items:center;gap:4px;}
/* 成本核算:成本价行 + 小问号弹窗 */
.cost-result{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:var(--font-sm);color:var(--text-secondary);position:relative;}
.cost-result__value{font-size:var(--font-base);font-weight:600;color:var(--primary);}
.cost-result__hint{font-size:var(--font-xs);color:var(--text-placeholder);margin-left:6px;}
.cost-result__apply{margin-left:auto;}
.cost-help{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--primary);color:#fff;font-size:10px;cursor:pointer;user-select:none;margin-left:4px;}
.cost-detail-pop{position:absolute;left:0;top:24px;display:none;background:#fff;border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:0 4px 16px rgba(0,0,0,.12);padding:14px 16px;z-index:50;min-width:340px;font-size:var(--font-sm);}
.cost-detail-pop.open{display:block;}
.cost-detail-pop__title{font-weight:600;color:var(--text-primary);margin-bottom:10px;font-size:var(--font-sm);}
.cost-detail-pop__table{width:100%;border-collapse:collapse;}
.cost-detail-pop__table th,.cost-detail-pop__table td{padding:6px 8px;font-size:var(--font-xs);text-align:left;border-bottom:1px solid #F0F0F0;}
.cost-detail-pop__table th{color:var(--text-secondary);font-weight:500;background:#FAFBFC;}
.cost-detail-pop__table tr:last-child td{border-bottom:none;}
.cost-detail-pop__total{margin-top:10px;padding-top:8px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:var(--font-sm);}
.cost-detail-pop__total strong{color:var(--primary);}
/* 成本核算样式已移除(详情页不再展示成本核算,编辑/新增页 din-dish-add.html 自带样式) */
/* 宣传视频多卡片 */
.promo-video-card{background:#FAFBFC;border:1px solid #F0F0F0;border-radius:var(--radius-md);overflow:hidden;display:flex;flex-direction:column;}
@@ -393,10 +379,11 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<div class="detail-tabs">
<div class="detail-tab active" onclick="switchDetailTab(0)">🍽️ 基本档案</div>
<div class="detail-tab" onclick="switchDetailTab(1)">🧩 餐品构成</div>
<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 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 class="detail-tab" onclick="switchDetailTab(6)">🎨 色泽档案</div>
</div>
</div>
<div class="panel-right__body" id="detailBody"></div>
@@ -494,21 +481,27 @@ 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,renderCook,renderTaste,renderTexture,renderColor];
const renderers = [renderBasic,renderComposition,renderPricing,renderCook,renderTaste,renderTexture,renderColor];
body.innerHTML = renderers[currentTab](d);
}
/* 删除餐品:二次确认 */
function deleteDish(idx){
var d = DISHES[idx];
if(!d) return;
var msg = '确定删除餐品「'+d.name+'」('+d.id+')吗?\n删除后不可恢复,请谨慎操作。';
if(!confirm(msg)) return;
/* 原型演示:仅从内存数据中移除并刷新列表 */
DISHES.splice(idx,1);
if(currentDish >= DISHES.length) currentDish = DISHES.length - 1;
if(currentDish < 0) currentDish = 0;
currentTab = 0;
renderDishList();
renderDetail();
}
function renderBasic(d){
var specRows = (d.specs||[]).map(function(s){
var statusHtml = s.isDefault ? '<span class="tag tag-blue" style="font-size:11px;">默认</span>' : '';
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>';
}).join('');
return '<div class="info-section"><div class="info-section__title">基本信息 <span><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>'
return '<div class="info-section"><div class="info-section__title">基本信息 <span><a class="link" style="color:var(--danger);margin-right:12px;" onclick="deleteDish('+currentDish+')">🗑 删除餐品</a><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>'
+ '<div class="info-grid">'
+ '<div class="info-cell"><div class="info-cell__label">餐品编号</div><div class="info-cell__value">'+d.id+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">餐品分类</div><div class="info-cell__value">'+d.category+'</div></div>'
@@ -522,9 +515,6 @@ function renderBasic(d){
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;">'+(d.crowd||[]).map(function(c){return '<div style="display:flex;align-items:center;gap:4px;"><span style="font-size:var(--font-sm);">'+c.name+'</span>'+levelTag(c.level)+'</div>';}).join('')+'</div>'
+ (d.crowdNote?'<div style="font-size:var(--font-sm);color:var(--text-secondary);background:#FFFBE6;padding:8px 12px;border-radius:var(--radius-sm);border:1px solid #FFE58F;">⚠️ '+d.crowdNote+'</div>':'')
+ '</div>'
+ '<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>'
+ renderPricingSection(d)
+ '<div class="info-section"><div class="info-section__title">📷 菜品展示</div>'
+ '<div class="media-grid">'
+ '<div class="media-card"><div class="media-card__thumb">图片</div></div>'
@@ -536,32 +526,32 @@ function renderBasic(d){
+ renderPromoVideosSection(d);
}
/* 餐品定价分区(普通 info-section 样式,含成本核算 */
/* 餐品定价 Tab:餐品规格 + 餐品定价(从基本档案拆出,2026-08-05 */
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 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>';
}).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>';
}
/* 餐品定价分区(普通 info-section 样式,详情页只展示字段,成本核算仅在编辑/新增页) */
function renderPricingSection(d){
var p = d.pricing || {};
var ingredientCost = (p.ingredientCost||0).toFixed(2);
var cookedWeight = d.cookedWeight||0;
var per100g = cookedWeight>0 ? (p.ingredientCost/cookedWeight*100) : (p.per100gCost||0);
var per100gStr = per100g.toFixed(2);
var profitRate = p.profitRate||0;
return '<div class="info-section"><div class="info-section__title">💰 餐品定价 <span><a class="link" href="din-dish-add.html?mode=edit&tab=0">编辑</a></span></div>'
return '<div class="info-section"><div class="info-section__title">💰 餐品定价 <span><a class="link" href="din-dish-add.html?mode=edit&tab=2">编辑</a></span></div>'
+ '<div class="info-grid">'
+ '<div class="info-cell"><div class="info-cell__label">称重基础价(元/100g</div><div class="info-cell__value">¥ '+(p.basePrice||'—')+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">利润</div><div class="info-cell__value">'+profitRate+'%</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">食材成本(元/</div><div class="info-cell__value">¥ '+ingredientCost+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">成本核算</div><div class="info-cell__value"><button class="btn btn-default" style="height:28px;font-size:var(--font-xs);padding:0 12px;" onclick="calcDetailCost('+currentDish+')">🧮 成本核算</button></div></div>'
+ '</div>'
+ '<div class="cost-result" id="costResultRow" style="display:none;">'
+ '<span>成本价(每百克):</span><span class="cost-result__value">¥<span id="costPriceDisplay">0.00</span></span><span class="cost-result__hint">/ 100g</span>'
+ '<span class="cost-help" onclick="toggleCostDetailPop(event)">?</span>'
+ '<div class="cost-detail-pop" id="costDetailPop">'
+ '<div class="cost-detail-pop__title">🧾 食材成本明细</div>'
+ '<table class="cost-detail-pop__table"><thead><tr><th>食材</th><th style="text-align:right;">用量(g)</th><th style="text-align:right;">单价(元/kg)</th><th style="text-align:right;">小计(元)</th></tr></thead><tbody id="costDetailTbody"></tbody></table>'
+ '<div class="cost-detail-pop__total"><span>食材总成本</span><strong>¥<span id="costDetailTotal">0.00</span></strong></div>'
+ '<div class="cost-detail-pop__total"><span>熟重</span><strong><span id="costDetailWeight">0</span> g</strong></div>'
+ '<div class="cost-detail-pop__total"><span>每百克成本</span><strong>¥<span id="costDetailPer100g">0.00</span></strong></div>'
+ '</div>'
+ '<button type="button" class="btn btn-primary cost-result__apply" style="height:28px;font-size:var(--font-xs);" onclick="applyDetailCost('+currentDish+')">应用到称重基础价</button>'
+ '<div class="info-cell"><div class="info-cell__label">利润</div><div class="info-cell__value">'+profitRate+'%</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">食材成本(元/100g</div><div class="info-cell__value">¥ '+ingredientCost+'</div></div>'
+ '</div>'
+ '</div>';
}
@@ -640,7 +630,7 @@ function renderNutriSection(d){
function renderCook(d){
var c = d.cook;
return '<div class="info-section"><div class="info-section__title">烹饪工艺 <span><a class="link" href="din-dish-add.html?mode=edit&tab=2">编辑</a></span></div>'
return '<div class="info-section"><div class="info-section__title">烹饪工艺 <span><a class="link" href="din-dish-add.html?mode=edit&tab=3">编辑</a></span></div>'
+ '<div class="info-grid">'
+ '<div class="info-cell"><div class="info-cell__label">主烹饪法</div><div class="info-cell__value">'+c.mainMethod+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">辅烹饪法</div><div class="info-cell__value">'+c.subMethod+'</div></div>'
@@ -654,7 +644,7 @@ function renderCook(d){
function renderTaste(d){
const t = d.taste;
const gradeTag = t.flavorGrade==='优秀'?'green':t.flavorGrade==='良好'?'blue':t.flavorGrade==='一般'?'orange':'red';
return `<div class="info-section"><div class="info-section__title">味道档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=3">编辑</a></span></div>
return `<div class="info-section"><div class="info-section__title">味道档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=4">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">酸度 (1-5)</div><div class="info-cell__value">${t.sour}</div></div>
<div class="info-cell"><div class="info-cell__label">甜度 (1-5)</div><div class="info-cell__value">${t.sweet}</div></div>
@@ -670,7 +660,7 @@ function renderTaste(d){
function renderColor(d){
const c = d.color;
const gradeTag = c.colorGrade==='优秀'?'green':c.colorGrade==='良好'?'blue':c.colorGrade==='一般'?'orange':'red';
return `<div class="info-section"><div class="info-section__title">色泽档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=5">编辑</a></span></div>
return `<div class="info-section"><div class="info-section__title">色泽档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=6">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">主色调</div><div class="info-cell__value">${c.mainColor}</div></div>
<div class="info-cell"><div class="info-cell__label">色泽均匀度 (1-5)</div><div class="info-cell__value">${c.uniformity}</div></div>
@@ -685,7 +675,7 @@ function renderColor(d){
function renderTexture(d){
const t = d.texture;
const gradeTag = t.textureGrade==='优秀'?'green':t.textureGrade==='良好'?'blue':t.textureGrade==='一般'?'orange':'red';
return `<div class="info-section"><div class="info-section__title">口感档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=4">编辑</a></span></div>
return `<div class="info-section"><div class="info-section__title">口感档案 <span><a class="link" href="din-dish-add.html?mode=edit&tab=5">编辑</a></span></div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">软硬度 (1-5)</div><div class="info-cell__value">${t.hardness}</div></div>
<div class="info-cell"><div class="info-cell__label">黏糯度 (1-5)</div><div class="info-cell__value">${t.sticky}</div></div>
@@ -745,79 +735,8 @@ function togglePromoVideo(el, dishIdx, videoIdx){
}
}
/* 成本核算:计算每百克成本并展示明细 */
function calcDetailCost(dishIdx){
var dish = DISHES[dishIdx];
if(!dish) return;
var ingredientCost = (dish.pricing && dish.pricing.ingredientCost) || 0;
var cookedWeight = dish.cookedWeight || 0;
if(cookedWeight <= 0){ alert('请先在「餐品构成」中填写熟重'); return; }
if(ingredientCost <= 0){ alert('当前食材成本为 0,请先录入食材'); return; }
var per100gCost = ingredientCost / cookedWeight * 100;
var row = document.getElementById('costResultRow');
if(row) row.style.display = 'flex';
var display = document.getElementById('costPriceDisplay');
if(display) display.textContent = per100gCost.toFixed(2);
/* 同步弹窗明细 */
var detailTbody = document.getElementById('costDetailTbody');
if(detailTbody){
var rows = dish.composition || [];
if(rows.length === 0){
detailTbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-placeholder);">暂无食材</td></tr>';
} else {
var html = '';
var total = 0;
rows.forEach(function(c){
var amount = parseFloat(c.amount) || 0;
var unitPrice = 18;
if((c.type||'').indexOf('辅材')>=0) unitPrice = 12;
else if((c.type||'').indexOf('调料')>=0) unitPrice = 8;
var subtotal = amount * unitPrice / 1000;
total += subtotal;
html += '<tr><td>'+c.name+'</td><td style="text-align:right;">'+amount+'</td><td style="text-align:right;">'+unitPrice.toFixed(2)+'</td><td style="text-align:right;">'+subtotal.toFixed(2)+'</td></tr>';
});
detailTbody.innerHTML = html;
var totalEl = document.getElementById('costDetailTotal');
if(totalEl) totalEl.textContent = total.toFixed(2);
var weightEl = document.getElementById('costDetailWeight');
if(weightEl) weightEl.textContent = cookedWeight;
var per100gEl = document.getElementById('costDetailPer100g');
if(per100gEl) per100gEl.textContent = per100gCost.toFixed(2);
}
}
}
/* 成本核算相关函数已移除(详情页不再展示成本核算,编辑/新增页 din-dish-add.html 自带实现) */
/* 小问号弹窗显示/切换 */
function toggleCostDetailPop(e){
if(e) e.stopPropagation();
var pop = document.getElementById('costDetailPop');
if(pop) pop.classList.toggle('open');
}
/* 应用成本价到称重基础价(叠加利润率) */
function applyDetailCost(dishIdx){
var dish = DISHES[dishIdx];
if(!dish) return;
var display = document.getElementById('costPriceDisplay');
var cost = parseFloat(display && display.textContent) || 0;
if(cost <= 0){ alert('请先点击成本核算'); return; }
var rate = (dish.pricing && dish.pricing.profitRate) || 0;
var base = cost * (1 + rate/100);
if(!dish.pricing) dish.pricing = {};
dish.pricing.basePrice = base.toFixed(2);
dish.pricing.per100gCost = cost.toFixed(2);
alert('已应用到称重基础价:¥'+base.toFixed(2)+'\n(成本 ¥'+cost.toFixed(2)+' × (1 + '+rate+'%)');
renderDetail();
}
/* 文档点击关闭弹窗 */
document.addEventListener('click', function(e){
var pop = document.getElementById('costDetailPop');
if(!pop || !pop.classList.contains('open')) return;
var help = e.target.closest('.cost-help');
var inPop = e.target.closest('.cost-detail-pop');
if(!help && !inPop) pop.classList.remove('open');
});
(function(){var a=document.querySelector('.sidebar-item.active');if(!a)return;var s=a.closest('.sidebar-subgroup');if(s)s.classList.add('open');})();
// 初始化
+13 -5
View File
@@ -296,6 +296,12 @@ var STEP_HTML = [
<div class="form-group"><label class="form-label">可食部分(%)</label><input class="form-input" id="edible" placeholder="0~100"></div>
<div class="form-group"><label class="form-label">水分(%)</label><input class="form-input" id="waterRate" placeholder="0~100"></div>
<div class="form-group"><label class="form-label">数据来源</label><select class="form-select" id="dataSource"><option value="1">手动添加</option></select></div>
<div class="form-group"><label class="form-label">基础定价</label>
<div style="position:relative;">
<input class="form-input" id="basePrice" type="number" step="0.01" min="0" placeholder="请输入" style="padding-right:70px;">
<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>
@@ -727,7 +733,7 @@ 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',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:[{name:'洗净米',yieldRate:'97'},{name:'免淘米',yieldRate:'95'}],
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,
@@ -738,7 +744,7 @@ 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',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:[{name:'去筋膜',yieldRate:'92'},{name:'切丁',yieldRate:'88',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,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,
@@ -749,7 +755,7 @@ var EDIT_FOODS = {
color:{main:'粉红色',grade:'优',desc:'色泽粉嫩,纹理清晰',cause:'肌红蛋白含量适中'},
taste:{main:'鲜味',sub:'微甜',intensity:'中',desc:'肉质鲜嫩,口感清甜'},
texture:{main:'嫩滑',chew:'易',digest:'中',desc:'纤维细腻,口感软嫩',cook:'腌制后煎炒口感最佳'}},
'0501001':{name:'鸡蛋(生)',code:'0501001',alias:'鲜鸡蛋',firstClass:'蛋类',secondClass:'鸡蛋',edible:'88',waterRate:'74.1',dataSource:'0',vegTypes:[],
'0501001':{name:'鸡蛋(生)',code:'0501001',alias:'鲜鸡蛋',firstClass:'蛋类',secondClass:'鸡蛋',edible:'88',waterRate:'74.1',dataSource:'0',basePrice:'6.5',vegTypes:[],
crowd:[{name:'全人群',level:'推荐'},{name:'婴幼儿',level:'推荐'},{name:'老年人',level:'推荐'}],
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.13,vitamin_b12:1.0,vitamin_c:0,vitamin_d:2.2,vitamin_e:1.84,vitamin_k:0,niacin:0.2,folic_acid:113,biotin:0,pantothenic_acid:1.3,total_choline:0,
@@ -760,7 +766,7 @@ 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',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:[{name:'去根',yieldRate:'90'},{name:'切段',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,
@@ -771,7 +777,7 @@ 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',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:[{name:'切块',yieldRate:'96',cutSpec:{shape:'块(中)',length:30,width:30,height:20,technique:'直刀'}},{name:'切丁',yieldRate:'94',cutSpec:{shape:'丁(中)',length:10,width:10,height:10,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,
@@ -800,6 +806,7 @@ function initEditMode(){
formData['edible'] = food.edible || '';
formData['waterRate'] = food.waterRate || '';
formData['dataSource'] = food.dataSource === 0 ? '0' : String(food.dataSource);
formData['basePrice'] = food.basePrice || '';
// 填充食物分类
selectedCategory.l1 = food.firstClass;
@@ -955,6 +962,7 @@ function confirmCopy(){
formData['edible'] = food.edible || '';
formData['waterRate'] = food.waterRate || '';
formData['dataSource'] = food.dataSource === 0 ? '0' : String(food.dataSource || '0');
formData['basePrice'] = food.basePrice || '';
// 净菜类型
vegTypes = food.vegTypes ? food.vegTypes.slice() : [];
+6 -5
View File
@@ -373,7 +373,7 @@ function formatCutSpec(cut){
// 食物数据(对齐 yx_mater_base_material 表结构)
const FOODS = [
{name:'粳米(生)',code:'0101001',alias:'大米',firstClass:'谷薯类',secondClass:'稻米',icon:'🌾',edible:'100',waterRate:'13.3',dataSource:0,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:[{name:'洗净米',yieldRate:'97'},{name:'免淘米',yieldRate:'95'}],
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,
@@ -383,7 +383,7 @@ const FOODS = [
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,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:'直刀'}}],
{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:'直刀'}}],
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,
@@ -393,7 +393,7 @@ const FOODS = [
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:'老年人建议炖煮至软烂,避免大块'}},
{name:'菠菜',code:'0701001',alias:'菠菜',firstClass:'蔬菜类',secondClass:'叶菜类',icon:'🥬',edible:'89',waterRate:'91.2',dataSource:0,vegTypes:[],
{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,
ca:66,phosphorus:47,kalium:311,mg:58,na:85.2,fe:2.9,zn:0.85,se:0.97,cu:0.1,mn:0.66,iodine:0,
@@ -403,7 +403,7 @@ const FOODS = [
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:'0401001',alias:'鸡蛋',firstClass:'蛋类',secondClass:'鸡蛋',icon:'🥚',edible:'88',waterRate:'74.1',dataSource:0,vegTypes:[],
{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,
ca:56,phosphorus:195,kalium:154,mg:10,na:131.5,fe:2.0,zn:1.1,se:14.34,cu:0.15,mn:0.04,iodine:27.2,
@@ -413,7 +413,7 @@ const FOODS = [
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,vegTypes:[{name:'豆腐块',yieldRate:'98',cutSpec:{shape:'块(中)',length:30,width:30,height:20,technique:'直刀'}}],
{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:'直刀'}}],
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,
@@ -473,6 +473,7 @@ 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>`;
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('');
+30
View File
@@ -1,5 +1,35 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-06] feat: 由食谱快速创建订单支持餐次级人数 + 新增「生成中」状态
### 变更背景
原「由食谱快速创建订单」弹窗要求用户在顶部统一填写就餐人数,但实际业务中早/午/晚/加餐各餐次人数差异较大,统一人数导致采购量核算不准。同时订单创建后系统需要根据人数智能核算采购量,该过程有耗时,原状态机无对应中间态,用户无法感知订单是否已生成完毕。
### 新增(`prod-clean-order.html`
- 食谱弹窗增加「预定配送时间」字段(datetime-local),位于食谱日期右侧
- 「当日食谱」表格新增「就餐人数」列,每个餐次独立填写人数(4 个 input)
- 新增 `validateMealDinerCounts()` 校验所选餐次人数是否填写完整
- 新增「生成中」订单状态:青色脉冲呼吸动效 `.tag-cyan` + `@keyframes statusPulse`
- `aggregateStatus()` 增加「生成中」聚合规则:任意一行生成中即整单生成中
- 订单状态筛选下拉框增加「生成中」选项
- 统计卡片区增加「生成中」卡片
- 订单操作列增加「生成完成」入口(`orderAction` 新增 `finishGenerate` 分支,流转到「备货中」)
- 食谱弹窗底部新增智能核算提示语(`.recipe-tip` 渐变蓝底提示框)
### 调整
- 食谱弹窗移除顶部「就餐人数」全局输入框
- 移除「订单汇总」预览区(`#recipeOrderSummary` 整块)
- 食谱餐品份数计算逻辑改为按各餐次人数累加:同菜品跨餐次时 dinerCount 累加,weight 按总份数核算
- 由食谱创建的订单 `lines[].status` 初始置为「生成中」(原为「备货中」)
- `submitRecipeOrder` 校验新增预定配送时间必填 + 所选餐次人数必填
- 默认配送时间函数 `getDefaultDeliveryTime()`:食谱日期次日 10:00
- 弹窗打开初始化逻辑移除已删除的 `recipeDinerCount`/`recipeOrderSummary` 引用
### 润色后提示语
> ⏳ 系统智能核算提示:订单提交后,系统将依据您填写的各餐次就餐人数自动核算采购量并生成净菜明细,处理过程约需 1~2 分钟。生成结果与最终订单金额将在 订单列表 中实时更新,请耐心等待并关注订单状态变化。
---
## [2026-08-05] feat: 净菜订单/申领页面统一新增订单金额字段与总金额栏
### 变更背景
+77 -74
View File
@@ -64,6 +64,15 @@ tr.detail-row td:first-child{padding-left:44px;}
.tag-red{background:#FFF1F0;color:var(--danger);}
.tag-gray{background:#F5F5F5;color:#999;}
.tag-purple{background:#F9F0FF;color:#722ED1;}
/* 生成中状态:脉冲呼吸动效 */
.tag-cyan{background:#E6FFFB;color:#08979C;border:1px solid #87E8DE;}
.tag-cyan::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#13C2C2;margin-right:5px;vertical-align:middle;animation:statusPulse 1.4s ease-in-out infinite;}
@keyframes statusPulse{0%,100%{opacity:0.3;transform:scale(0.8);}50%{opacity:1;transform:scale(1.2);}}
/* 食谱创建提示语 */
.recipe-tip{display:flex;align-items:flex-start;gap:8px;margin-top:16px;padding:12px 16px;background:linear-gradient(90deg,#E6F7FF 0%,#F0F9FF 100%);border:1px solid #91D5FF;border-radius:var(--radius-md);color:#096DD9;font-size:var(--font-sm);line-height:1.6;}
.recipe-tip__icon{font-size:18px;flex-shrink:0;line-height:1.4;}
.recipe-tip__text{flex:1;}
.recipe-tip__text b{color:#0050B3;}
.link{color:var(--primary);cursor:pointer;font-size:var(--font-sm);}.link:hover{color:#40a9ff;}
.pagination{display:flex;align-items:center;justify-content:flex-end;padding:16px 24px;gap:8px;border-top:1px solid var(--border);}
.pagination span{font-size:var(--font-sm);color:var(--text-secondary);}
@@ -212,15 +221,15 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<div class="stats-row">
<div class="stat-card"><div class="stat-card__icon blue">📝</div><div class="stat-card__info"><div class="stat-card__value">8</div><div class="stat-card__label">今日订单数</div></div></div>
<div class="stat-card"><div class="stat-card__icon" style="background:#E6FFFB;"></div><div class="stat-card__info"><div class="stat-card__value">0</div><div class="stat-card__label">生成中</div></div></div>
<div class="stat-card"><div class="stat-card__icon green"></div><div class="stat-card__info"><div class="stat-card__value">4</div><div class="stat-card__label">已完成</div></div></div>
<div class="stat-card"><div class="stat-card__icon orange">🚚</div><div class="stat-card__info"><div class="stat-card__value">2</div><div class="stat-card__label">配送中</div></div></div>
<div class="stat-card"><div class="stat-card__icon purple">📦</div><div class="stat-card__info"><div class="stat-card__value">2</div><div class="stat-card__label">部分完成</div></div></div>
</div>
<div class="filter-bar">
<div class="filter-item"><label>订单号/菜品名称</label><input class="filter-input" type="text" placeholder="请输入订单号或菜品名称"></div>
<div class="filter-item"><label>净菜类型</label><select class="filter-select"><option>全部</option><option>净菜包</option><option>餐品净菜包</option></select></div>
<div class="filter-item"><label>订单类型</label><select class="filter-select"><option>全部</option><option>内供</option><option>外售</option></select></div>
<div class="filter-item"><label>订单状态</label><select class="filter-select"><option>全部</option><option>备货中</option><option>待配送</option><option>配送中</option><option>部分完成</option><option>已完成</option></select></div>
<div class="filter-item"><label>订单状态</label><select class="filter-select"><option>全部</option><option>生成中</option><option>备货中</option><option>待配送</option><option>配送中</option><option>部分完成</option><option>已完成</option></select></div>
<div class="filter-item"><label>预定配送时间</label><input class="filter-input" type="date" style="min-width:150px"> ~ <input class="filter-input" type="date" style="min-width:150px"></div>
<button class="btn btn-primary">查询</button><button class="btn btn-default">重置</button>
</div>
@@ -311,26 +320,21 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<input type="date" id="recipeDate" onchange="onRecipeDateChange()">
</div>
<div class="form-item">
<div class="form-item__label">就餐人数<span class="required">*</span></div>
<input id="recipeDinerCount" inputmode="numeric" placeholder="请输入就餐人数" oninput="sanitizeIntegerInput(this);onRecipeParamChange()">
<div class="form-item__label">预定配送时间<span class="required">*</span></div>
<input type="datetime-local" id="recipeDeliveryTime">
</div>
</div>
<div id="dailyMenuSection" style="margin-top:20px;display:none;">
<div class="drawer-section__title">当日食谱</div>
<div class="drawer-section__title">当日食谱(每餐次可单独设置就餐人数)</div>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;" id="dailyMenuTable">
<thead><tr><th style="width:60px;">餐次</th><th>餐品</th><th style="width:60px;">选择</th></tr></thead>
<thead><tr><th style="width:70px;">餐次</th><th>餐品</th><th style="width:130px;">就餐人数</th><th style="width:60px;">选择</th></tr></thead>
<tbody id="dailyMenuBody"></tbody>
</table>
</div>
</div>
<div id="recipeOrderSummary" style="margin-top:20px;display:none;">
<div class="drawer-section__title">订单汇总(将生成一个订单,包含以下所有菜品)</div>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead><tr><th>餐品名称</th><th>包装规格</th><th>需求份数</th><th>需求计量(kg)</th></tr></thead>
<tbody id="recipeOrderSummaryBody"></tbody>
</table>
<div class="recipe-tip">
<span class="recipe-tip__icon"></span>
<span class="recipe-tip__text"><b>系统智能核算提示:</b>订单提交后,系统将依据您填写的各餐次就餐人数自动核算采购量并生成净菜明细,处理过程约需 1~2 分钟。生成结果与最终订单金额将在 <b>订单列表</b> 中实时更新,请耐心等待并关注订单状态变化。</span>
</div>
</div>
</div>
@@ -475,6 +479,8 @@ function formatMoney(n){var v=parseFloat(n)||0;return '¥'+v.toLocaleString('zh-
/* ========== 状态聚合逻辑 ========== */
function aggregateStatus(lines){
var statuses=lines.map(function(l){return l.status;});
/* 任意一行处于生成中 → 整单生成中 */
if(statuses.some(function(s){return s==='生成中';}))return '生成中';
if(statuses.every(function(s){return s==='已完成';}))return '已完成';
if(statuses.some(function(s){return s==='配送中';}) && !statuses.some(function(s){return s==='备货中'||s==='待配送';}))return '配送中';
if(statuses.some(function(s){return s==='待配送';}) && !statuses.some(function(s){return s==='备货中';}))return '待配送';
@@ -487,7 +493,7 @@ function aggregateStatus(lines){
}
function statusTag(status){
var map={已完成:'tag-green',配送中:'tag-blue',待配送:'tag-orange',备货中:'tag-gray','部分完成':'tag-purple'};
var map={'生成中':'tag-cyan',已完成:'tag-green',配送中:'tag-blue',待配送:'tag-orange',备货中:'tag-gray','部分完成':'tag-purple'};
return '<span class="tag '+map[status]+'">'+status+'</span>';
}
@@ -524,7 +530,9 @@ function renderOrderTable(){
html+='<td>'+order.creator+'</td>';
/* 操作列:状态按钮 + 详情 + 删除 */
html+='<td class="td-actions">';
if(aggStatus==='备货中'){
if(aggStatus==='生成中'){
html+='<span class="link" style="color:#08979C;" onclick="event.stopPropagation();orderAction(\''+order.orderNo+'\',\'finishGenerate\')">生成完成</span> ';
}else if(aggStatus==='备货中'){
html+='<span class="link" style="color:var(--success);" onclick="event.stopPropagation();orderAction(\''+order.orderNo+'\',\'completePrep\')">备货完成</span> ';
}else if(aggStatus==='待配送'){
html+='<span class="link" style="color:var(--primary);" onclick="event.stopPropagation();orderAction(\''+order.orderNo+'\',\'deliver\')">立即配送</span> ';
@@ -585,7 +593,8 @@ function orderAction(orderNo,action){
var order=orders.find(function(o){return o.orderNo===orderNo;});
if(!order)return;
var nextStatus='';
if(action==='completePrep')nextStatus='待配送';
if(action==='finishGenerate')nextStatus='备货中';
else if(action==='completePrep')nextStatus='待配送';
else if(action==='deliver')nextStatus='配送中';
else if(action==='receive')nextStatus='已完成';
if(!nextStatus)return;
@@ -1030,6 +1039,15 @@ function getDishSpecForRecipe(name){
var MEAL_NAMES=['早餐','午餐','晚餐','加餐'];
function getDefaultDate(){return '2026-05-20';}
/* 默认配送时间:食谱日期次日 10:00 */
function getDefaultDeliveryTime(){
var d=new Date(getDefaultDate());
d.setDate(d.getDate()+1);
var yyyy=d.getFullYear();
var mm=String(d.getMonth()+1).padStart(2,'0');
var dd=String(d.getDate()).padStart(2,'0');
return yyyy+'-'+mm+'-'+dd+'T10:00';
}
function getDailyMenu(dateStr){
var d=new Date(dateStr);
@@ -1049,107 +1067,93 @@ function onRecipeDateChange(){
var tbody=document.getElementById('dailyMenuBody');
if(!dateVal){
dailySection.style.display='none';
document.getElementById('recipeOrderSummary').style.display='none';
return;
}
var daily=getDailyMenu(dateVal);
if(!daily){
tbody.innerHTML='<tr><td colspan="3" style="text-align:center;color:var(--text-placeholder);padding:16px;">该日期暂无食谱数据</td></tr>';
tbody.innerHTML='<tr><td colspan="4" style="text-align:center;color:var(--text-placeholder);padding:16px;">该日期暂无食谱数据</td></tr>';
dailySection.style.display='block';
document.getElementById('recipeOrderSummary').style.display='none';
return;
}
var rows='';
for(var mi=0;mi<4;mi++){
var dishes=daily.meals[mi]&&daily.meals[mi][daily.dayIdx]||[];
var dishHtml=dishes.length?dishes.join('、'):'<span style="color:var(--text-placeholder);">—</span>';
rows+='<tr><td style="font-weight:600;">'+MEAL_NAMES[mi]+'</td><td>'+dishHtml+'</td>'
+'<td style="text-align:center;"><input type="checkbox" class="meal-cb" value="'+mi+'" onchange="onRecipeParamChange()"></td></tr>';
rows+='<tr>'
+'<td style="font-weight:600;">'+MEAL_NAMES[mi]+'</td>'
+'<td>'+dishHtml+'</td>'
+'<td><input type="text" inputmode="numeric" class="meal-diner-input" data-meal-idx="'+mi+'" placeholder="请输入人数" oninput="sanitizeIntegerInput(this)" style="width:110px;height:30px;border:1px solid var(--border);border-radius:4px;padding:0 8px;font-size:var(--font-sm);outline:none;"></td>'
+'<td style="text-align:center;"><input type="checkbox" class="meal-cb" value="'+mi+'"></td>'
+'</tr>';
}
tbody.innerHTML=rows;
dailySection.style.display='block';
document.getElementById('recipeOrderSummary').style.display='none';
}
function onRecipeParamChange(){
var dateVal=document.getElementById('recipeDate').value;
var dinerCount=parseInt(document.getElementById('recipeDinerCount').value||'0',10);
var summaryDiv=document.getElementById('recipeOrderSummary');
var tbody=document.getElementById('recipeOrderSummaryBody');
if(!dateVal||dinerCount<=0){summaryDiv.style.display='none';return;}
var daily=getDailyMenu(dateVal);
if(!daily){summaryDiv.style.display='none';return;}
var mealIndices=[];
document.querySelectorAll('#dailyMenuBody .meal-cb:checked').forEach(function(cb){mealIndices.push(parseInt(cb.value));});
if(mealIndices.length===0){summaryDiv.style.display='none';return;}
var dishMap={};
mealIndices.forEach(function(mi){
var dayDishes=daily.meals[mi]&&daily.meals[mi][daily.dayIdx]||[];
dayDishes.forEach(function(dishName){
if(!dishMap[dishName])dishMap[dishName]=getDishSpecForRecipe(dishName);
});
/* 校验所选餐次是否都已填写就餐人数,并返回缺失餐次名 */
function validateMealDinerCounts(checkedMealIndices){
var missing=[];
checkedMealIndices.forEach(function(mi){
var input=document.querySelector('#dailyMenuBody .meal-diner-input[data-meal-idx="'+mi+'"]');
var val=input?input.value.trim():'';
if(!val||parseInt(val)<=0)missing.push(MEAL_NAMES[mi]);
});
var dishNames=Object.keys(dishMap);
if(dishNames.length===0){summaryDiv.style.display='none';return;}
var rows='';
for(var name in dishMap){
var sp=dishMap[name];
var servings=dinerCount;
var weightKg=(sp.baseGram*servings/1000).toFixed(2);
var specTxt=sp.baseGram+'g/'+(sp.portion||sp.unit);
rows+='<tr><td>'+name+'</td><td>'+specTxt+'</td><td>'+servings+'</td><td>'+weightKg+'</td></tr>';
}
tbody.innerHTML=rows;
summaryDiv.style.display='block';
return missing;
}
function submitRecipeOrder(){
var recipeDate=document.getElementById('recipeDate').value;
var dinerCount=document.getElementById('recipeDinerCount').value;
var meals=[];
document.querySelectorAll('#dailyMenuBody .meal-cb:checked').forEach(function(cb){meals.push(MEAL_NAMES[parseInt(cb.value)]);});
if(!recipeDate){alert('请选择食谱日期');return;}
if(meals.length===0){alert('请至少选择一个餐次');return;}
if(!dinerCount||parseInt(dinerCount)<=0){alert('请输入有效的就餐人数');return;}
/* 由食谱创建一个包含多道菜品的订单 */
var daily=getDailyMenu(recipeDate);
var deliveryTime=document.getElementById('recipeDeliveryTime').value;
var mealIndices=[];
document.querySelectorAll('#dailyMenuBody .meal-cb:checked').forEach(function(cb){mealIndices.push(parseInt(cb.value));});
if(!recipeDate){alert('请选择食谱日期');return;}
if(!deliveryTime){alert('请选择预定配送时间');return;}
if(mealIndices.length===0){alert('请至少选择一个餐次');return;}
var missing=validateMealDinerCounts(mealIndices);
if(missing.length>0){alert('以下餐次尚未填写就餐人数:'+missing.join('、'));return;}
/* 由食谱创建一个包含所选餐次全部菜品的订单,订单初始状态为「生成中」 */
var daily=getDailyMenu(recipeDate);
var dishMap={};
mealIndices.forEach(function(mi){
var input=document.querySelector('#dailyMenuBody .meal-diner-input[data-meal-idx="'+mi+'"]');
var dinerCount=parseInt(input.value.trim(),10);
var dayDishes=daily.meals[mi]&&daily.meals[mi][daily.dayIdx]||[];
dayDishes.forEach(function(dishName){
if(!dishMap[dishName])dishMap[dishName]=getDishSpecForRecipe(dishName);
if(!dishMap[dishName])dishMap[dishName]={spec:getDishSpecForRecipe(dishName),mealIdx:mi,dinerCount:dinerCount};
else{
/* 同菜品跨餐次:累加人数(按总人数核算) */
dishMap[dishName].dinerCount+=dinerCount;
}
});
});
var orderNo='CO-'+String(orders.length+1).padStart(3,'0');
var newLines=[];
var di=0;
for(var name in dishMap){
var sp=dishMap[name];
var item=dishMap[name];
var sp=item.spec;
var baseGram=sp.baseGram;
var servings=item.dinerCount;
newLines.push({
id:'L'+(Date.now()+Object.keys(dishMap).indexOf(name)),
id:'L'+(Date.now()+di),
ncType:'cp',foodName:name,
spec:baseGram+'g/'+(sp.portion||sp.unit),
portion:sp.portion,baseGram:baseGram,unit:sp.unit,
qty:parseInt(dinerCount),
weight:parseFloat((baseGram*parseInt(dinerCount)/1000).toFixed(2)),
status:'备货中'
qty:servings,
weight:parseFloat((baseGram*servings/1000).toFixed(2)),
status:'生成中'
});
di++;
}
orders.unshift({
orderNo:orderNo,orderType:'内供',ncType:'cp',
claimTime:new Date().toISOString().slice(0,16).replace('T',' '),
deliverTime:recipeDate+' 10:00',creator:'系统',
deliverTime:deliveryTime.replace('T',' '),creator:'系统',
lines:newLines
});
closeModal('modal-recipe-order');
renderOrderTable();
document.getElementById('recipeDate').value=getDefaultDate();
document.getElementById('recipeDinerCount').value='';
document.getElementById('dailyMenuSection').style.display='none';
document.getElementById('recipeOrderSummary').style.display='none';
alert('已根据 '+recipeDate+' 食谱创建订单 '+orderNo+',包含 '+newLines.length+' 道菜品');
alert('订单 '+orderNo+' 已提交,系统正在根据各餐次就餐人数智能核算采购量,请关注订单状态变化(生成中 → 备货中)。');
}
function sanitizeIntegerInput(el){el.value=el.value.replace(/[^\d]/g,'');}
@@ -1161,9 +1165,8 @@ function sanitizeIntegerInput(el){el.value=el.value.replace(/[^\d]/g,'');}
if(id==='modal-order')resetOrderForm();
if(id==='modal-recipe-order'){
document.getElementById('recipeDate').value=getDefaultDate();
document.getElementById('recipeDinerCount').value='';
document.getElementById('recipeDeliveryTime').value=getDefaultDeliveryTime();
document.getElementById('dailyMenuSection').style.display='none';
document.getElementById('recipeOrderSummary').style.display='none';
onRecipeDateChange();
}
document.getElementById(id).classList.add('show');