feat: 餐线设置新增用户范围必填复选 + 食谱订单餐品明细表份数调整

- din-canteen-line.html: 餐线设置/新增餐线弹窗「基础信息」下新增「用户范围」必填复选(用户类型多选)
- prod-clean-order.html: 食谱快速创建订单弹窗餐品明细表移除「包装规格」列,改为「份数」列(固定单位「份」);提示语追加最大包装克重4kg/份说明
This commit is contained in:
zhangyan
2026-08-25 11:39:01 +08:00
parent 2922834e9d
commit 00cd56530c
3 changed files with 83 additions and 32 deletions
+40
View File
@@ -98,6 +98,10 @@ tr:hover td{background:#FAFAFA;}
.form-row-inline{display:flex;gap:16px;align-items:flex-start;}
.form-row-inline .form-row{flex:1;}
.form-row-inline .form-row.status-row{flex:0 0 140px;}
/* 用户范围复选 */
.checkbox-group{display:flex;flex-wrap:wrap;gap:8px 20px;padding:8px 12px;background:#FAFAFA;border:1px solid var(--border);border-radius:var(--radius-sm);}
.checkbox-item{display:inline-flex;align-items:center;gap:6px;font-size:var(--font-sm);color:var(--text-primary);cursor:pointer;user-select:none;}
.checkbox-item input[type="checkbox"]{width:14px;height:14px;cursor:pointer;accent-color:var(--primary);}
/* 分区标题 */
.form-section-title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);margin:20px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.form-section-title:first-child{margin-top:0;}
@@ -318,6 +322,17 @@ tr:hover td{background:#FAFAFA;}
</div>
</div>
<div class="form-row">
<label class="form-label"><em>*</em> 用户范围</label>
<div class="checkbox-group" id="lineUserScope">
<label class="checkbox-item"><input type="checkbox" value="staff"> 员工</label>
<label class="checkbox-item"><input type="checkbox" value="member"> 会员用户</label>
<label class="checkbox-item"><input type="checkbox" value="normal"> 普通用户</label>
<label class="checkbox-item"><input type="checkbox" value="worker"> 工作人员</label>
<label class="checkbox-item"><input type="checkbox" value="external"> 外部人员</label>
</div>
</div>
<!-- 收费模式 -->
<div class="form-section-title">
<span>收费模式</span>
@@ -380,6 +395,8 @@ var MODE_OPTIONS=[
{value:'byWeight',label:'称重计费'},
{value:'free',label:'免费'}
];
/* 用户类型选项(用户范围复选) */
var ALL_USER_TYPES=['staff','member','normal','worker','external'];
/* 生成收费模式矩阵:7天×4餐次,同一模式 */
function genCharging(mode, prices){
return WEEKDAYS.map(function(){
@@ -393,6 +410,7 @@ function genCharging(mode, prices){
var lines=[
{
id:'L1', name:'A餐线(自选餐)', settleType:'auto', status:'on',
userScope:['staff','member','normal','worker','external'],
charging: genCharging('byMeal', [8,15,15,6]),
devices:[
{type:'营养秤', code:'CL-A01', name:'餐线营养秤1号', days:326, online:true},
@@ -405,6 +423,7 @@ var lines=[
},
{
id:'L2', name:'B餐线(营养点餐)', settleType:'auto', status:'on',
userScope:['staff','member'],
charging: genCharging('byWeight'),
devices:[
{type:'营养秤', code:'CL-B01', name:'餐线营养秤4号', days:400, online:true},
@@ -415,6 +434,7 @@ var lines=[
},
{
id:'L3', name:'C餐线(清真)', settleType:'manual', status:'off',
userScope:['external','worker'],
charging: genCharging('free'),
devices:[
{type:'营养秤', code:'CL-C01', name:'餐线营养秤6号', days:90, online:true},
@@ -479,12 +499,14 @@ function openLineModal(id){
settle.value=l.settleType;
tempStatus=l.status;
tempCharging=JSON.parse(JSON.stringify(l.charging));
setUserScope(l.userScope||ALL_USER_TYPES);
}else{
title.textContent='新增餐线';
nameInput.value='';
settle.value='auto';
tempStatus='on';
tempCharging=genCharging('free');
setUserScope(ALL_USER_TYPES);
}
renderStatusSwitch();
renderChargeTable();
@@ -504,6 +526,20 @@ function toggleLineStatusSwitch(){
tempStatus=tempStatus==='on'?'off':'on';
renderStatusSwitch();
}
/* 用户范围复选:回填勾选状态 */
function setUserScope(arr){
document.querySelectorAll('#lineUserScope input[type="checkbox"]').forEach(function(cb){
cb.checked=(arr||[]).indexOf(cb.value)>-1;
});
}
/* 用户范围复选:读取当前勾选值 */
function getUserScope(){
var arr=[];
document.querySelectorAll('#lineUserScope input[type="checkbox"]:checked').forEach(function(cb){
arr.push(cb.value);
});
return arr;
}
/* ========== 收费模式矩阵 ========== */
function renderChargeTable(){
@@ -577,7 +613,9 @@ function applyBatch(){
function confirmLine(){
var name=document.getElementById('lineName').value.trim();
var settle=document.getElementById('lineSettle').value;
var userScope=getUserScope();
if(!name){ alert('请输入餐线名称'); return; }
if(userScope.length===0){ alert('请选择用户范围'); return; }
for(var w=0;w<7;w++){
for(var m=0;m<4;m++){
var c=tempCharging[w][m];
@@ -588,12 +626,14 @@ function confirmLine(){
var l=lines.find(function(x){return x.id===editingLineId;});
if(l){
l.name=name; l.settleType=settle; l.status=tempStatus;
l.userScope=userScope;
l.charging=JSON.parse(JSON.stringify(tempCharging));
}
}else{
lines.push({
id:'L'+Date.now(),
name:name, settleType:settle, status:tempStatus,
userScope:userScope,
charging:JSON.parse(JSON.stringify(tempCharging)),
devices:[]
});
+10
View File
@@ -1,5 +1,15 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-25] feat: 餐线设置新增用户范围必填复选 + 食谱订单餐品明细表份数调整
- `din-canteen-line.html` 餐线管理:
- 餐线设置/新增餐线弹窗「基础信息」下新增「用户范围」必填复选(用户类型多选:员工/会员用户/普通用户/工作人员/外部人员)
- 新增 checkbox-group/checkbox-item 样式,支持编辑回填与新增默认全选,保存时校验至少勾选一项
- `prod-clean-order.html` 净菜订单:
- 食谱快速创建订单弹窗「餐品明细」表:移除「包装规格」列(克数+单位下拉),改为「份数」列(固定单位「份」,输入框编辑份数)
- 提示语追加最大包装克重说明(默认4kg/份,超限按最大包装反算份数),两段说明之间空一行
- 份数存储 recipeSpecMap 改为 recipeServingMap,提交订单固定单位「份」并回退就餐人数作为份数
## [2026-08-24] feat: 任务中心页重构(统计卡片/分段Tab/动态筛选) + 仓库页新增净菜包柜
- `sup-task-center.html` 任务中心重构:
+33 -32
View File
@@ -368,14 +368,14 @@ 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:34%;">菜品名称</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>
</div>
<div class="recipe-tip">
<span class="recipe-tip__icon"></span>
<span class="recipe-tip__text"><b>系统智能核算提示:</b>订单提交后,系统将依据您填写的各餐次就餐人数自动核算采购量并生成净菜明细,处理过程约需 1~2 分钟。生成结果与最终订单金额将在 <b>订单列表</b> 中实时更新,请耐心等待并关注订单状态变化。</span>
<span class="recipe-tip__text"><b>系统智能核算提示:</b>订单提交后,系统将依据您填写的各餐次就餐人数自动核算采购量并生成净菜明细,处理过程约需 1~2 分钟。生成结果与最终订单金额将在 <b>订单列表</b> 中实时更新,请耐心等待并关注订单状态变化。<br><br>系统默认最大包装克重为4kg/份,若预测生重 ➗ 份数 > 4kg,则会默认按照最大包装克重反算份数。</span>
</div>
</div>
</div>
@@ -1357,8 +1357,8 @@ function onRecipeDateChange(){
/* ========== 餐品调料配置表(食谱快速创建专用) ========== */
/* recipeSeasoningMap: { 菜名: true|false },跨餐次重复菜品自动合并,状态在重渲时保留 */
var recipeSeasoningMap={};
/* recipeSpecMap: { 菜名: {baseGram, portion, unit} },餐品明细表包装规格编辑值,重渲时保留 */
var recipeSpecMap={};
/* recipeServingMap: { 菜名: 份数 },餐品明细表份数编辑值,重渲时保留 */
var recipeServingMap={};
function renderDishSeasoningTable(){
var section=document.getElementById('dishSeasoningSection');
var tbody=document.getElementById('dishSeasoningBody');
@@ -1366,15 +1366,18 @@ function renderDishSeasoningTable(){
var dateVal=document.getElementById('recipeDate').value;
var daily=dateVal?getDailyMenu(dateVal):null;
if(!daily){section.style.display='none';tbody.innerHTML='';return;}
/* 收集所有已勾选餐次的菜品,按菜名去重 */
/* 收集所有已勾选餐次的菜品,按菜名去重并累加就餐人数 */
var checkedMealIndices=[];
document.querySelectorAll('#dailyMenuBody .meal-cb:checked').forEach(function(cb){checkedMealIndices.push(parseInt(cb.value));});
var dishSet={};
var dishDinerMap={};
var orderedNames=[];
checkedMealIndices.forEach(function(mi){
var input=document.querySelector('#dailyMenuBody .meal-diner-input[data-meal-idx="'+mi+'"]');
var dinerCount=parseInt(input&&input.value.trim(),10)||0;
var dayDishes=daily.meals[mi]&&daily.meals[mi][daily.dayIdx]||[];
dayDishes.forEach(function(name){
if(!dishSet[name]){dishSet[name]=true;orderedNames.push(name);}
if(!(name in dishDinerMap)){dishDinerMap[name]=0;orderedNames.push(name);}
dishDinerMap[name]+=dinerCount;
});
});
if(orderedNames.length===0){
@@ -1388,17 +1391,22 @@ function renderDishSeasoningTable(){
/* 未配置过的菜品默认 false(不需要);已配置的保留 */
if(typeof recipeSeasoningMap[name]!=='boolean')recipeSeasoningMap[name]=false;
var ns=recipeSeasoningMap[name]===true;
/* 未编辑过包装规格的菜品,用兜底规格初始化 */
if(!recipeSpecMap[name]){
var sp=getDishSpecForRecipe(name);
recipeSpecMap[name]={baseGram:sp.baseGram,unit:sp.unit||'袋'};
/* 每份生重(克)取系统默认规格;就餐人数为该菜品跨餐次累加值 */
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 spec=recipeSpecMap[name];
var servings=recipeServingMap[name];
html+='<tr><td style="font-weight:500;">'+name+'</td>';
/* 包装规格:左 input 克数 + 右 select 包装单位 */
html+='<td><div class="spec-input-group">'
+'<input inputmode="numeric" placeholder="数" value="'+(spec.baseGram||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateRecipeSpecGram(\''+name.replace(/'/g,'\\\'')+'\',this.value)">'
+'<select onchange="updateRecipeSpecUnit(\''+name.replace(/'/g,'\\\'')+'\',this.value)">'+buildUnitOptions(spec.unit)+'</select>'
/* 份数:固定单位「份」,输入框编辑份数 */
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><div class="seasoning-radio-group">';
html+='<label><input type="radio" name="rs-'+encodeURIComponent(name)+'" '+(ns?'checked':'')+' onchange="updateRecipeSeasoning(\''+name.replace(/'/g,'\\\'')+'\',true)"> 是</label>';
@@ -1410,15 +1418,9 @@ function renderDishSeasoningTable(){
function updateRecipeSeasoning(name,val){
recipeSeasoningMap[name]=!!val;
}
/* 更新食谱餐品包装规格克数 */
function updateRecipeSpecGram(name,val){
if(!recipeSpecMap[name])recipeSpecMap[name]={baseGram:0,unit:'袋'};
recipeSpecMap[name].baseGram=parseInt(val,10)||0;
}
/* 更新食谱餐品包装规格单位 */
function updateRecipeSpecUnit(name,val){
if(!recipeSpecMap[name])recipeSpecMap[name]={baseGram:0,unit:'袋'};
recipeSpecMap[name].unit=val;
/* 更新食谱餐品数 */
function updateRecipeServing(name,val){
recipeServingMap[name]=parseInt(val,10)||0;
}
/* 校验所选餐次是否都已填写就餐人数,并返回缺失餐次名 */
@@ -1463,18 +1465,17 @@ function submitRecipeOrder(){
for(var name in dishMap){
var item=dishMap[name];
var sp=item.spec;
/* 优先使用餐品明细表编辑过的包装规格,未编辑回退默认规格 */
var edited=recipeSpecMap[name];
var baseGram=edited?edited.baseGram:sp.baseGram;
var unit=edited?(edited.unit||sp.unit):sp.unit;
var servings=item.dinerCount;
/* 包装规格固定为「份」,份数优先取餐品明细表编辑,未编辑回退就餐人数 */
var baseGram=sp.baseGram||500;
var unit='份';
var servings=recipeServingMap[name]||item.dinerCount;
newLines.push({
id:'L'+(Date.now()+di),
ncType:'cp',foodName:name,
spec:baseGram+'g/'+unit,
portion:sp.portion,baseGram:baseGram,unit:unit,
qty:servings,
weight:parseFloat((baseGram*servings/1000).toFixed(2)),
weight:parseFloat((baseGram*item.dinerCount/1000).toFixed(2)),
status:'生成中',
needSeasoning:recipeSeasoningMap[name]===true
});
@@ -1505,7 +1506,7 @@ function sanitizeIntegerInput(el){el.value=el.value.replace(/[^\d]/g,'');}
document.getElementById('recipeDeliveryTime').value=getDefaultDeliveryTime();
document.getElementById('dailyMenuSection').style.display='none';
recipeSeasoningMap={};
recipeSpecMap={};
recipeServingMap={};
onRecipeDateChange();
}
document.getElementById(id).classList.add('show');