feat: 净菜订单生成历史加建议熟重/规格列 + 食谱快速创建份数改规格

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhangyan
2026-09-01 11:33:08 +08:00
co-authored by Claude Opus 4.6
parent 7333189799
commit 2996e835c7
2 changed files with 51 additions and 27 deletions
+6
View File
@@ -1,5 +1,11 @@
# 营养用餐管理端 CHANGELOG
## [2026-09-01] feat: 净菜订单生成历史加建议熟重/规格列 + 食谱快速创建份数改规格
- `prod-clean-order.html` 净菜订单:
- 「净菜订单生成历史」弹窗新增「建议熟重(kg)」列(按就餐人数推算所需熟重)与「规格」列(规格重量/规格名称,读取自有餐品库-餐品定价规格)
- 「由食谱快速创建订单」弹窗餐品明细「份数」改为「规格」下拉(规格重量/规格名称),份数按所选规格自动计算(每份生重>4kg 按 4kg/份反算,否则=就餐人数)
## [2026-08-31] feat: 餐线营养秤概览筛选栏下沉 + 重量变化折线图
- `din-terminal.html` 设备概览「餐线营养秤」:
+45 -27
View File
@@ -369,7 +369,7 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<div class="drawer-section__title">餐品明细(重复餐品已自动合并,请逐道确认是否需要内配调料)</div>
<div style="overflow-x:auto;">
<table class="inline-builder-table" id="dishSeasoningTable" style="margin-bottom:0;">
<thead><tr><th style="width:24%;">菜品名称</th><th>份数</th><th>内配调料</th></tr></thead>
<thead><tr><th style="width:24%;">菜品名称</th><th>规格</th><th>内配调料</th></tr></thead>
<tbody id="dishSeasoningBody"></tbody>
</table>
</div>
@@ -414,7 +414,7 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<!-- 生成记录明细(订单信息与餐品明细合并) -->
<div style="overflow-x:auto;">
<table class="ing-table">
<thead><tr><th>食谱日期</th><th>餐次</th><th>餐品名称</th><th>就餐人数</th><th>内配料(是否)</th><th>打包份数</th><th>预定配送时间</th><th>开始生成时间</th><th>生成成功时间</th></tr></thead>
<thead><tr><th>食谱日期</th><th>餐次</th><th>餐品名称</th><th>就餐人数</th><th>建议熟重(kg)</th><th>内配料(是否)</th><th>规格</th><th>打包份数</th><th>预定配送时间</th><th>开始生成时间</th><th>生成成功时间</th></tr></thead>
<tbody id="genHistoryInfoBody"></tbody>
</table>
</div>
@@ -855,12 +855,16 @@ function renderGenHistoryOrder(orderNo){
var infoBody=document.getElementById('genHistoryInfoBody');
var html='';
rec.details.forEach(function(d){
var spec=getDishSpecForRecipe(d.foodName);
var specLabel=spec.baseGram+'g/'+spec.portion;
html+='<tr>'
+'<td>'+rec.recipeDate+'</td>'
+'<td>'+d.meal+'</td>'
+'<td style="font-weight:500;">'+d.foodName+'</td>'
+'<td>'+d.dinerCount+'</td>'
+'<td>'+(parseFloat(d.cookedWeight).toFixed(2))+' kg</td>'
+'<td>'+genSeasoningTag(d.needSeasoning)+'</td>'
+'<td>'+specLabel+'</td>'
+'<td>'+d.servings+'</td>'
+'<td>'+rec.deliverTime+'</td>'
+'<td>'+rec.startTime+'</td>'
@@ -1441,6 +1445,12 @@ function getDishSpecForRecipe(name){
}
return RECIPE_FALLBACK[name]||{portion:'标准份',baseGram:500,unit:'份'};
}
/* 食谱餐品可选规格列表:优先总库多规格,无则回退单规格 */
function getDishSpecOptionsForRecipe(name){
var specs=getSpecsByDish(name);
if(specs&&specs.length>0)return specs;
return [getDishSpecForRecipe(name)];
}
var MEAL_NAMES=['早餐','午餐','晚餐','加餐'];
function getDefaultDate(){return '2026-05-20';}
@@ -1499,8 +1509,8 @@ function onRecipeDateChange(){
/* ========== 餐品调料配置表(食谱快速创建专用) ========== */
/* recipeSeasoningMap: { 菜名: true|false },跨餐次重复菜品自动合并,状态在重渲时保留 */
var recipeSeasoningMap={};
/* recipeServingMap: { 菜名: 份数 },餐品明细表份数编辑值,重渲时保留 */
var recipeServingMap={};
/* recipeSpecMap: { 菜名: "baseGram|portion|unit" },餐品明细表所选规格,重渲时保留 */
var recipeSpecMap={};
function renderDishSeasoningTable(){
var section=document.getElementById('dishSeasoningSection');
var tbody=document.getElementById('dishSeasoningBody');
@@ -1533,23 +1543,21 @@ function renderDishSeasoningTable(){
/* 未配置过的菜品默认 false(不需要);已配置的保留 */
if(typeof recipeSeasoningMap[name]!=='boolean')recipeSeasoningMap[name]=false;
var ns=recipeSeasoningMap[name]===true;
/* 每份生重(克)取系统默认规格;就餐人数为该菜品跨餐次累加值 */
var sp=getDishSpecForRecipe(name);
var baseGram=sp.baseGram||500;
var dinerCount=dishDinerMap[name]||0;
/* 预测生重(总,克)= 每份生重 × 就餐人数 */
var predictedRawG=baseGram*dinerCount;
/* 份数:每份生重>4kg 时按最大包装克重 4kg/份 反算,否则取就餐人数 */
if(!recipeServingMap[name]){
recipeServingMap[name]=baseGram>4000?Math.ceil(predictedRawG/4000):dinerCount;
/* 规格列表:优先总库多规格,无则回退单规格 */
var specs=getDishSpecOptionsForRecipe(name);
/* 首次渲染默认选中第一个规格 */
if(!recipeSpecMap[name]&&specs.length>0){
recipeSpecMap[name]=specs[0].baseGram+'|'+specs[0].portion+'|'+(specs[0].unit||'份');
}
var servings=recipeServingMap[name];
var selVal=recipeSpecMap[name];
html+='<tr><td style="font-weight:500;">'+name+'</td>';
/* 份数:固定单位「份」,输入框编辑份数 */
html+='<td><div class="spec-input-group" style="justify-content:center;">'
+'<input inputmode="numeric" placeholder="份数" value="'+servings+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateRecipeServing(\''+name.replace(/'/g,'\\\'')+'\',this.value)" style="width:56px;">'
+'<span style="color:var(--text-secondary);font-size:var(--font-sm);white-space:nowrap;">份</span>'
+'</div></td>';
/* 规格:下拉选择 规格重量/规格名称 */
html+='<td><select onchange="updateRecipeSpec(\''+name.replace(/'/g,'\\\'')+'\',this.value)" style="width:130px;height:30px;border:1px solid var(--border);border-radius:4px;padding:0 6px;font-size:var(--font-sm);outline:none;">';
specs.forEach(function(s){
var sval=s.baseGram+'|'+s.portion+'|'+(s.unit||'份');
html+='<option value="'+sval+'"'+(sval===selVal?' selected':'')+'>'+s.baseGram+'g/'+s.portion+'</option>';
});
html+='</select></td>';
html+='<td><div class="seasoning-radio-group">';
html+='<label><input type="radio" name="rs-'+encodeURIComponent(name)+'" '+(ns?'checked':'')+' onchange="updateRecipeSeasoning(\''+name.replace(/'/g,'\\\'')+'\',true)"> 是</label>';
html+='<label><input type="radio" name="rs-'+encodeURIComponent(name)+'" '+(!ns?'checked':'')+' onchange="updateRecipeSeasoning(\''+name.replace(/'/g,'\\\'')+'\',false)"> 否</label>';
@@ -1560,9 +1568,9 @@ function renderDishSeasoningTable(){
function updateRecipeSeasoning(name,val){
recipeSeasoningMap[name]=!!val;
}
/* 更新食谱餐品份数 */
function updateRecipeServing(name,val){
recipeServingMap[name]=parseInt(val,10)||0;
/* 更新食谱餐品所选规格 */
function updateRecipeSpec(name,val){
recipeSpecMap[name]=val;
}
/* 校验所选餐次是否都已填写就餐人数,并返回缺失餐次名 */
@@ -1628,15 +1636,25 @@ function submitRecipeOrder(){
for(var name in dishMap){
var item=dishMap[name];
var sp=item.spec;
/* 包装规格固定为「份」,份数优先取餐品明细表编辑值,未编辑回退就餐人数 */
var baseGram=sp.baseGram||500;
var unit='份';
var servings=recipeServingMap[name]||item.dinerCount;
var portion=sp.portion||'标准份';
var unit=sp.unit||'份';
/* 采用餐品明细表所选规格 */
if(recipeSpecMap[name]){
var spParts=recipeSpecMap[name].split('|');
if(spParts.length>=2){
baseGram=parseFloat(spParts[0])||baseGram;
portion=spParts[1]||portion;
unit=spParts[2]||'份';
}
}
/* 份数按所选规格自动计算:每份生重>4kg 时按 4kg/份 反算,否则=就餐人数 */
var servings=baseGram>4000?Math.ceil(baseGram*item.dinerCount/4000):item.dinerCount;
newLines.push({
id:'L'+(Date.now()+di),
ncType:'cp',foodName:name,
spec:baseGram+'g/'+unit,
portion:sp.portion,baseGram:baseGram,unit:unit,
portion:portion,baseGram:baseGram,unit:unit,
qty:servings,
dinerCount:item.dinerCount,
weight:parseFloat((baseGram*item.dinerCount/1000).toFixed(2)),
@@ -1670,7 +1688,7 @@ function sanitizeIntegerInput(el){el.value=el.value.replace(/[^\d]/g,'');}
document.getElementById('recipeDeliveryTime').value=getDefaultDeliveryTime();
document.getElementById('dailyMenuSection').style.display='none';
recipeSeasoningMap={};
recipeServingMap={};
recipeSpecMap={};
onRecipeDateChange();
}
if(id==='modal-gen-history'){document.getElementById('ghDate').value='';renderGenHistory();}