feat: 批量充值抽离独立页面 + 餐品档案定价区改造 + 账户交互优化

- din-user-recharge.html 新增(从 din-user-mgmt.html 弹窗抽离,含筛选/候选表/配置区/汇总栏)
- din-user-mgmt.html 清理批量充值弹窗 CSS/JS/HTML,「批量充值」按钮改为跳转
- din-dish-add/all/own.html 定价区改造 + 成本核算按钮修复 + 营养指标精简
- din-user-account.html 支付方式精简为微信/现金/支付宝
- me-account.html 姓名行增加可编辑态
- index.html SITE_MAP 新增「批量充值」入口
- CHANGELOG 同步记录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-04 14:35:26 +08:00
co-authored by Claude Opus 4.7
parent 459cd40de2
commit 6a505f2d84
9 changed files with 1796 additions and 496 deletions
+1
View File
@@ -294,6 +294,7 @@ const SITE_MAP = [
{ file:'sup-biz-analysis.html', label:'加工经营分析' },
{ file:'sup-process-analysis.html', label:'加工分析' },
{ file:'din-user-mgmt.html', label:'用户管理' },
{ file:'din-user-recharge.html', label:'批量充值' },
{ file:'din-dish-all.html', label:'菜品总库' },
{ file:'din-dish-own.html', label:'自有餐品库' },
{ file:'din-dish-all-dialog.html', label:'菜品总库弹窗', dialog:true },
@@ -117,7 +117,10 @@
</div>
<!-- 信息列表 -->
<div class="info-group">
<div class="info-row"><span class="info-label">姓名</span><span class="info-value">张三</span></div>
<div class="info-row editable" onclick="alert('【交互注释】打开姓名修改弹窗')">
<span class="info-label">姓名</span>
<span class="info-value">张三<i class="edit-icon">✏️</i></span>
</div>
<div class="info-row editable" onclick="alert('【交互注释】打开手机号修改弹窗')">
<span class="info-label">手机号</span>
<span class="info-value">138****6789<i class="edit-icon">✏️</i></span>
+352 -51
View File
@@ -174,6 +174,55 @@ body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei
.step-readonly .ingredient-table td a.link{color:#bbb !important;cursor:not-allowed !important;pointer-events:none !important;}
.step-readonly .btn-text{color:#bbb !important;cursor:not-allowed !important;pointer-events:none !important;}
.step-readonly-tip{font-size:12px;color:#d46b08;background:#fff7e6;border:1px solid #ffd591;border-radius:4px;padding:4px 10px;margin-bottom:12px;display:inline-block;}
/* 营养指标只读提示 */
.nutri-readonly-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;}
/* 营养指标只读值块 */
.form-readonly{height:36px;line-height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 10px;background:#F5F5F5;color:var(--text-primary);font-size:var(--font-sm);font-weight:500;}
/* 营养指标四大项(简洁卡片) */
.nutri-hero-form{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:14px;}
.nutri-hero-form__cell{background:#FAFBFC;border:1px solid var(--border);border-radius:var(--radius-md);padding:12px 14px;}
.nutri-hero-form__label{font-size:var(--font-xs);color:var(--text-secondary);margin-bottom:6px;font-weight:500;}
.nutri-hero-form__value{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);line-height:1.2;}
.nutri-hero-form__unit{font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;margin-left:2px;}
/* 营养指标更多(折叠) */
.nutri-more-form{margin-top:8px;border-top:1px dashed #E8E8E8;padding-top:12px;}
.nutri-more-form__toggle{display:inline-flex;align-items:center;gap:4px;color:var(--primary);font-size:var(--font-sm);cursor:pointer;user-select:none;padding:6px 12px;border-radius:var(--radius-sm);background:var(--primary-light);transition:all .15s;}
.nutri-more-form__toggle:hover{background:#BAE7FF;}
.nutri-more-form__toggle .arrow{display:inline-block;transition:transform .2s;font-size:10px;}
.nutri-more-form.open .nutri-more-form__toggle .arrow{transform:rotate(180deg);}
.nutri-more-form__body{display:none;margin-top:12px;}
.nutri-more-form.open .nutri-more-form__body{display:block;}
/* 食材成本输入框 + 内嵌成本核算按钮 */
.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-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);}
/* 宣传视频列表(多视频) */
.promo-video-list{display:flex;flex-direction:column;gap:10px;margin-top:10px;}
.promo-video-item{display:flex;align-items:center;gap:12px;background:#FAFBFC;border:1px solid #F0F0F0;border-radius:var(--radius-md);padding:10px 14px;}
.promo-video-item__thumb{width:64px;height:48px;border-radius:var(--radius-sm);background:#E8E8E8;display:flex;align-items:center;justify-content:center;font-size:24px;color:#999;flex-shrink:0;}
.promo-video-item__info{flex:1;min-width:0;}
.promo-video-item__name{font-size:var(--font-sm);font-weight:500;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.promo-video-item__meta{font-size:var(--font-xs);color:var(--text-placeholder);margin-top:2px;}
.promo-video-item__actions{display:flex;align-items:center;gap:10px;flex-shrink:0;}
</style>
</head>
<body>
@@ -343,7 +392,7 @@ var INNOV_MAP = {
{name:'藜麦',netType:'藜麦粒',amount:60,pct:'30%',type:'主材',allergen:'无',note:'低GI'},
{name:'大米',netType:'熟米饭',amount:60,pct:'30%',type:'辅材',allergen:'无',note:'基底'}
],
cook:{mainMethod:'蒸',subMethod:'无',heat:'大火→中火',duration:30,oil:0,salt:3,temp:100,output:200,steps:4,desc:''},
cook:{mainMethod:'蒸',subMethod:'无',heat:'大火→中火',duration:30,temp:100,steps:4,desc:''},
nutri:{calorie:342,protein:12.8,carb:58.4,fat:5.2,vitC:0,mineral:38,fiber:9.6,sodium:210},
nutriGrade:'达标',
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:2,smell:3.8,flavorScore:3.6,flavorGrade:'good'},
@@ -359,7 +408,7 @@ var INNOV_MAP = {
{name:'胡萝卜',netType:'胡萝卜丝',amount:40,pct:'13%',type:'辅材',allergen:'无',note:'维C协同'},
{name:'食用油',netType:'净油',amount:5,pct:'2%',type:'调料',allergen:'无',note:'少量'}
],
cook:{mainMethod:'煮',subMethod:'无',heat:'大火→小火',duration:15,oil:5,salt:2,temp:100,output:300,steps:3,desc:''},
cook:{mainMethod:'煮',subMethod:'无',heat:'大火→小火',duration:15,temp:100,steps:3,desc:''},
nutri:{calorie:68,protein:5.4,carb:6.2,fat:2.8,vitC:32,mineral:86,fiber:2.4,sodium:320},
nutriGrade:'达标',
taste:{sour:1,sweet:1,bitter:1,spicy:1,salty:3,smell:3.5,flavorScore:3.4,flavorGrade:'average'},
@@ -389,13 +438,38 @@ var STEP_HTML = [
<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><option>盐</option></select></div>
<div class="form-group"><label class="form-label">过敏原</label><input class="form-input" placeholder="无则填无"></div>
<div class="form-group"><label class="form-label">餐品标签</label><select class="form-select"><option>请选择</option><option>低脂</option><option>高蛋白</option><option>素食</option><option>低卡</option><option>高纤维</option><option>低GI</option><option>高糖</option><option>无麸质</option></select></div>
<div class="form-group"><label class="form-label">称重基础价(元/100g)<span class="required">*</span></label><input class="form-input" type="number" step="0.01" placeholder="请输入称重基础价"></div>
<div class="form-group"><label class="form-label">称重会员价(元/100g)<span class="required">*</span></label><input class="form-input" type="number" step="0.01" placeholder="请输入称重会员价"></div>
<div class="form-group"><label class="form-label">上架状态<span class="required">*</span></label><select class="form-select"><option>上架</option><option>下架</option></select></div>
</div>
<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>规格</th><th>单位重量(g)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>默认</th><th style="width:60px;">操作</th></tr></thead>
@@ -423,44 +497,54 @@ var STEP_HTML = [
<div class="upload-area__text">点击或拖拽上传制作视频</div>
<div class="upload-area__hint">支持 MP4 / MOV,不超过 500MB</div>
</div>
<div class="form-subtitle">🎬 宣传视频</div>
<div class="promo-head">
<div class="promo-toggle" onclick="togglePromoVideo(this)">
<span class="promo-toggle__switch"></span>
<span class="promo-toggle__label">未启用</span>
</div>
<span style="font-size:var(--font-xs);color:var(--text-placeholder);">控制宣传视频在员工端是否可见</span>
</div>
<div class="upload-area" onclick="triggerPromoVideoUpload()">
<div class="upload-area__icon">⇧</div>
<div class="upload-area__text">点击或拖拽上传宣传视频</div>
<div class="upload-area__hint">支持 MP4 / MOV,不超过 500MB</div>
</div>`,
<div class="form-subtitle">🎬 宣传视频 <span style="font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;">支持上传多个,每个可独立启用</span></div>
<div class="promo-video-list" id="promoVideoList"></div>
<button type="button" class="dynamic-row-add" onclick="addPromoVideoRow()"> 添加宣传视频</button>
<div class="upload-hint">已上传的宣传视频将独立显示在员工端,每个可单独启用/停用</div>`,
// Step 1: 餐品构成(含熟重 + 营养指标)
// Step 1: 餐品构成(含熟重 + 营养指标自动计算
`<div class="form-card__title">🧩 餐品构成</div>
<div class="form-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:8px;">
<div class="form-group"><label class="form-label">熟重(g)<span class="required">*</span></label><input class="form-input" type="number" min="0" step="0.1" placeholder="请输入熟重"></div>
<div class="form-group"><label class="form-label">熟重(g)<span class="required">*</span></label><input class="form-input" id="cookedWeightInput" type="number" min="0" step="0.1" placeholder="请输入熟重" oninput="recalcAddNutri()"></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<span style="font-size:var(--font-sm);font-weight:600;">食材清单<span class="required">*</span></span>
<button class="btn btn-primary" style="height:30px;font-size:var(--font-xs);" onclick="openIngredientDialog()"> 新增食材</button>
</div>
<table class="ingredient-table">
<table class="ingredient-table" id="ingredientTable">
<thead><tr><th>食材名称</th><th>净菜类型</th><th>用量(g/份)</th><th>食材类型</th><th>过敏原</th><th>备注</th><th>操作</th></tr></thead>
<tbody><tr><td colspan="7" style="text-align:center;color:var(--text-placeholder);padding:32px;">暂无食材,请点击"新增食材"添加</td></tr></tbody>
<tbody id="ingredientTbody"><tr><td colspan="7" style="text-align:center;color:var(--text-placeholder);padding:32px;">暂无食材,请点击"新增食材"添加</td></tr></tbody>
</table>
<div class="form-subtitle">📊 营养指标</div>
<div class="form-grid">
<div class="form-group"><label class="form-label">热量 (kcal/100g)<span class="required">*</span></label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">蛋白质 (g)<span class="required">*</span></label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">碳水化合物 (g)<span class="required">*</span></label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">脂肪 (g)<span class="required">*</span></label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">维生素C (mg)</label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">矿物质 (mg)</label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">膳食纤维 (g)</label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">钠 (mg)</label><input class="form-input" type="number" placeholder="0.0"></div>
<div class="form-group"><label class="form-label">营养等级</label><select class="form-select"><option value="">请选择</option><option value="达标">达标</option><option value="偏差">偏差</option><option value="未达标">未达标</option></select></div>
<div class="form-subtitle">📊 营养指标 <span style="font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;">(按 100g 熟重计)</span></div>
<div class="nutri-readonly-tip">ⓘ 营养指标根据食材清单自动计算,不可手动修改;请先录入食材后系统会自动汇总</div>
<div class="nutri-hero-form">
<div class="nutri-hero-form__cell"><div class="nutri-hero-form__label">🔥 热量</div><div class="nutri-hero-form__value"><span id="nutriCalorie">0.0</span><span class="nutri-hero-form__unit">kcal</span></div></div>
<div class="nutri-hero-form__cell"><div class="nutri-hero-form__label">💪 蛋白质</div><div class="nutri-hero-form__value"><span id="nutriProtein">0.0</span><span class="nutri-hero-form__unit">g</span></div></div>
<div class="nutri-hero-form__cell"><div class="nutri-hero-form__label">🌾 碳水化合物</div><div class="nutri-hero-form__value"><span id="nutriCarb">0.0</span><span class="nutri-hero-form__unit">g</span></div></div>
<div class="nutri-hero-form__cell"><div class="nutri-hero-form__label">🥑 脂肪</div><div class="nutri-hero-form__value"><span id="nutriFat">0.0</span><span class="nutri-hero-form__unit">g</span></div></div>
</div>
<div class="nutri-more-form" id="nutriMoreForm">
<div class="nutri-more-form__toggle" onclick="document.getElementById('nutriMoreForm').classList.toggle('open')">查看更多营养指标 <span class="arrow">▼</span></div>
<div class="nutri-more-form__body">
<div class="form-grid">
<div class="form-group"><label class="form-label">维生素C (mg)</label><div class="form-readonly" id="nutriVitC">0.0</div></div>
<div class="form-group"><label class="form-label">矿物质 (mg)</label><div class="form-readonly" id="nutriMineral">0.0</div></div>
<div class="form-group"><label class="form-label">膳食纤维 (g)</label><div class="form-readonly" id="nutriFiber">0.0</div></div>
<div class="form-group"><label class="form-label">钠 (mg)</label><div class="form-readonly" id="nutriSodium">0.0</div></div>
<div class="form-group"><label class="form-label">钙 (mg)</label><div class="form-readonly" id="nutriCalcium">0.0</div></div>
<div class="form-group"><label class="form-label">铁 (mg)</label><div class="form-readonly" id="nutriIron">0.0</div></div>
<div class="form-group"><label class="form-label">锌 (mg)</label><div class="form-readonly" id="nutriZinc">0.0</div></div>
<div class="form-group"><label class="form-label">钾 (mg)</label><div class="form-readonly" id="nutriPotassium">0.0</div></div>
<div class="form-group"><label class="form-label">胆固醇 (mg)</label><div class="form-readonly" id="nutriCholesterol">0.0</div></div>
<div class="form-group"><label class="form-label">维生素A (μg)</label><div class="form-readonly" id="nutriVitA">0.0</div></div>
<div class="form-group"><label class="form-label">维生素E (mg)</label><div class="form-readonly" id="nutriVitE">0.0</div></div>
<div class="form-group"><label class="form-label">维生素 B1 (mg)</label><div class="form-readonly" id="nutriVitB1">0.0</div></div>
<div class="form-group"><label class="form-label">维生素 B2 (mg)</label><div class="form-readonly" id="nutriVitB2">0.0</div></div>
<div class="form-group"><label class="form-label">镁 (mg)</label><div class="form-readonly" id="nutriMagnesium">0.0</div></div>
<div class="form-group"><label class="form-label">磷 (mg)</label><div class="form-readonly" id="nutriPhosphorus">0.0</div></div>
<div class="form-group"><label class="form-label">硒 (μg)</label><div class="form-readonly" id="nutriSelenium">0.0</div></div>
</div>
</div>
</div>`,
// Step 2: 烹饪工艺
@@ -470,12 +554,10 @@ var STEP_HTML = [
<div class="form-group"><label class="form-label">辅烹饪法</label><select class="form-select"><option>无</option><option>煎</option><option>炒</option><option>蒸</option><option>炸</option></select></div>
<div class="form-group"><label class="form-label">火候</label><input class="form-input" placeholder="如:大火→中火"></div>
<div class="form-group"><label class="form-label">烹饪时长(min)</label><input class="form-input" type="number" placeholder="请输入时长"></div>
<div class="form-group"><label class="form-label">油用量(g)</label><input class="form-input" type="number" placeholder="请输入油用量"></div>
<div class="form-group"><label class="form-label">盐用量(g)</label><input class="form-input" type="number" placeholder="请输入盐用量"></div>
<div class="form-group"><label class="form-label">烹饪温度(℃)</label><input class="form-input" placeholder="如:100→80"></div>
<div class="form-group"><label class="form-label">出品量(g)</label><input class="form-input" type="number" placeholder="请输入出品量"></div>
<div class="form-group"><label class="form-label">工艺步骤数</label><input class="form-input" type="number" placeholder="请输入步骤数"></div>
</div>`,
</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: 味道档案
`<div class="form-card__title">👅 味道档案</div>
@@ -518,7 +600,14 @@ var STEP_HTML = [
function renderStep(){
document.getElementById('stepContent').innerHTML = STEP_HTML[currentStep];
updateStepUI();
if(currentStep===0) renderCrowdRows();
if(currentStep===0){
renderCrowdRows();
renderPromoVideoList();
/* Step 0 阶段无需联动营养(食材在 Step 1) */
}
if(currentStep===1){
setTimeout(function(){ recalcAddNutri(); }, 0);
}
if(pageMode==='edit' || pageMode==='convert') fillEditData();
if(pageMode==='convert') lockConvertStep();
}
@@ -589,12 +678,203 @@ function toggleCleanSale(el){
var label = el.querySelector('.sale-toggle__label');
if(label) label.textContent = active ? '上架' : '下架';
}
/* 宣传视频启用/未启用切换 */
/* 宣传视频启用/未启用切换(每个视频独立) */
function togglePromoVideo(el){
var active = el.classList.toggle('active');
var label = el.querySelector('.promo-toggle__label');
if(label) label.textContent = active ? '已启用' : '未启用';
}
/* 宣传视频列表(多视频上传 + 独立启用) */
var promoVideoSeq = 0;
var promoVideoList = [];
function renderPromoVideoList(){
var list = document.getElementById('promoVideoList');
if(!list) return;
if(promoVideoList.length === 0){
list.innerHTML = '<div style="text-align:center;padding:16px;color:var(--text-placeholder);font-size:var(--font-sm);background:#FAFBFC;border:1px dashed var(--border);border-radius:var(--radius-md);">暂无宣传视频,点击下方按钮添加</div>';
return;
}
list.innerHTML = promoVideoList.map(function(v,i){
return '<div class="promo-video-item">'
+ '<div class="promo-video-item__thumb">🎬</div>'
+ '<div class="promo-video-item__info"><div class="promo-video-item__name">'+(v.title||('宣传视频 '+(i+1)))+'</div><div class="promo-video-item__meta">'+(v.duration||'—')+' · '+(v.size||'—')+' · '+(v.uploaded?'已上传':'待上传')+'</div></div>'
+ '<div class="promo-video-item__actions">'
+ '<span class="link" style="font-size:var(--font-xs);" onclick="triggerPromoVideoUpload('+i+')">'+(v.uploaded?'重新上传':'上传视频')+'</span>'
+ '<div class="promo-toggle'+(v.enabled?' active':'')+'" onclick="toggleAddPromoVideo(this,'+i+')"><span class="promo-toggle__switch"></span><span class="promo-toggle__label">'+(v.enabled?'已启用':'未启用')+'</span></div>'
+ '<button type="button" class="btn-text btn-text--danger" onclick="removePromoVideoRow('+i+')">删除</button>'
+ '</div></div>';
}).join('');
}
function addPromoVideoRow(){
promoVideoSeq++;
promoVideoList.push({seq:promoVideoSeq, title:'宣传视频 '+promoVideoSeq, duration:'—', size:'—', uploaded:false, enabled:true});
renderPromoVideoList();
}
function removePromoVideoRow(idx){
if(!confirm('确定删除该宣传视频?')) return;
promoVideoList.splice(idx,1);
renderPromoVideoList();
}
function toggleAddPromoVideo(el, idx){
var v = promoVideoList[idx];
if(!v) return;
v.enabled = !v.enabled;
var label = el.querySelector('.promo-toggle__label');
if(label) label.textContent = v.enabled ? '已启用' : '未启用';
el.classList.toggle('active', v.enabled);
}
function triggerPromoVideoUpload(idx){
if(typeof idx === 'number'){
alert('选择第 '+(idx+1)+' 个宣传视频文件(原型演示)');
promoVideoList[idx].uploaded = true;
promoVideoList[idx].duration = '1分30秒';
promoVideoList[idx].size = '48MB';
renderPromoVideoList();
} else {
alert('选择宣传视频文件(原型演示)');
}
}
/* 餐品定价:成本核算 + 应用到称重基础价 */
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');
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>';
});
}
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);
}
}
/* 小问号弹窗显示/切换 */
function toggleCostDetailPop(e){
if(e) e.stopPropagation();
var pop = document.getElementById('costDetailPop');
if(pop) pop.classList.toggle('open');
}
/* 应用成本价到称重基础价(叠加利润率) */
function applyCostToBasePrice(){
var cost = parseFloat(document.getElementById('costPriceDisplay') && document.getElementById('costPriceDisplay').textContent) || 0;
if(cost <= 0){ return; }
var profitInput = document.getElementById('profitRateInput');
var rate = profitInput ? (parseFloat(profitInput.value)||0) : 0;
var base = cost * (1 + rate/100);
var baseInput = document.getElementById('basePriceInput');
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(){
/* 简化原型:根据食材数量生成示例值 */
var tbody = document.getElementById('ingredientTbody');
var ingCount = 0;
if(tbody){
var emptyRow = tbody.querySelector('td[colspan]');
if(!emptyRow) ingCount = tbody.querySelectorAll('tr').length;
}
var vals = {
calorie: ingCount * 75,
protein: ingCount * 4.5,
carb: ingCount * 8,
fat: ingCount * 5,
vitC: ingCount * 5,
mineral: ingCount * 12,
fiber: ingCount * 0.8,
sodium: ingCount * 95,
calcium: ingCount * 8,
iron: ingCount * 0.8,
zinc: ingCount * 0.5,
potassium: ingCount * 60,
cholesterol: ingCount * 22,
vitA: ingCount * 18,
vitE: ingCount * 0.18,
vitB1: ingCount * 0.06,
vitB2: ingCount * 0.05,
magnesium: ingCount * 8,
phosphorus: ingCount * 50,
selenium: ingCount * 5
};
var map = {calorie:'nutriCalorie',protein:'nutriProtein',carb:'nutriCarb',fat:'nutriFat',vitC:'nutriVitC',mineral:'nutriMineral',fiber:'nutriFiber',sodium:'nutriSodium',calcium:'nutriCalcium',iron:'nutriIron',zinc:'nutriZinc',potassium:'nutriPotassium',cholesterol:'nutriCholesterol',vitA:'nutriVitA',vitE:'nutriVitE',vitB1:'nutriVitB1',vitB2:'nutriVitB2',magnesium:'nutriMagnesium',phosphorus:'nutriPhosphorus',selenium:'nutriSelenium'};
Object.keys(map).forEach(function(k){
var el = document.getElementById(map[k]);
if(el) el.textContent = vals[k].toFixed(1);
});
/* 同步定价区食材成本 */
var ingredientCost = (ingCount * 0.9).toFixed(2);
var costInput = document.getElementById('ingredientCostInput');
if(costInput) costInput.value = ingredientCost;
}
/* 食材添加后重新计算营养指标 */
function recalcAfterIngredientChange(){
if(currentStep === 1){
recalcAddNutri();
}
}
function cancelSpec(btn){btn.closest('tr').remove();}
function toggleDefault(el){
var tbody=document.getElementById('specBody');
@@ -674,6 +954,8 @@ function confirmAddIngredient(){
/* 重置表单 */
resetIngredientForm();
closeIngredientDialog();
/* 重新计算营养指标和定价 */
recalcAfterIngredientChange();
}
/* 图片上传 */
@@ -745,12 +1027,14 @@ function saveCrowdRows(){
var EDIT_DATA = {
name:'红烧肉', category:'荤菜', method:'炖', meal:'午/晚', pyramid:'畜禽肉蛋水产',
allergen:'无', tag:'高蛋白', status:'上架',
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],
specs:[{spec:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{spec:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false,isDefault:false},{spec:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false,isDefault:false}],
cookedWeight:200,
composition:[{name:'五花肉',netType:'红烧块',amount:150,pct:'60%',type:'主材',allergen:'无',note:'—'},{name:'冰糖',netType:'糖浆',amount:20,pct:'8%',type:'调料',allergen:'无',note:'上色'},{name:'酱油',netType:'调味汁',amount:15,pct:'6%',type:'调料',allergen:'含大豆',note:'调味'},{name:'料酒',netType:'料酒汁',amount:10,pct:'4%',type:'调料',allergen:'无',note:'去腥'}],
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,oil:15,salt:3,temp:100,output:200,steps:6,desc:'1.五花肉切块焯水;2.锅中煎至金黄;3.加冰糖炒色;4.加酱油料酒;5.加水小火炖40分钟;6.大火收汁'},
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:2.1,mineral:45,fiber:1.8,sodium:486},
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,temp:100,steps:6,desc:'1.五花肉切块焯水;2.锅中煎至金黄;3.加冰糖炒色;4.加酱油料酒;5.加水小火炖40分钟;6.大火收汁'},
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:2.1,mineral:45,fiber:1.8,sodium:486,calcium:18,iron:2.4,zinc:1.8,potassium:280,cholesterol:88,vitA:14,vitE:0.6,vitB1:0.22,vitB2:0.12,magnesium:20,phosphorus:160,selenium:18},
taste:{sour:1,sweet:3,bitter:1,spicy:1,salty:4,smell:4.2,flavorScore:3.8,flavorGrade:'good'},
color:{mainColor:'红褐色',uniformity:4,gloss:4,freshness:4,appearance:4,colorScore:4.0,colorGrade:'good'},
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'good'}
@@ -813,9 +1097,15 @@ function fillEditData(){
var d = EDIT_DATA;
if(currentStep===0){
// 基本档案
// 基本档案 - 输入顺序:餐品名称(0), 过敏原(1), [定价: basePrice(2), profitRate(3), ingredientCost(4)]
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);
setSelectByText(selects[2], d.pyramid);
@@ -838,11 +1128,18 @@ function fillEditData(){
specBody.appendChild(tr);
});
}
// 预填宣传视频
if(d.promoVideos && d.promoVideos.length>0){
promoVideoList = d.promoVideos.map(function(v, i){
promoVideoSeq = Math.max(promoVideoSeq, i+1);
return {seq:i+1, title:v.title||('宣传视频 '+(i+1)), duration:v.duration||'—', size:v.size||'—', uploaded:true, enabled:!!v.enabled};
});
renderPromoVideoList();
}
} else if(currentStep===1){
// 餐品构成 - 预填熟重 + 食材表格 + 营养指标
// inputs[0] = 熟重;inputs[1..8] = 营养指标 8 项
// 餐品构成 - 熟重 + 食材表格 + 营养指标(只读自动计算)
if(inputs[0] && d.cookedWeight!==undefined) inputs[0].value = d.cookedWeight;
var tbody = container.querySelector('table tbody');
var tbody = container.querySelector('#ingredientTable tbody');
if(tbody){
tbody.innerHTML = '';
d.composition.forEach(function(c){
@@ -853,15 +1150,19 @@ function fillEditData(){
tbody.appendChild(tr);
});
}
// 营养指标inputs[1..8]
var n = d.nutri;
var nvals = [n.calorie, n.protein, n.carb, n.fat, n.vitC, n.mineral, n.fiber, n.sodium];
for(var k=0;k<nvals.length;k++){ if(inputs[1+k] && nvals[k]!==undefined) inputs[1+k].value = nvals[k]; }
if(selects[0] && INNOV && INNOV.nutriGrade) setSelectByText(selects[0], INNOV.nutriGrade);
// 营养指标 - 通过 ID 设置只读文本(4 大项 + 16 项更多
if(d.nutri){
var n = d.nutri;
var idMap = {calorie:'nutriCalorie',protein:'nutriProtein',carb:'nutriCarb',fat:'nutriFat',vitC:'nutriVitC',mineral:'nutriMineral',fiber:'nutriFiber',sodium:'nutriSodium',calcium:'nutriCalcium',iron:'nutriIron',zinc:'nutriZinc',potassium:'nutriPotassium',cholesterol:'nutriCholesterol',vitA:'nutriVitA',vitE:'nutriVitE',vitB1:'nutriVitB1',vitB2:'nutriVitB2',magnesium:'nutriMagnesium',phosphorus:'nutriPhosphorus',selenium:'nutriSelenium'};
Object.keys(idMap).forEach(function(k){
var el = document.getElementById(idMap[k]);
if(el && n[k]!==undefined) el.textContent = n[k];
});
}
} else if(currentStep===2){
// 烹饪工艺
// 烹饪工艺 - 字段顺序:主烹饪法、辅烹饪法、火候、时长、温度、步骤数
var c = d.cook;
var vals = [c.mainMethod, c.subMethod, c.heat, c.duration, c.oil, c.salt, c.temp, c.output, c.steps];
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||'';
+275 -47
View File
@@ -176,6 +176,59 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
.status-toggle.active .status-toggle__switch::after{transform:translateX(16px);}
.status-toggle__label{font-size:var(--font-xs);color:var(--text-placeholder);}
.status-toggle.active .status-toggle__label{color:var(--success);}
/* 宣传视频启用开关(蓝色) */
.promo-toggle{display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none;}
.promo-toggle__switch{width:32px;height:18px;background:#D9D9D9;border-radius:9px;position:relative;transition:background .2s;flex-shrink:0;}
.promo-toggle__switch::after{content:'';position:absolute;left:2px;top:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 2px rgba(0,0,0,0.15);}
.promo-toggle.active .promo-toggle__switch{background:var(--primary);}
.promo-toggle.active .promo-toggle__switch::after{transform:translateX(14px);}
.promo-toggle__label{font-size:11px;color:var(--text-placeholder);}
.promo-toggle.active .promo-toggle__label{color:var(--primary);font-weight:500;}
/* 营养指标 - 四大项简洁卡片 */
.nutri-hero{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:14px;}
.nutri-hero__cell{background:#FAFBFC;border:1px solid var(--border);border-radius:var(--radius-md);padding:12px 14px;}
.nutri-hero__label{font-size:var(--font-xs);color:var(--text-secondary);margin-bottom:6px;}
.nutri-hero__value{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);line-height:1.2;}
.nutri-hero__unit{font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;margin-left:2px;}
/* 营养指标 - 折叠区 */
.nutri-more{margin-top:8px;border-top:1px dashed #E8E8E8;padding-top:12px;}
.nutri-more__toggle{display:inline-flex;align-items:center;gap:4px;color:var(--primary);font-size:var(--font-sm);cursor:pointer;user-select:none;padding:4px 10px;border-radius:var(--radius-sm);background:var(--primary-light);transition:all .15s;}
.nutri-more__toggle:hover{background:#BAE7FF;}
.nutri-more__toggle .arrow{display:inline-block;transition:transform .2s;font-size:10px;}
.nutri-more.open .nutri-more__toggle .arrow{transform:rotate(180deg);}
.nutri-more__body{display:none;margin-top:12px;}
.nutri-more.open .nutri-more__body{display:block;}
/* 营养指标 - 自动计算提示 */
.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);}
/* 宣传视频多卡片 */
.promo-video-card{background:#FAFBFC;border:1px solid #F0F0F0;border-radius:var(--radius-md);overflow:hidden;display:flex;flex-direction:column;}
.promo-video-card__thumb{height:100px;background:#E8E8E8;display:flex;align-items:center;justify-content:center;font-size:32px;color:#999;position:relative;}
.promo-video-card__badge{position:absolute;top:6px;left:6px;background:rgba(0,0,0,0.55);color:#fff;font-size:10px;padding:2px 6px;border-radius:3px;}
.promo-video-card__info{padding:8px 12px;flex:1;}
.promo-video-card__title{font-size:var(--font-sm);font-weight:500;margin-bottom:2px;}
.promo-video-card__meta{font-size:var(--font-xs);color:var(--text-placeholder);}
.promo-video-card__footer{padding:6px 12px;border-top:1px solid #F0F0F0;display:flex;align-items:center;justify-content:space-between;background:#fff;}
</style>
</head>
<body>
@@ -325,42 +378,57 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<script>
// 餐品数据
const DISHES = [
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{name:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{name:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false},{name:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',weightBasePrice:'6.00',weightVipPrice:'5.40',promoVideoEnabled:true,crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{name:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{name:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false},{name:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
composition:[{name:'猪五花肉',netType:'红烧块',amount:160,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'生姜',netType:'姜片',amount:5,pct:'2.5%',state:'净菜',type:'辅材',allergen:'无',note:'—'},{name:'生抽',netType:'调味汁',amount:15,pct:'7.5%',state:'原料',type:'调料',allergen:'含麸质',note:'酱油'},{name:'冰糖',netType:'糖浆',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'上色提味'},{name:'料酒',netType:'料酒汁',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'去腥'}],
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:0.8,mineral:42,fiber:0.3,sodium:486,nutriGrade:'偏差',note:'铁2.4mg、锌1.8mg'},
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,oil:15,salt:3,temp:'100→80',output:200,steps:6},
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:0.8,mineral:42,fiber:0.3,sodium:486,calcium:18,iron:2.4,zinc:1.8,potassium:280,cholesterol:88,vitA:14,vitE:0.6,vitB1:0.22,vitB2:0.12,magnesium:20,phosphorus:160,selenium:18,copper:0.12,manganese:0.05,nutriGrade:'偏差',note:'铁2.4mg、锌1.8mg'},
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,temp:'100→80',steps:6},
taste:{sour:1,sweet:3,bitter:1,spicy:1,salty:4,smell:4.2,flavorScore:3.8,flavorGrade:'良好'},
color:{mainColor:'红褐色',uniformity:4,gloss:4,freshness:4,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'良好'}
},
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{name:'标准份',weight:150,price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',weightBasePrice:'3.00',weightVipPrice:'2.70',promoVideoEnabled:false,crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{name:'标准份',weight:150,price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
pricing:{basePrice:'3.00',ingredientCost:1.80,per100gCost:1.20,profitRate:150,suggestedPrice:'3.00'},
promoVideos:[{title:'清炒时蔬宣传片',duration:'45秒',size:'18MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
composition:[{name:'时令青菜',netType:'青菜段',amount:120,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'蒜末',netType:'蒜末',amount:5,pct:'3.3%',state:'净菜',type:'调料',allergen:'无',note:'—'},{name:'食用油',netType:'净油',amount:8,pct:'5.3%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'1.3%',state:'原料',type:'调料',allergen:'无',note:'—'}],
nutri:{calorie:68,protein:2.1,carb:8.6,fat:3.2,vitC:28.5,mineral:86,fiber:2.4,sodium:186,nutriGrade:'达标',note:'铁1.2mg、钙45mg'},
cook:{mainMethod:'炒',subMethod:'—',heat:'大火',duration:5,oil:8,salt:2,temp:'200',output:150,steps:4},
nutri:{calorie:68,protein:2.1,carb:8.6,fat:3.2,vitC:28.5,mineral:86,fiber:2.4,sodium:186,calcium:45,iron:1.2,zinc:0.4,potassium:280,cholesterol:0,vitA:268,vitE:0.9,vitB1:0.04,vitB2:0.06,magnesium:24,phosphorus:42,selenium:1.2,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'铁1.2mg、钙45mg'},
cook:{mainMethod:'炒',subMethod:'—',heat:'大火',duration:5,temp:'200',steps:4},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:2,smell:3.5,flavorScore:3.2,flavorGrade:'一般'},
color:{mainColor:'翠绿色',uniformity:5,gloss:4,freshness:5,appearance:4,colorScore:4.5,colorGrade:'优秀'},
texture:{hardness:3,sticky:1,crispy:4,smooth:3,elastic:2,textureScore:3.4,textureGrade:'良好'}
},
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{name:'标准份',weight:300,price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',weightBasePrice:'1.67',weightVipPrice:'1.50',promoVideoEnabled:true,crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{name:'标准份',weight:300,price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
pricing:{basePrice:'1.67',ingredientCost:1.20,per100gCost:0.40,profitRate:150,suggestedPrice:'1.00'},
promoVideos:[{title:'番茄鸡蛋汤宣传片',duration:'1分钟',size:'24MB',enabled:true},{title:'家常做法教程',duration:'3分钟',size:'80MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
composition:[{name:'番茄',netType:'番茄块',amount:150,pct:'50%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'鸡蛋',netType:'鸡蛋液',amount:60,pct:'20%',state:'原料',type:'主材',allergen:'含蛋类',note:'—'},{name:'食用油',netType:'净油',amount:5,pct:'1.7%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'0.7%',state:'原料',type:'调料',allergen:'无',note:'—'}],
nutri:{calorie:45,protein:3.8,carb:4.2,fat:2.1,vitC:18.2,mineral:32,fiber:1.1,sodium:280,nutriGrade:'达标',note:'铁0.8mg、钾210mg'},
cook:{mainMethod:'煮',subMethod:'炒',heat:'中火',duration:12,oil:5,salt:2,temp:'100',output:300,steps:5},
nutri:{calorie:45,protein:3.8,carb:4.2,fat:2.1,vitC:18.2,mineral:32,fiber:1.1,sodium:280,calcium:18,iron:0.8,zinc:0.4,potassium:210,cholesterol:110,vitA:62,vitE:0.5,vitB1:0.04,vitB2:0.12,magnesium:12,phosphorus:80,selenium:8,copper:0.05,manganese:0.06,nutriGrade:'达标',note:'铁0.8mg、钾210mg'},
cook:{mainMethod:'煮',subMethod:'炒',heat:'中火',duration:12,temp:'100',steps:5},
taste:{sour:3,sweet:2,bitter:1,spicy:1,salty:2,smell:3.8,flavorScore:3.5,flavorGrade:'良好'},
color:{mainColor:'橙红色',uniformity:4,gloss:3,freshness:5,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:1,sticky:2,crispy:1,smooth:5,elastic:2,textureScore:3.8,textureGrade:'良好'}
},
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{name:'标准份',weight:200,price:3,cleanPrice:2,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:2,cleanPrice:1.5,cleanSale:false},{name:'大份',weight:300,price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',weightBasePrice:'1.50',weightVipPrice:'1.35',promoVideoEnabled:false,crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{name:'标准份',weight:200,price:3,cleanPrice:2,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:2,cleanPrice:1.5,cleanSale:false},{name:'大份',weight:300,price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
pricing:{basePrice:'1.50',ingredientCost:1.00,per100gCost:0.50,profitRate:150,suggestedPrice:'1.25'},
promoVideos:[],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
composition:[{name:'大米',netType:'熟米饭',amount:100,pct:'100%',state:'原料',type:'主材',allergen:'无',note:'—'}],
nutri:{calorie:232,protein:4.8,carb:51.2,fat:0.6,vitC:0,mineral:12,fiber:0.4,sodium:3,nutriGrade:'达标',note:'—'},
cook:{mainMethod:'蒸',subMethod:'—',heat:'大火→中火',duration:30,oil:0,salt:0,temp:'100',output:200,steps:2},
nutri:{calorie:232,protein:4.8,carb:51.2,fat:0.6,vitC:0,mineral:12,fiber:0.4,sodium:3,calcium:10,iron:0.5,zinc:0.8,potassium:43,cholesterol:0,vitA:0,vitE:0.1,vitB1:0.02,vitB2:0.02,magnesium:12,phosphorus:43,selenium:1.5,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'—'},
cook:{mainMethod:'蒸',subMethod:'—',heat:'大火→中火',duration:30,temp:'100',steps:2},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:1,smell:3.0,flavorScore:2.8,flavorGrade:'一般'},
color:{mainColor:'乳白色',uniformity:5,gloss:4,freshness:4,appearance:4,colorScore:4.2,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:3,elastic:2,textureScore:3.2,textureGrade:'一般'}
},
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{name:'标准份',weight:180,price:14,cleanPrice:10,cleanSale:false,isDefault:true},{name:'大份',weight:280,price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',weightBasePrice:'7.78',weightVipPrice:'7.00',promoVideoEnabled:true,crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{name:'标准份',weight:180,price:14,cleanPrice:10,cleanSale:false,isDefault:true},{name:'大份',weight:280,price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
pricing:{basePrice:'7.78',ingredientCost:5.20,per100gCost:2.89,profitRate:170,suggestedPrice:'7.80'},
promoVideos:[{title:'糖醋里脊宣传片',duration:'2分钟',size:'58MB',enabled:true},{title:'正宗做法揭秘',duration:'5分钟',size:'150MB',enabled:true},{title:'出品摆盘展示',duration:'30秒',size:'12MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
composition:[{name:'猪里脊肉',netType:'里脊丝',amount:120,pct:'66.7%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'淀粉',netType:'淀粉糊',amount:20,pct:'11.1%',state:'原料',type:'辅材',allergen:'无',note:'挂糊'},{name:'白糖',netType:'糖浆',amount:25,pct:'13.9%',state:'原料',type:'调料',allergen:'无',note:'糖醋汁'},{name:'食醋',netType:'调味汁',amount:15,pct:'8.3%',state:'原料',type:'调料',allergen:'无',note:'糖醋汁'}],
nutri:{calorie:420,protein:22.4,carb:38.2,fat:18.6,vitC:1.2,mineral:28,fiber:0.6,sodium:620,nutriGrade:'未达标',note:'铁1.8mg'},
cook:{mainMethod:'炸',subMethod:'炒',heat:'大火',duration:15,oil:50,salt:1,temp:'180',output:180,steps:7},
nutri:{calorie:420,protein:22.4,carb:38.2,fat:18.6,vitC:1.2,mineral:28,fiber:0.6,sodium:620,calcium:22,iron:1.8,zinc:1.6,potassium:340,cholesterol:75,vitA:8,vitE:0.4,vitB1:0.45,vitB2:0.18,magnesium:24,phosphorus:200,selenium:14,copper:0.1,manganese:0.08,nutriGrade:'未达标',note:'铁1.8mg'},
cook:{mainMethod:'炸',subMethod:'炒',heat:'大火',duration:15,temp:'180',steps:7},
taste:{sour:4,sweet:4,bitter:1,spicy:1,salty:3,smell:4.0,flavorScore:4.2,flavorGrade:'良好'},
color:{mainColor:'金黄色',uniformity:4,gloss:5,freshness:4,appearance:5,colorScore:4.5,colorGrade:'优秀'},
texture:{hardness:3,sticky:1,crispy:5,smooth:4,elastic:3,textureScore:4.0,textureGrade:'良好'}
@@ -416,8 +484,6 @@ function renderBasic(d){
<div class="info-cell"><div class="info-cell__label">适用餐次</div><div class="info-cell__value">${d.meal}</div></div>
<div class="info-cell"><div class="info-cell__label">过敏原</div><div class="info-cell__value">${d.allergen}</div></div>
<div class="info-cell"><div class="info-cell__label">餐品标签</div><div class="info-cell__value">${d.tags.map(t=>'<span class="tag tag-blue">'+t+'</span>').join('')}</div></div>
<div class="info-cell"><div class="info-cell__label">称重基础价(元/100g)</div><div class="info-cell__value">${d.weightBasePrice||'—'}</div></div>
<div class="info-cell"><div class="info-cell__label">称重会员价(元/100g)</div><div class="info-cell__value">${d.weightVipPrice||'—'}</div></div>
<div class="info-cell"><div class="info-cell__label">来源</div><div class="info-cell__value"><span class="tag ${d.source==='系统内置'?'tag-blue':d.source==='味养创新'?'tag-orange':'tag-gray'}">${d.source||'—'}</span></div></div>
<div class="info-cell"><div class="info-cell__label">启用状态</div><div class="info-cell__value"><span class="tag tag-${d.status==='启用'?'green':'gray'}">${d.status}</span></div></div>
</div></div>
@@ -426,6 +492,7 @@ function renderBasic(d){
${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>单位重量(g)</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>
@@ -436,10 +503,60 @@ function renderBasic(d){
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">🎬</div></div>
</div></div>
<div class="info-section"><div class="info-section__title">🎬 宣传视频 <span class="promo-status-tag">${d.promoVideoEnabled?'<span class="tag tag-blue">已启用</span>':'<span class="tag tag-gray">未启用</span>'}</span></div>
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">🎬</div><div class="media-card__info"><div class="media-card__title">${d.name} 宣传视频</div><div class="media-card__meta">1分30秒 · 48MB · <span class="link">播放</span></div></div></div>
</div></div>`;
${renderPromoVideosSection(d)}`;
}
/* 餐品定价分区(普通 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>'
+ '<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>'
+ '</div>';
}
/* 宣传视频分区(多视频 + 独立启用开关) */
function renderPromoVideosSection(d){
var videos = d.promoVideos || [];
var enabledCount = videos.filter(function(v){return v.enabled;}).length;
if(videos.length === 0){
return '<div class="info-section"><div class="info-section__title">🎬 宣传视频 <span class="promo-status-tag"><span class="tag tag-gray">未上传</span></span></div>'
+ '<div style="text-align:center;padding:24px;color:var(--text-placeholder);font-size:var(--font-sm);background:#FAFBFC;border:1px dashed var(--border);border-radius:var(--radius-md);">暂无宣传视频,请在编辑页上传</div></div>';
}
var cards = videos.map(function(v,i){
return '<div class="promo-video-card">'
+ '<div class="promo-video-card__thumb">🎬'
+ (v.enabled?'<span class="promo-video-card__badge">已启用</span>':'<span class="promo-video-card__badge" style="background:rgba(0,0,0,0.35);">未启用</span>')
+ '</div>'
+ '<div class="promo-video-card__info"><div class="promo-video-card__title">'+v.title+'</div><div class="promo-video-card__meta">'+v.duration+' · '+v.size+'</div></div>'
+ '<div class="promo-video-card__footer">'
+ '<span class="link" style="font-size:var(--font-xs);">▶ 播放</span>'
+ '<div class="promo-toggle'+(v.enabled?' active':'')+'" onclick="togglePromoVideo(this,'+currentDish+','+i+')"><span class="promo-toggle__switch"></span><span class="promo-toggle__label">'+(v.enabled?'已启用':'未启用')+'</span></div>'
+ '</div></div>';
}).join('');
return '<div class="info-section"><div class="info-section__title">🎬 宣传视频 <span class="promo-status-tag"><span class="tag tag-blue">已启用 '+enabledCount+' / 共 '+videos.length+' 个</span></span></div>'
+ '<div class="media-grid">'+cards+'</div></div>';
}
function renderComposition(d){
@@ -458,35 +575,50 @@ function renderComposition(d){
}
function renderNutriSection(d){
const n = d.nutri;
return `<div class="info-section"><div class="info-section__title">📊 营养指标</div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">热量 (kcal/100g)</div><div class="info-cell__value">${n.calorie}</div></div>
<div class="info-cell"><div class="info-cell__label">蛋白质 (g)</div><div class="info-cell__value">${n.protein}</div></div>
<div class="info-cell"><div class="info-cell__label">碳水化合物 (g)</div><div class="info-cell__value">${n.carb}</div></div>
<div class="info-cell"><div class="info-cell__label">脂肪 (g)</div><div class="info-cell__value">${n.fat}</div></div>
<div class="info-cell"><div class="info-cell__label">维生素C (mg)</div><div class="info-cell__value">${n.vitC}</div></div>
<div class="info-cell"><div class="info-cell__label">矿物质 (mg)</div><div class="info-cell__value">${n.mineral}</div></div>
<div class="info-cell"><div class="info-cell__label">膳食纤维 (g)</div><div class="info-cell__value">${n.fiber}</div></div>
<div class="info-cell"><div class="info-cell__label">钠 (mg)</div><div class="info-cell__value">${n.sodium}</div></div>
<div class="info-cell"><div class="info-cell__label">营养等级</div><div class="info-cell__value">${n.nutriGrade==='达标'?'<span class="tag tag-green">达标</span>':n.nutriGrade==='偏差'?'<span class="tag tag-orange">偏差</span>':'<span class="tag tag-red">未达标</span>'}</div></div>
</div></div>`;
var n = d.nutri || {};
/* 营养指标 4 大项(简洁卡片) */
var hero = '<div class="info-section"><div class="info-section__title">📊 营养指标 <span style="font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;">(按 100g 熟重计)</span></div>'
+ '<div class="nutri-tip">ⓘ 营养指标根据食材清单自动计算,不可手动修改</div>'
+ '<div class="nutri-hero">'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">🔥 热量</div><div class="nutri-hero__value">'+(n.calorie||0)+'<span class="nutri-hero__unit">kcal</span></div></div>'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">💪 蛋白质</div><div class="nutri-hero__value">'+(n.protein||0)+'<span class="nutri-hero__unit">g</span></div></div>'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">🌾 碳水化合物</div><div class="nutri-hero__value">'+(n.carb||0)+'<span class="nutri-hero__unit">g</span></div></div>'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">🥑 脂肪</div><div class="nutri-hero__value">'+(n.fat||0)+'<span class="nutri-hero__unit">g</span></div></div>'
+ '</div>';
/* 营养指标 - 其他指标折叠 */
var moreCells = [
{label:'维生素C',value:n.vitC,unit:'mg'},{label:'矿物质',value:n.mineral,unit:'mg'},
{label:'膳食纤维',value:n.fiber,unit:'g'},{label:'钠',value:n.sodium,unit:'mg'},
{label:'钙',value:n.calcium,unit:'mg'},{label:'铁',value:n.iron,unit:'mg'},
{label:'锌',value:n.zinc,unit:'mg'},{label:'钾',value:n.potassium,unit:'mg'},
{label:'胆固醇',value:n.cholesterol,unit:'mg'},{label:'维生素A',value:n.vitA,unit:'μg'},
{label:'维生素E',value:n.vitE,unit:'mg'},{label:'维生素 B1',value:n.vitB1,unit:'mg'},
{label:'维生素 B2',value:n.vitB2,unit:'mg'},{label:'镁',value:n.magnesium,unit:'mg'},
{label:'磷',value:n.phosphorus,unit:'mg'},{label:'硒',value:n.selenium,unit:'μg'},
{label:'铜',value:n.copper,unit:'mg'},{label:'锰',value:n.manganese,unit:'mg'}
];
var moreHtml = moreCells.map(function(c){
return '<div class="info-cell"><div class="info-cell__label">'+c.label+' ('+c.unit+')</div><div class="info-cell__value">'+(c.value!==undefined?c.value:'—')+'</div></div>';
}).join('');
hero += '<div class="nutri-more" id="nutriMore">'
+ '<div class="nutri-more__toggle" onclick="document.getElementById(\'nutriMore\').classList.toggle(\'open\')">查看更多营养指标 <span class="arrow">▼</span></div>'
+ '<div class="nutri-more__body"><div class="info-grid">'+moreHtml+'</div></div>'
+ '</div>';
hero += '</div>';
return hero;
}
function renderCook(d){
const 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>
<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>
<div class="info-cell"><div class="info-cell__label">火候</div><div class="info-cell__value">${c.heat}</div></div>
<div class="info-cell"><div class="info-cell__label">烹饪时长</div><div class="info-cell__value">${c.duration}min</div></div>
<div class="info-cell"><div class="info-cell__label">油用量</div><div class="info-cell__value">${c.oil}g</div></div>
<div class="info-cell"><div class="info-cell__label">盐用量</div><div class="info-cell__value">${c.salt}g</div></div>
<div class="info-cell"><div class="info-cell__label">烹饪温度</div><div class="info-cell__value">${c.temp}℃</div></div>
<div class="info-cell"><div class="info-cell__label">出品量</div><div class="info-cell__value">${c.output}g</div></div>
<div class="info-cell"><div class="info-cell__label">工艺步骤数</div><div class="info-cell__value">${c.steps}步</div></div>
</div></div>`;
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>'
+ '<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>'
+ '<div class="info-cell"><div class="info-cell__label">火候</div><div class="info-cell__value">'+c.heat+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">烹饪时长</div><div class="info-cell__value">'+c.duration+'min</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">烹饪温度</div><div class="info-cell__value">'+c.temp+'℃</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">工艺步骤数</div><div class="info-cell__value">'+c.steps+'步</div></div>'
+ '</div></div>';
}
function renderTaste(d){
@@ -561,6 +693,102 @@ function toggleCleanSale(el){
var label = el.querySelector('.sale-toggle__label');
if(label) label.textContent = active ? '上架' : '下架';
}
/* 宣传视频启用/未启用切换(每个视频独立) */
function togglePromoVideo(el, dishIdx, videoIdx){
var dish = DISHES[dishIdx];
if(!dish || !dish.promoVideos || !dish.promoVideos[videoIdx]) return;
dish.promoVideos[videoIdx].enabled = !dish.promoVideos[videoIdx].enabled;
var enabled = dish.promoVideos[videoIdx].enabled;
var label = el.querySelector('.promo-toggle__label');
if(label) label.textContent = enabled ? '已启用' : '未启用';
el.classList.toggle('active', enabled);
var badge = el.closest('.promo-video-card').querySelector('.promo-video-card__badge');
if(badge){
badge.textContent = enabled ? '已启用' : '未启用';
badge.style.background = enabled ? 'rgba(0,0,0,0.55)' : 'rgba(0,0,0,0.35)';
}
/* 更新顶部统计标签 */
var titleEl = document.querySelector('.promo-status-tag');
if(titleEl){
var enabledCount = dish.promoVideos.filter(function(v){return v.enabled;}).length;
titleEl.innerHTML = '<span class="tag tag-blue">已启用 '+enabledCount+' / 共 '+dish.promoVideos.length+' 个</span>';
}
}
/* 成本核算:计算每百克成本并展示明细 */
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);
}
}
}
/* 小问号弹窗显示/切换 */
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');})();
// 初始化
+298 -72
View File
@@ -176,6 +176,59 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
.status-toggle.active .status-toggle__switch::after{transform:translateX(16px);}
.status-toggle__label{font-size:var(--font-xs);color:var(--text-placeholder);}
.status-toggle.active .status-toggle__label{color:var(--success);}
/* 宣传视频启用开关(蓝色) */
.promo-toggle{display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none;}
.promo-toggle__switch{width:32px;height:18px;background:#D9D9D9;border-radius:9px;position:relative;transition:background .2s;flex-shrink:0;}
.promo-toggle__switch::after{content:'';position:absolute;left:2px;top:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 2px rgba(0,0,0,0.15);}
.promo-toggle.active .promo-toggle__switch{background:var(--primary);}
.promo-toggle.active .promo-toggle__switch::after{transform:translateX(14px);}
.promo-toggle__label{font-size:11px;color:var(--text-placeholder);}
.promo-toggle.active .promo-toggle__label{color:var(--primary);font-weight:500;}
/* 营养指标 - 四大项简洁卡片 */
.nutri-hero{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:14px;}
.nutri-hero__cell{background:#FAFBFC;border:1px solid var(--border);border-radius:var(--radius-md);padding:12px 14px;}
.nutri-hero__label{font-size:var(--font-xs);color:var(--text-secondary);margin-bottom:6px;}
.nutri-hero__value{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);line-height:1.2;}
.nutri-hero__unit{font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;margin-left:2px;}
/* 营养指标 - 折叠区 */
.nutri-more{margin-top:8px;border-top:1px dashed #E8E8E8;padding-top:12px;}
.nutri-more__toggle{display:inline-flex;align-items:center;gap:4px;color:var(--primary);font-size:var(--font-sm);cursor:pointer;user-select:none;padding:4px 10px;border-radius:var(--radius-sm);background:var(--primary-light);transition:all .15s;}
.nutri-more__toggle:hover{background:#BAE7FF;}
.nutri-more__toggle .arrow{display:inline-block;transition:transform .2s;font-size:10px;}
.nutri-more.open .nutri-more__toggle .arrow{transform:rotate(180deg);}
.nutri-more__body{display:none;margin-top:12px;}
.nutri-more.open .nutri-more__body{display:block;}
/* 营养指标 - 自动计算提示 */
.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);}
/* 宣传视频多卡片 */
.promo-video-card{background:#FAFBFC;border:1px solid #F0F0F0;border-radius:var(--radius-md);overflow:hidden;display:flex;flex-direction:column;}
.promo-video-card__thumb{height:100px;background:#E8E8E8;display:flex;align-items:center;justify-content:center;font-size:32px;color:#999;position:relative;}
.promo-video-card__badge{position:absolute;top:6px;left:6px;background:rgba(0,0,0,0.55);color:#fff;font-size:10px;padding:2px 6px;border-radius:3px;}
.promo-video-card__info{padding:8px 12px;flex:1;}
.promo-video-card__title{font-size:var(--font-sm);font-weight:500;margin-bottom:2px;}
.promo-video-card__meta{font-size:var(--font-xs);color:var(--text-placeholder);}
.promo-video-card__footer{padding:6px 12px;border-top:1px solid #F0F0F0;display:flex;align-items:center;justify-content:space-between;background:#fff;}
/* 页面信息栏 + 食堂切换 */
.page-header-bar{display:flex;align-items:center;justify-content:space-between;background:#fff;border-radius:var(--radius-md);padding:16px 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);}
.page-header-bar__left{display:flex;align-items:center;gap:12px;}
@@ -349,42 +402,57 @@ tbody tr:hover td.td-actions{background:#F8FBFF;}
<script>
// 餐品数据
const DISHES = [
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{name:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{name:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false},{name:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',weightBasePrice:'6.00',weightVipPrice:'5.40',promoVideoEnabled:true,crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
{id:'DISH001',name:'红烧肉',icon:'🥩',category:'荤菜',tags:['高蛋白'],specs:[{name:'标准份',weight:200,price:12,cleanPrice:9,cleanSale:false,isDefault:true},{name:'小份',weight:120,price:8,cleanPrice:6,cleanSale:false},{name:'大份',weight:300,price:18,cleanPrice:13,cleanSale:false}],method:'炖',meal:'午/晚',allergen:'无',status:'启用',source:'手动添加',
pricing:{basePrice:'6.00',ingredientCost:4.80,per100gCost:2.40,profitRate:150,suggestedPrice:'6.00'},
promoVideos:[{title:'红烧肉宣传片',duration:'1分30秒',size:'48MB',enabled:true},{title:'制作工艺展示',duration:'2分15秒',size:'72MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'高血脂人群',level:'慎食'},{name:'高血压人群',level:'适量'}],crowdNote:'高血脂人群控制摄入量',cookedWeight:200,
composition:[{name:'猪五花肉',netType:'红烧块',amount:160,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'生姜',netType:'姜片',amount:5,pct:'2.5%',state:'净菜',type:'辅材',allergen:'无',note:'—'},{name:'生抽',netType:'调味汁',amount:15,pct:'7.5%',state:'原料',type:'调料',allergen:'含麸质',note:'酱油'},{name:'冰糖',netType:'糖浆',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'上色提味'},{name:'料酒',netType:'料酒汁',amount:10,pct:'5%',state:'原料',type:'调料',allergen:'无',note:'去腥'}],
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:0.8,mineral:42,fiber:0.3,sodium:486,nutriGrade:'偏差',note:'铁2.4mg、锌1.8mg'},
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,oil:15,salt:3,temp:'100→80',output:200,steps:6},
nutri:{calorie:385,protein:18.2,carb:12.4,fat:28.6,vitC:0.8,mineral:42,fiber:0.3,sodium:486,calcium:18,iron:2.4,zinc:1.8,potassium:280,cholesterol:88,vitA:14,vitE:0.6,vitB1:0.22,vitB2:0.12,magnesium:20,phosphorus:160,selenium:18,copper:0.12,manganese:0.05,nutriGrade:'偏差',note:'铁2.4mg、锌1.8mg'},
cook:{mainMethod:'炖',subMethod:'煎',heat:'中火→小火',duration:45,temp:'100→80',steps:6},
taste:{sour:1,sweet:3,bitter:1,spicy:1,salty:4,smell:4.2,flavorScore:3.8,flavorGrade:'良好'},
color:{mainColor:'红褐色',uniformity:4,gloss:4,freshness:4,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:4,elastic:3,textureScore:3.6,textureGrade:'良好'}
},
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{name:'标准份',weight:150,price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',weightBasePrice:'3.00',weightVipPrice:'2.70',promoVideoEnabled:false,crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
{id:'DISH002',name:'清炒时蔬',icon:'🥬',category:'素菜',tags:['低脂','素食'],specs:[{name:'标准份',weight:150,price:6,cleanPrice:4.5,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:4,cleanPrice:3,cleanSale:false}],method:'炒',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
pricing:{basePrice:'3.00',ingredientCost:1.80,per100gCost:1.20,profitRate:150,suggestedPrice:'3.00'},
promoVideos:[{title:'清炒时蔬宣传片',duration:'45秒',size:'18MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'推荐'},{name:'肾结石人群',level:'慎食'}],crowdNote:'肾结石人群慎食(草酸含量高)',cookedWeight:140,
composition:[{name:'时令青菜',netType:'青菜段',amount:120,pct:'80%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'蒜末',netType:'蒜末',amount:5,pct:'3.3%',state:'净菜',type:'调料',allergen:'无',note:'—'},{name:'食用油',netType:'净油',amount:8,pct:'5.3%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'1.3%',state:'原料',type:'调料',allergen:'无',note:'—'}],
nutri:{calorie:68,protein:2.1,carb:8.6,fat:3.2,vitC:28.5,mineral:86,fiber:2.4,sodium:186,nutriGrade:'达标',note:'铁1.2mg、钙45mg'},
cook:{mainMethod:'炒',subMethod:'—',heat:'大火',duration:5,oil:8,salt:2,temp:'200',output:150,steps:4},
nutri:{calorie:68,protein:2.1,carb:8.6,fat:3.2,vitC:28.5,mineral:86,fiber:2.4,sodium:186,calcium:45,iron:1.2,zinc:0.4,potassium:280,cholesterol:0,vitA:268,vitE:0.9,vitB1:0.04,vitB2:0.06,magnesium:24,phosphorus:42,selenium:1.2,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'铁1.2mg、钙45mg'},
cook:{mainMethod:'炒',subMethod:'—',heat:'大火',duration:5,temp:'200',steps:4},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:2,smell:3.5,flavorScore:3.2,flavorGrade:'一般'},
color:{mainColor:'翠绿色',uniformity:5,gloss:4,freshness:5,appearance:4,colorScore:4.5,colorGrade:'优秀'},
texture:{hardness:3,sticky:1,crispy:4,smooth:3,elastic:2,textureScore:3.4,textureGrade:'良好'}
},
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{name:'标准份',weight:300,price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',weightBasePrice:'1.67',weightVipPrice:'1.50',promoVideoEnabled:true,crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
{id:'DISH003',name:'番茄鸡蛋汤',icon:'🍅',category:'汤品',tags:['低卡'],specs:[{name:'标准份',weight:300,price:5,cleanPrice:3.5,cleanSale:false,isDefault:true}],method:'煮',meal:'午/晚',allergen:'含蛋类',status:'启用',source:'系统内置',
pricing:{basePrice:'1.67',ingredientCost:1.20,per100gCost:0.40,profitRate:150,suggestedPrice:'1.00'},
promoVideos:[{title:'番茄鸡蛋汤宣传片',duration:'1分钟',size:'24MB',enabled:true},{title:'家常做法教程',duration:'3分钟',size:'80MB',enabled:true}],
crowd:[{name:'全人群',level:'推荐'},{name:'儿童',level:'推荐'},{name:'老年人',level:'推荐'}],crowdNote:'',cookedWeight:300,
composition:[{name:'番茄',netType:'番茄块',amount:150,pct:'50%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'鸡蛋',netType:'鸡蛋液',amount:60,pct:'20%',state:'原料',type:'主材',allergen:'含蛋类',note:'—'},{name:'食用油',netType:'净油',amount:5,pct:'1.7%',state:'原料',type:'调料',allergen:'无',note:'—'},{name:'食盐',netType:'盐粒',amount:2,pct:'0.7%',state:'原料',type:'调料',allergen:'无',note:'—'}],
nutri:{calorie:45,protein:3.8,carb:4.2,fat:2.1,vitC:18.2,mineral:32,fiber:1.1,sodium:280,nutriGrade:'达标',note:'铁0.8mg、钾210mg'},
cook:{mainMethod:'煮',subMethod:'炒',heat:'中火',duration:12,oil:5,salt:2,temp:'100',output:300,steps:5},
nutri:{calorie:45,protein:3.8,carb:4.2,fat:2.1,vitC:18.2,mineral:32,fiber:1.1,sodium:280,calcium:18,iron:0.8,zinc:0.4,potassium:210,cholesterol:110,vitA:62,vitE:0.5,vitB1:0.04,vitB2:0.12,magnesium:12,phosphorus:80,selenium:8,copper:0.05,manganese:0.06,nutriGrade:'达标',note:'铁0.8mg、钾210mg'},
cook:{mainMethod:'煮',subMethod:'炒',heat:'中火',duration:12,temp:'100',steps:5},
taste:{sour:3,sweet:2,bitter:1,spicy:1,salty:2,smell:3.8,flavorScore:3.5,flavorGrade:'良好'},
color:{mainColor:'橙红色',uniformity:4,gloss:3,freshness:5,appearance:4,colorScore:4.0,colorGrade:'良好'},
texture:{hardness:1,sticky:2,crispy:1,smooth:5,elastic:2,textureScore:3.8,textureGrade:'良好'}
},
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{name:'标准份',weight:200,price:3,cleanPrice:2,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:2,cleanPrice:1.5,cleanSale:false},{name:'大份',weight:300,price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',weightBasePrice:'1.50',weightVipPrice:'1.35',promoVideoEnabled:false,crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
{id:'DISH004',name:'米饭',icon:'🍚',category:'主食',tags:['主食'],specs:[{name:'标准份',weight:200,price:3,cleanPrice:2,cleanSale:false,isDefault:true},{name:'小份',weight:100,price:2,cleanPrice:1.5,cleanSale:false},{name:'大份',weight:300,price:4,cleanPrice:3,cleanSale:false}],method:'蒸',meal:'早/午/晚',allergen:'无',status:'启用',source:'系统内置',
pricing:{basePrice:'1.50',ingredientCost:1.00,per100gCost:0.50,profitRate:150,suggestedPrice:'1.25'},
promoVideos:[],
crowd:[{name:'全人群',level:'推荐'},{name:'糖尿病人群',level:'适量'}],crowdNote:'糖尿病人群注意控制量',cookedWeight:200,
composition:[{name:'大米',netType:'熟米饭',amount:100,pct:'100%',state:'原料',type:'主材',allergen:'无',note:'—'}],
nutri:{calorie:232,protein:4.8,carb:51.2,fat:0.6,vitC:0,mineral:12,fiber:0.4,sodium:3,nutriGrade:'达标',note:'—'},
cook:{mainMethod:'蒸',subMethod:'—',heat:'大火→中火',duration:30,oil:0,salt:0,temp:'100',output:200,steps:2},
nutri:{calorie:232,protein:4.8,carb:51.2,fat:0.6,vitC:0,mineral:12,fiber:0.4,sodium:3,calcium:10,iron:0.5,zinc:0.8,potassium:43,cholesterol:0,vitA:0,vitE:0.1,vitB1:0.02,vitB2:0.02,magnesium:12,phosphorus:43,selenium:1.5,copper:0.05,manganese:0.18,nutriGrade:'达标',note:'—'},
cook:{mainMethod:'蒸',subMethod:'—',heat:'大火→中火',duration:30,temp:'100',steps:2},
taste:{sour:1,sweet:2,bitter:1,spicy:1,salty:1,smell:3.0,flavorScore:2.8,flavorGrade:'一般'},
color:{mainColor:'乳白色',uniformity:5,gloss:4,freshness:4,appearance:4,colorScore:4.2,colorGrade:'良好'},
texture:{hardness:2,sticky:3,crispy:1,smooth:3,elastic:2,textureScore:3.2,textureGrade:'一般'}
},
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{name:'标准份',weight:180,price:14,cleanPrice:10,cleanSale:false,isDefault:true},{name:'大份',weight:280,price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',weightBasePrice:'7.78',weightVipPrice:'7.00',promoVideoEnabled:true,crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
{id:'DISH005',name:'糖醋里脊',icon:'🍖',category:'荤菜',tags:['高糖'],specs:[{name:'标准份',weight:180,price:14,cleanPrice:10,cleanSale:false,isDefault:true},{name:'大份',weight:280,price:20,cleanPrice:14,cleanSale:false}],method:'炸',meal:'午',allergen:'无',status:'停用',source:'味养创新',
pricing:{basePrice:'7.78',ingredientCost:5.20,per100gCost:2.89,profitRate:170,suggestedPrice:'7.80'},
promoVideos:[{title:'糖醋里脊宣传片',duration:'2分钟',size:'58MB',enabled:true},{title:'正宗做法揭秘',duration:'5分钟',size:'150MB',enabled:true},{title:'出品摆盘展示',duration:'30秒',size:'12MB',enabled:false}],
crowd:[{name:'成人',level:'推荐'},{name:'糖尿病人群',level:'慎食'},{name:'高血脂人群',level:'适量'}],crowdNote:'糖尿病人群慎食,含糖量高',cookedWeight:180,
composition:[{name:'猪里脊肉',netType:'里脊丝',amount:120,pct:'66.7%',state:'净菜',type:'主材',allergen:'无',note:'—'},{name:'淀粉',netType:'淀粉糊',amount:20,pct:'11.1%',state:'原料',type:'辅材',allergen:'无',note:'挂糊'},{name:'白糖',netType:'糖浆',amount:25,pct:'13.9%',state:'原料',type:'调料',allergen:'无',note:'糖醋汁'},{name:'食醋',netType:'调味汁',amount:15,pct:'8.3%',state:'原料',type:'调料',allergen:'无',note:'糖醋汁'}],
nutri:{calorie:420,protein:22.4,carb:38.2,fat:18.6,vitC:1.2,mineral:28,fiber:0.6,sodium:620,nutriGrade:'未达标',note:'铁1.8mg'},
cook:{mainMethod:'炸',subMethod:'炒',heat:'大火',duration:15,oil:50,salt:1,temp:'180',output:180,steps:7},
nutri:{calorie:420,protein:22.4,carb:38.2,fat:18.6,vitC:1.2,mineral:28,fiber:0.6,sodium:620,calcium:22,iron:1.8,zinc:1.6,potassium:340,cholesterol:75,vitA:8,vitE:0.4,vitB1:0.45,vitB2:0.18,magnesium:24,phosphorus:200,selenium:14,copper:0.1,manganese:0.08,nutriGrade:'未达标',note:'铁1.8mg'},
cook:{mainMethod:'炸',subMethod:'炒',heat:'大火',duration:15,temp:'180',steps:7},
taste:{sour:4,sweet:4,bitter:1,spicy:1,salty:3,smell:4.0,flavorScore:4.2,flavorGrade:'良好'},
color:{mainColor:'金黄色',uniformity:4,gloss:5,freshness:4,appearance:5,colorScore:4.5,colorGrade:'优秀'},
texture:{hardness:3,sticky:1,crispy:5,smooth:4,elastic:3,textureScore:4.0,textureGrade:'良好'}
@@ -433,37 +501,85 @@ function renderBasic(d){
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>';
return '<tr><td>'+s.name+'</td><td>'+s.weight+'</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>
<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>
<div class="info-cell"><div class="info-cell__label">适用餐次</div><div class="info-cell__value">${d.meal}</div></div>
<div class="info-cell"><div class="info-cell__label">过敏原</div><div class="info-cell__value">${d.allergen}</div></div>
<div class="info-cell"><div class="info-cell__label">餐品标签</div><div class="info-cell__value">${d.tags.map(t=>'<span class="tag tag-blue">'+t+'</span>').join('')}</div></div>
<div class="info-cell"><div class="info-cell__label">称重基础价(元/100g)</div><div class="info-cell__value">${d.weightBasePrice||'—'}</div></div>
<div class="info-cell"><div class="info-cell__label">称重会员价(元/100g)</div><div class="info-cell__value">${d.weightVipPrice||'—'}</div></div>
<div class="info-cell"><div class="info-cell__label">来源</div><div class="info-cell__value"><span class="tag ${d.source==='系统内置'?'tag-blue':d.source==='味养创新'?'tag-orange':'tag-gray'}">${d.source||'—'}</span></div></div>
<div class="info-cell"><div class="info-cell__label">启用状态</div><div class="info-cell__value"><span class="tag tag-${d.status==='启用'?'green':'gray'}">${d.status}</span></div></div>
</div></div>
<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>单位重量(g)</th><th>餐品售价(元/份)</th><th>净菜包售价(元/份)</th><th>外售净菜包</th><th>状态</th></tr></thead><tbody>${specRows}</tbody></table></div>
<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>
<div class="media-card"><div class="media-card__thumb">图片</div></div>
<div class="media-card"><div class="media-card__thumb">图片</div></div>
</div></div>
<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>
</div></div>
<div class="info-section"><div class="info-section__title">🎬 宣传视频 <span class="promo-status-tag">${d.promoVideoEnabled?'<span class="tag tag-blue">已启用</span>':'<span class="tag tag-gray">未启用</span>'}</span></div>
<div class="media-grid">
<div class="media-card"><div class="media-card__thumb">🎬</div><div class="media-card__info"><div class="media-card__title">${d.name} 宣传视频</div><div class="media-card__meta">1分30秒 · 48MB · <span class="link">播放</span></div></div></div>
</div></div>`;
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>'
+ '<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>'
+ '<div class="info-cell"><div class="info-cell__label">适用餐次</div><div class="info-cell__value">'+d.meal+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">过敏原</div><div class="info-cell__value">'+d.allergen+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">餐品标签</div><div class="info-cell__value">'+d.tags.map(function(t){return '<span class="tag tag-blue">'+t+'</span>';}).join('')+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">来源</div><div class="info-cell__value"><span class="tag '+(d.source==='系统内置'?'tag-blue':d.source==='味养创新'?'tag-orange':'tag-gray')+'">'+(d.source||'—')+'</span></div></div>'
+ '<div class="info-cell"><div class="info-cell__label">启用状态</div><div class="info-cell__value"><span class="tag tag-'+(d.status==='启用'?'green':'gray')+'">'+d.status+'</span></div></div>'
+ '</div></div>'
+ '<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>单位重量(g)</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>'
+ '<div class="media-card"><div class="media-card__thumb">图片</div></div>'
+ '<div class="media-card"><div class="media-card__thumb">图片</div></div>'
+ '</div></div>'
+ '<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></div></div>'
+ renderPromoVideosSection(d);
}
/* 餐品定价分区(普通 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>'
+ '<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>'
+ '</div>';
}
/* 宣传视频分区(多视频 + 独立启用开关) */
function renderPromoVideosSection(d){
var videos = d.promoVideos || [];
var enabledCount = videos.filter(function(v){return v.enabled;}).length;
if(videos.length === 0){
return '<div class="info-section"><div class="info-section__title">🎬 宣传视频 <span class="promo-status-tag"><span class="tag tag-gray">未上传</span></span></div>'
+ '<div style="text-align:center;padding:24px;color:var(--text-placeholder);font-size:var(--font-sm);background:#FAFBFC;border:1px dashed var(--border);border-radius:var(--radius-md);">暂无宣传视频,请在编辑页上传</div></div>';
}
var cards = videos.map(function(v,i){
return '<div class="promo-video-card">'
+ '<div class="promo-video-card__thumb">🎬'
+ (v.enabled?'<span class="promo-video-card__badge">已启用</span>':'<span class="promo-video-card__badge" style="background:rgba(0,0,0,0.35);">未启用</span>')
+ '</div>'
+ '<div class="promo-video-card__info"><div class="promo-video-card__title">'+v.title+'</div><div class="promo-video-card__meta">'+v.duration+' · '+v.size+'</div></div>'
+ '<div class="promo-video-card__footer">'
+ '<span class="link" style="font-size:var(--font-xs);">▶ 播放</span>'
+ '<div class="promo-toggle'+(v.enabled?' active':'')+'" onclick="togglePromoVideo(this,'+currentDish+','+i+')"><span class="promo-toggle__switch"></span><span class="promo-toggle__label">'+(v.enabled?'已启用':'未启用')+'</span></div>'
+ '</div></div>';
}).join('');
return '<div class="info-section"><div class="info-section__title">🎬 宣传视频 <span class="promo-status-tag"><span class="tag tag-blue">已启用 '+enabledCount+' / 共 '+videos.length+' 个</span></span></div>'
+ '<div class="media-grid">'+cards+'</div></div>';
}
function renderComposition(d){
@@ -482,35 +598,50 @@ function renderComposition(d){
}
function renderNutriSection(d){
const n = d.nutri;
return `<div class="info-section"><div class="info-section__title">📊 营养指标</div>
<div class="info-grid">
<div class="info-cell"><div class="info-cell__label">热量 (kcal/100g)</div><div class="info-cell__value">${n.calorie}</div></div>
<div class="info-cell"><div class="info-cell__label">蛋白质 (g)</div><div class="info-cell__value">${n.protein}</div></div>
<div class="info-cell"><div class="info-cell__label">碳水化合物 (g)</div><div class="info-cell__value">${n.carb}</div></div>
<div class="info-cell"><div class="info-cell__label">脂肪 (g)</div><div class="info-cell__value">${n.fat}</div></div>
<div class="info-cell"><div class="info-cell__label">维生素C (mg)</div><div class="info-cell__value">${n.vitC}</div></div>
<div class="info-cell"><div class="info-cell__label">矿物质 (mg)</div><div class="info-cell__value">${n.mineral}</div></div>
<div class="info-cell"><div class="info-cell__label">膳食纤维 (g)</div><div class="info-cell__value">${n.fiber}</div></div>
<div class="info-cell"><div class="info-cell__label">钠 (mg)</div><div class="info-cell__value">${n.sodium}</div></div>
<div class="info-cell"><div class="info-cell__label">营养等级</div><div class="info-cell__value">${n.nutriGrade==='达标'?'<span class="tag tag-green">达标</span>':n.nutriGrade==='偏差'?'<span class="tag tag-orange">偏差</span>':'<span class="tag tag-red">未达标</span>'}</div></div>
</div></div>`;
var n = d.nutri || {};
/* 营养指标 4 大项(简洁卡片) */
var hero = '<div class="info-section"><div class="info-section__title">📊 营养指标 <span style="font-size:var(--font-xs);color:var(--text-placeholder);font-weight:normal;">(按 100g 熟重计)</span></div>'
+ '<div class="nutri-tip">ⓘ 营养指标根据食材清单自动计算,不可手动修改</div>'
+ '<div class="nutri-hero">'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">🔥 热量</div><div class="nutri-hero__value">'+(n.calorie||0)+'<span class="nutri-hero__unit">kcal</span></div></div>'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">💪 蛋白质</div><div class="nutri-hero__value">'+(n.protein||0)+'<span class="nutri-hero__unit">g</span></div></div>'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">🌾 碳水化合物</div><div class="nutri-hero__value">'+(n.carb||0)+'<span class="nutri-hero__unit">g</span></div></div>'
+ '<div class="nutri-hero__cell"><div class="nutri-hero__label">🥑 脂肪</div><div class="nutri-hero__value">'+(n.fat||0)+'<span class="nutri-hero__unit">g</span></div></div>'
+ '</div>';
/* 营养指标 - 其他指标折叠 */
var moreCells = [
{label:'维生素C',value:n.vitC,unit:'mg'},{label:'矿物质',value:n.mineral,unit:'mg'},
{label:'膳食纤维',value:n.fiber,unit:'g'},{label:'钠',value:n.sodium,unit:'mg'},
{label:'钙',value:n.calcium,unit:'mg'},{label:'铁',value:n.iron,unit:'mg'},
{label:'锌',value:n.zinc,unit:'mg'},{label:'钾',value:n.potassium,unit:'mg'},
{label:'胆固醇',value:n.cholesterol,unit:'mg'},{label:'维生素A',value:n.vitA,unit:'μg'},
{label:'维生素E',value:n.vitE,unit:'mg'},{label:'维生素 B1',value:n.vitB1,unit:'mg'},
{label:'维生素 B2',value:n.vitB2,unit:'mg'},{label:'镁',value:n.magnesium,unit:'mg'},
{label:'磷',value:n.phosphorus,unit:'mg'},{label:'硒',value:n.selenium,unit:'μg'},
{label:'铜',value:n.copper,unit:'mg'},{label:'锰',value:n.manganese,unit:'mg'}
];
var moreHtml = moreCells.map(function(c){
return '<div class="info-cell"><div class="info-cell__label">'+c.label+' ('+c.unit+')</div><div class="info-cell__value">'+(c.value!==undefined?c.value:'—')+'</div></div>';
}).join('');
hero += '<div class="nutri-more" id="nutriMore">'
+ '<div class="nutri-more__toggle" onclick="document.getElementById(\'nutriMore\').classList.toggle(\'open\')">查看更多营养指标 <span class="arrow">▼</span></div>'
+ '<div class="nutri-more__body"><div class="info-grid">'+moreHtml+'</div></div>'
+ '</div>';
hero += '</div>';
return hero;
}
function renderCook(d){
const 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>
<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>
<div class="info-cell"><div class="info-cell__label">火候</div><div class="info-cell__value">${c.heat}</div></div>
<div class="info-cell"><div class="info-cell__label">烹饪时长</div><div class="info-cell__value">${c.duration}min</div></div>
<div class="info-cell"><div class="info-cell__label">油用量</div><div class="info-cell__value">${c.oil}g</div></div>
<div class="info-cell"><div class="info-cell__label">盐用量</div><div class="info-cell__value">${c.salt}g</div></div>
<div class="info-cell"><div class="info-cell__label">烹饪温度</div><div class="info-cell__value">${c.temp}℃</div></div>
<div class="info-cell"><div class="info-cell__label">出品量</div><div class="info-cell__value">${c.output}g</div></div>
<div class="info-cell"><div class="info-cell__label">工艺步骤数</div><div class="info-cell__value">${c.steps}步</div></div>
</div></div>`;
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>'
+ '<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>'
+ '<div class="info-cell"><div class="info-cell__label">火候</div><div class="info-cell__value">'+c.heat+'</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">烹饪时长</div><div class="info-cell__value">'+c.duration+'min</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">烹饪温度</div><div class="info-cell__value">'+c.temp+'℃</div></div>'
+ '<div class="info-cell"><div class="info-cell__label">工艺步骤数</div><div class="info-cell__value">'+c.steps+'步</div></div>'
+ '</div></div>';
}
function renderTaste(d){
@@ -585,6 +716,101 @@ function toggleCleanSale(el){
var label = el.querySelector('.sale-toggle__label');
if(label) label.textContent = active ? '上架' : '下架';
}
/* 宣传视频启用/未启用切换(每个视频独立) */
function togglePromoVideo(el, dishIdx, videoIdx){
var dish = DISHES[dishIdx];
if(!dish || !dish.promoVideos || !dish.promoVideos[videoIdx]) return;
dish.promoVideos[videoIdx].enabled = !dish.promoVideos[videoIdx].enabled;
var enabled = dish.promoVideos[videoIdx].enabled;
var label = el.querySelector('.promo-toggle__label');
if(label) label.textContent = enabled ? '已启用' : '未启用';
el.classList.toggle('active', enabled);
var badge = el.closest('.promo-video-card').querySelector('.promo-video-card__badge');
if(badge){
badge.textContent = enabled ? '已启用' : '未启用';
badge.style.background = enabled ? 'rgba(0,0,0,0.55)' : 'rgba(0,0,0,0.35)';
}
var titleEl = document.querySelector('.promo-status-tag');
if(titleEl){
var enabledCount = dish.promoVideos.filter(function(v){return v.enabled;}).length;
titleEl.innerHTML = '<span class="tag tag-blue">已启用 '+enabledCount+' / 共 '+dish.promoVideos.length+' 个</span>';
}
}
/* 成本核算:计算每百克成本并展示明细 */
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);
}
}
}
/* 小问号弹窗显示/切换 */
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');})();
// 初始化
+1 -1
View File
@@ -285,7 +285,7 @@ tbody tr:hover{background:#F8FBFF;}
<div class="recharge-form-item">
<label>支付方式</label>
<select id="rechargeMethod">
<option>微信支付</option><option>现金支付</option><option>银行转账</option><option>支付宝支付</option><option>后台挂账</option>
<option>微信支付</option><option>现金支付</option><option>支付宝支付</option>
</select>
</div>
<div class="recharge-form-item">
+1 -323
View File
@@ -136,45 +136,6 @@ tbody td.td-actions{overflow:visible;}
.vip-edit-form__item input:focus{border-color:var(--primary);}
.vip-edit-form__readonly{height:36px;display:flex;align-items:center;font-size:var(--font-sm);color:var(--text-primary);font-weight:500;background:#FAFAFA;border-radius:var(--radius-sm);padding:0 10px;}
/* 批量充值弹窗 */
.batch-recharge-modal{width:880px;max-width:94vw;height:80vh;max-height:680px;display:flex;flex-direction:column;}
.batch-recharge-modal .modal-body{padding:0;display:flex;flex-direction:column;overflow:hidden;flex:1;}
.br-filter{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:12px 20px;border-bottom:1px solid var(--border);background:#FAFAFA;}
.br-filter .filter-item{display:flex;align-items:center;gap:6px;}
.br-filter .filter-item label{font-size:var(--font-xs);color:var(--text-secondary);white-space:nowrap;}
.br-filter .filter-item input,.br-filter .filter-item select{height:30px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 8px;font-size:var(--font-sm);outline:none;background:#fff;min-width:120px;}
.br-filter .filter-item select{padding-right:24px;appearance:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 8px center;cursor:pointer;}
.br-filter .btn{height:30px;padding:0 12px;font-size:var(--font-sm);}
.br-table-wrap{flex:1;overflow:auto;padding:0 20px;}
.br-table{width:100%;border-collapse:collapse;font-size:var(--font-sm);}
.br-table thead th{position:sticky;top:0;background:#fff;padding:10px 8px;text-align:left;color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);font-size:var(--font-xs);z-index:1;}
.br-table tbody td{padding:10px 8px;border-bottom:1px solid #f5f5f5;color:var(--text-primary);}
.br-table tbody tr:hover{background:#F8FBFF;}
.br-table input[type="checkbox"]{cursor:pointer;}
.br-table .br-balance-before{color:var(--text-secondary);}
.br-table .br-balance-after{color:var(--primary);font-weight:600;}
.br-table .br-arrow{color:var(--text-placeholder);padding:0 4px;}
.br-empty{text-align:center;padding:40px 0;color:var(--text-placeholder);font-size:var(--font-sm);}
.br-config{border-top:1px solid var(--border);padding:12px 20px;background:#fff;}
.br-config__row{display:grid;grid-template-columns:1.2fr 1fr 1.6fr;gap:12px;margin-bottom:10px;}
.br-config__item{display:flex;flex-direction:column;gap:4px;}
.br-config__item label{font-size:var(--font-xs);color:var(--text-secondary);}
.br-config__item input,.br-config__item select{height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 8px;font-size:var(--font-sm);outline:none;width:100%;}
.br-config__item select{padding-right:24px;appearance:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 8px center;cursor:pointer;}
.br-config__item input:focus,.br-config__item select:focus{border-color:var(--primary);}
.br-config__amount{display:flex;align-items:center;height:32px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 8px;background:#fff;}
.br-config__amount:focus-within{border-color:var(--primary);}
.br-config__amount .currency{font-size:var(--font-base);font-weight:600;color:var(--text-primary);margin-right:2px;}
.br-config__amount input{border:none;font-size:var(--font-base);font-weight:600;outline:none;width:100%;padding:0;background:transparent;height:32px;}
.br-summary{display:flex;align-items:center;justify-content:space-between;background:#E6F7FF;border-radius:var(--radius-sm);padding:8px 12px;font-size:var(--font-sm);}
.br-summary__count{color:var(--text-secondary);}
.br-summary__count b{color:var(--primary);font-weight:600;margin:0 2px;}
.br-summary__total{display:flex;align-items:center;gap:8px;}
.br-summary__total-label{color:var(--text-secondary);}
.br-summary__total-before{color:var(--text-secondary);}
.br-summary__total-arrow{color:var(--text-placeholder);}
.br-summary__total-after{color:var(--primary);font-weight:600;font-size:var(--font-base);}
/* 页面信息栏 */
.page-header-bar{display:flex;align-items:center;justify-content:space-between;background:#fff;border-radius:var(--radius-md);padding:16px 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);}
.page-header-bar__left{display:flex;align-items:center;gap:12px;}
@@ -511,7 +472,7 @@ tbody td.td-actions{overflow:visible;}
</div>
<div class="action-bar">
<button class="btn btn-primary"> 新增用户</button>
<button class="btn btn-primary" onclick="openBatchRecharge()">💰 批量充值</button>
<a href="din-user-recharge.html" class="btn btn-primary">💰 批量充值</a>
<button class="btn btn-primary"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 10v2h10v-2M7 2v7M4.5 6.5L7 9l2.5-2.5" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>导出列表数据</button>
<button class="btn btn-default"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4"/><path d="M4 5h6M4 7.5h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>查看导出任务</button>
</div>
@@ -799,13 +760,6 @@ const Modal = {
close() {
const o = document.querySelector('.modal-overlay');
if (!o) return;
// 清理批量充值弹窗的级联监听
if (typeof closeBrCascaderOnOutside === 'function') {
document.removeEventListener('click', closeBrCascaderOnOutside);
}
brCasEl = null;
brCasLeftEl = null;
brCasRightEl = null;
o.classList.add('closing');
setTimeout(() => o.remove(), 200);
}
@@ -1191,282 +1145,6 @@ function saveVipEdit() {
showToast('会员信息已保存');
}
/* ========== 批量充值弹窗 ========== */
// 候选用户池(来自 Tab1 个人账户列表)
var batchRechargeUsers = [
{name:'张伟', phone:'13812345601', code:'EMP003', category:'会员用户', unit:'', dept:'', balance:256.50},
{name:'李娜', phone:'13912345602', code:'EMP001', category:'会员用户', unit:'', dept:'', balance:512.00},
{name:'王磊', phone:'18812345603', code:'EMP002', category:'普通用户', unit:'', dept:'', balance:0},
{name:'赵敏', phone:'13712345604', code:'EMP004', category:'会员用户', unit:'', dept:'', balance:320.00},
{name:'陈刚', phone:'15812345605', code:'EMP005', category:'普通用户', unit:'', dept:'', balance:0},
{name:'刘芳', phone:'13612345606', code:'EMP006', category:'会员用户', unit:'', dept:'', balance:678.00},
{name:'孙磊', phone:'18612345607', code:'EMP007', category:'普通用户', unit:'', dept:'', balance:0},
{name:'钱芳', phone:'15912345608', code:'EMP008', category:'会员用户', unit:'', dept:'', balance:0},
{name:'张运维', phone:'13512345609', code:'EMP009', category:'员工', unit:'总部机关', dept:'运维部', balance:0},
{name:'李后勤', phone:'18712345610', code:'EMP010', category:'员工', unit:'总部机关', dept:'后勤部', balance:0},
{name:'王保安', phone:'15612345611', code:'WK003', category:'工作人员', unit:'', dept:'', balance:0},
{name:'赵清洁', phone:'18912345612', code:'WK004', category:'工作人员', unit:'', dept:'', balance:0},
{name:'林教授', phone:'13312345613', code:'EXT001', category:'外部人员', unit:'外部单位', dept:'访客组', balance:0}
];
// 选中状态、筛选条件
var brSelected = {}; // { code: true }
var brFilter = {name:'', code:'', category:'', status:''};
function openBatchRecharge() {
// 重置状态
brSelected = {};
brFilter = {name:'', code:'', category:'', unit:'', dept:''};
// 重置弹窗内级联选择状态
brCasSelectedUnit = '';
brCasSelectedDept = '';
var html = ''
+ '<div class="br-filter">'
+ ' <div class="filter-item"><label>姓名</label><input type="text" id="brFname" placeholder="请输入姓名" oninput="onBrFilterChange()"></div>'
+ ' <div class="filter-item"><label>用户编号</label><input type="text" id="brFcode" placeholder="请输入编号" oninput="onBrFilterChange()"></div>'
+ ' <div class="filter-item"><label>单位部门</label><div class="cascader" id="brCascader">'
+ ' <div class="cascader__trigger" onclick="toggleBrCascader()"><span class="cascader__text">全部</span><span class="cascader__arrow">▾</span></div>'
+ ' <div class="cascader__panel"><div class="cascader__col cascader__col--left" id="brCasLeft"></div><div class="cascader__col cascader__col--right" id="brCasRight"></div></div>'
+ ' </div></div>'
+ ' <div class="filter-item"><label>人员类别</label><select id="brFcat" onchange="onBrFilterChange()">'
+ ' <option value="">全部</option><option>会员用户</option><option>普通用户</option><option>员工</option><option>工作人员</option><option>外部人员</option>'
+ ' </select></div>'
+ ' <button class="btn btn-default" onclick="resetBrFilter()">重置</button>'
+ '</div>'
+ '<div class="br-table-wrap"><table class="br-table" id="brTable"></table></div>'
+ '<div class="br-config">'
+ ' <div class="br-config__row">'
+ ' <div class="br-config__item">'
+ ' <label>充值金额(每人)</label>'
+ ' <div class="br-config__amount"><span class="currency">¥</span><input type="number" id="brAmount" placeholder="0.00" min="0.01" step="0.01" oninput="onBrAmountChange()"></div>'
+ ' </div>'
+ ' <div class="br-config__item">'
+ ' <label>支付方式</label>'
+ ' <select id="brMethod"><option>微信支付</option><option>现金支付</option><option>银行转账</option><option>支付宝支付</option><option>后台挂账</option></select>'
+ ' </div>'
+ ' <div class="br-config__item">'
+ ' <label>备注</label>'
+ ' <input type="text" id="brRemark" placeholder="选填">'
+ ' </div>'
+ ' </div>'
+ ' <div class="br-summary" id="brSummary"></div>'
+ '</div>';
var footer = '<button class="btn btn-default" onclick="Modal.close()">取消</button>'
+ '<button class="btn btn-primary" id="brSubmit" onclick="submitBatchRecharge()">确认充值</button>';
Modal.open('批量充值', html, 'batch-recharge-modal', footer);
// 初始化弹窗内级联控件
brCasEl = document.getElementById('brCascader');
brCasLeftEl = document.getElementById('brCasLeft');
brCasRightEl = document.getElementById('brCasRight');
renderBrCasLeft();
renderBrCasRight();
updateBrCasText();
// 绑定外部点击关闭
document.addEventListener('click', closeBrCascaderOnOutside);
renderBrTable();
updateBrSummary();
}
/* ========== 弹窗内级联选择器(独立实例,避免与外层冲突) ========== */
var brCasEl = null, brCasLeftEl = null, brCasRightEl = null;
var brCasSelectedUnit = '', brCasSelectedDept = '';
function renderBrCasLeft() {
var html = '<div class="cascader__col-item'+(brCasSelectedUnit===''?' active':'')+'" data-val="">全部</div>';
Object.keys(CASCADER_DATA).forEach(function(u){
html += '<div class="cascader__col-item'+(brCasSelectedUnit===u?' active':'')+'" data-val="'+u+'">'+u+'</div>';
});
brCasLeftEl.innerHTML = html;
brCasLeftEl.querySelectorAll('.cascader__col-item').forEach(function(item){
item.onclick = function(e){ e.stopPropagation(); selectBrUnit(this.dataset.val); };
});
}
function renderBrCasRight() {
if (!brCasSelectedUnit) { brCasRightEl.innerHTML = ''; return; }
var depts = CASCADER_DATA[brCasSelectedUnit] || [];
var html = '<div class="cascader__col-item'+(brCasSelectedDept===''?' active':'')+'" data-val="">全部</div>';
depts.forEach(function(d){
html += '<div class="cascader__col-item'+(brCasSelectedDept===d?' active':'')+'" data-val="'+d+'">'+d+'</div>';
});
brCasRightEl.innerHTML = html;
brCasRightEl.querySelectorAll('.cascader__col-item').forEach(function(item){
item.onclick = function(e){ e.stopPropagation(); selectBrDept(this.dataset.val); };
});
}
function selectBrUnit(val) {
brCasSelectedUnit = val;
brCasSelectedDept = '';
brFilter.unit = val;
brFilter.dept = '';
renderBrCasLeft();
renderBrCasRight();
if (!val) { updateBrCasText(); closeBrCascader(); renderBrTable(); updateBrSummary(); }
}
function selectBrDept(val) {
brCasSelectedDept = val;
brFilter.dept = val;
renderBrCasRight();
updateBrCasText();
closeBrCascader();
renderBrTable();
updateBrSummary();
}
function updateBrCasText() {
var txt = '全部';
if (brCasSelectedUnit && brCasSelectedDept) txt = brCasSelectedUnit + '/' + brCasSelectedDept;
else if (brCasSelectedUnit) txt = brCasSelectedUnit;
if (brCasEl) brCasEl.querySelector('.cascader__text').textContent = txt;
}
function toggleBrCascader() {
if (!brCasEl) return;
var isOpen = brCasEl.classList.contains('open');
if (isOpen) { closeBrCascader(); }
else { renderBrCasLeft(); renderBrCasRight(); brCasEl.classList.add('open'); }
}
function closeBrCascader() {
if (brCasEl) brCasEl.classList.remove('open');
}
function closeBrCascaderOnOutside(e) {
// 仅在弹窗打开期间生效
if (!brCasEl) return;
if (!brCasEl.contains(e.target)) closeBrCascader();
}
function getFilteredBrUsers() {
return batchRechargeUsers.filter(function(u){
if (brFilter.name && u.name.indexOf(brFilter.name) < 0) return false;
if (brFilter.code && u.code.toLowerCase().indexOf(brFilter.code.toLowerCase()) < 0) return false;
if (brFilter.category && u.category !== brFilter.category) return false;
if (brFilter.unit && u.unit !== brFilter.unit) return false;
if (brFilter.dept && u.dept !== brFilter.dept) return false;
return true;
});
}
function onBrFilterChange() {
brFilter.name = document.getElementById('brFname').value.trim();
brFilter.code = document.getElementById('brFcode').value.trim();
brFilter.category = document.getElementById('brFcat').value;
renderBrTable();
updateBrSummary();
}
function resetBrFilter() {
brFilter = {name:'', code:'', category:'', unit:'', dept:''};
brCasSelectedUnit = '';
brCasSelectedDept = '';
document.getElementById('brFname').value = '';
document.getElementById('brFcode').value = '';
document.getElementById('brFcat').value = '';
renderBrCasLeft();
renderBrCasRight();
updateBrCasText();
renderBrTable();
updateBrSummary();
}
function renderBrTable() {
var list = getFilteredBrUsers();
var amount = parseFloat(document.getElementById('brAmount') && document.getElementById('brAmount').value) || 0;
var allChecked = list.length > 0 && list.every(function(u){return brSelected[u.code];});
var thead = '<thead><tr>'
+ '<th style="width:36px;"><input type="checkbox" '+(allChecked?'checked':'')+' onchange="onBrSelectAll(this.checked)"></th>'
+ '<th>姓名</th><th>手机号</th><th>用户编号</th><th>人员类别</th><th>单位部门</th><th style="width:200px;">充值前后余额</th>'
+ '</tr></thead><tbody>';
var rows = '';
list.forEach(function(u){
var checked = brSelected[u.code] ? 'checked' : '';
var afterBalance = (u.balance + amount).toFixed(2);
var balanceCell = '<span class="br-balance-before">¥'+u.balance.toFixed(2)+'</span>'
+ '<span class="br-arrow">→</span>'
+ '<span class="br-balance-after">¥'+afterBalance+'</span>';
var deptText = (u.unit && u.dept) ? (u.unit + '/' + u.dept) : (u.unit || '—');
rows += '<tr>'
+ '<td><input type="checkbox" '+checked+' onchange="onBrSelectRow(\''+u.code+'\', this.checked)"></td>'
+ '<td>'+u.name+'</td>'
+ '<td>'+(u.phone || '—')+'</td>'
+ '<td>'+u.code+'</td>'
+ '<td>'+u.category+'</td>'
+ '<td>'+deptText+'</td>'
+ '<td>'+balanceCell+'</td>'
+ '</tr>';
});
if (!list.length) {
rows = '<tr><td colspan="7" class="br-empty">暂无符合条件的用户</td></tr>';
}
document.getElementById('brTable').innerHTML = thead + rows + '</tbody>';
}
function onBrSelectAll(checked) {
var list = getFilteredBrUsers();
list.forEach(function(u){
if (checked) brSelected[u.code] = true;
else delete brSelected[u.code];
});
renderBrTable();
updateBrSummary();
}
function onBrSelectRow(code, checked) {
if (checked) brSelected[code] = true;
else delete brSelected[code];
renderBrTable();
updateBrSummary();
}
function onBrAmountChange() {
// 金额变化时同步刷新表格中的"充值后余额"与底部汇总
renderBrTable();
updateBrSummary();
}
function updateBrSummary() {
var amount = parseFloat(document.getElementById('brAmount').value) || 0;
var selected = batchRechargeUsers.filter(function(u){return brSelected[u.code];});
var count = selected.length;
var totalBefore = selected.reduce(function(s, u){return s + u.balance;}, 0);
var totalAfter = totalBefore + amount * count;
var summaryEl = document.getElementById('brSummary');
if (!summaryEl) return;
if (count === 0) {
summaryEl.innerHTML = '<div class="br-summary__count">未选择任何用户</div>';
} else {
summaryEl.innerHTML = ''
+ '<div class="br-summary__count">已选 <b>'+count+'</b> 人,每人充值 <b>¥'+amount.toFixed(2)+'</b></div>'
+ '<div class="br-summary__total">'
+ ' <span class="br-summary__total-label">合计:</span>'
+ ' <span class="br-summary__total-before">¥'+totalBefore.toFixed(2)+'</span>'
+ ' <span class="br-summary__total-arrow">→</span>'
+ ' <span class="br-summary__total-after">¥'+totalAfter.toFixed(2)+'</span>'
+ '</div>';
}
// 提交按钮置灰控制
var submitBtn = document.getElementById('brSubmit');
if (submitBtn) {
var valid = count > 0 && amount > 0;
submitBtn.disabled = !valid;
submitBtn.style.opacity = valid ? '1' : '0.5';
submitBtn.style.cursor = valid ? 'pointer' : 'not-allowed';
}
}
function submitBatchRecharge() {
var amount = parseFloat(document.getElementById('brAmount').value);
if (!amount || amount <= 0) { showToast('请输入有效的充值金额'); return; }
var count = batchRechargeUsers.filter(function(u){return brSelected[u.code];}).length;
if (count === 0) { showToast('请至少选择一个用户'); return; }
var method = document.getElementById('brMethod').value;
var totalAdd = (amount * count).toFixed(2);
Modal.close();
showToast('批量充值成功!已为 '+count+' 名用户充值 '+method+' 共 ¥'+totalAdd);
}
/* ========== 人脸采集弹窗 ========== */
// 模拟:张伟、李娜有人脸数据,其他人无
const faceDataMap = { '张伟': true, '李娜': true };
+824
View File
@@ -0,0 +1,824 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>营养管理 - 批量充值 | 健康CQ原型</title>
<style>
/* 设计令牌 */
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:#F5F7FA;color:#333;min-width:1280px;}
:root{
--primary:#1890FF;--primary-light:#E6F7FF;--success:#52C41A;--warning:#FAAD14;--danger:#FF4D4F;
--text-primary:#333;--text-secondary:#666;--text-placeholder:#999;--border:#E8E8E8;--background:#F5F7FA;
--font-xs:11px;--font-sm:13px;--font-base:15px;--font-lg:17px;--font-xl:20px;--font-xxl:24px;
--radius-sm:4px;--radius-md:8px;--radius-lg:12px;
--shadow-sm:0 1px 4px rgba(0,0,0,0.08);--shadow-md:0 4px 12px rgba(0,0,0,0.12);
}
/* 通用布局 */
.top-bar{display:flex;align-items:center;height:56px;background:#001529;padding:0 24px;position:fixed;top:0;left:0;right:0;z-index:100;}
.top-bar__logo{display:flex;align-items:center;gap:8px;color:#fff;font-size:var(--font-lg);font-weight:600;white-space:nowrap;width:240px;flex-shrink:0;}
.top-bar__logo svg{width:28px;height:28px;}
.top-bar__nav{display:flex;align-items:center;gap:4px;flex:1;}
.top-bar__nav a{color:rgba(255,255,255,0.65);text-decoration:none;padding:6px 16px;border-radius:var(--radius-sm);font-size:var(--font-sm);transition:all .2s;}
.top-bar__nav a:hover{color:#fff;background:rgba(255,255,255,0.08);}
.top-bar__nav a.active{color:#fff;background:var(--primary);}
.top-bar__right{display:flex;align-items:center;gap:16px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);}
.top-bar__right .btn-back{color:rgba(255,255,255,0.65);cursor:pointer;display:flex;align-items:center;gap:4px;text-decoration:none;}
.top-bar__right .btn-back:hover{color:#fff;}
.avatar{width:32px;height:32px;border-radius:50%;background:#1890FF;display:flex;align-items:center;justify-content:center;color:#fff;font-size:var(--font-sm);cursor:pointer;}
.sidebar{position:fixed;top:56px;left:0;bottom:0;width:240px;background:#001529;overflow-y:auto;z-index:90;padding-top:8px;}
.sidebar-menu__title{color:rgba(255,255,255,0.35);font-size:var(--font-xs);padding:16px 24px 8px;text-transform:uppercase;letter-spacing:.5px;}
.sidebar-item{display:flex;align-items:center;gap:8px;padding:10px 24px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);cursor:pointer;transition:all .15s;text-decoration:none;}
.sidebar-item:hover{color:#fff;background:rgba(255,255,255,0.06);}
.sidebar-item.active{color:#fff;background:var(--primary);}
.mi{display:inline-block;width:20px;text-align:center;filter:grayscale(1);opacity:0.65;margin-right:2px;}
.sidebar-item.active .mi{opacity:0.9;}
.sidebar-subgroup__header{display:flex;align-items:center;gap:6px;padding:10px 24px;color:rgba(255,255,255,0.65);font-size:var(--font-sm);cursor:pointer;transition:all .15s;user-select:none;}
.sidebar-subgroup__header:hover{color:rgba(255,255,255,0.85);background:rgba(255,255,255,0.04);}
.sidebar-subgroup__header .arrow{width:10px;height:10px;transition:transform .2s;flex-shrink:0;}
.sidebar-subgroup__header .arrow path{stroke:rgba(255,255,255,0.45);}
.sidebar-subgroup.open>.sidebar-subgroup__header{color:rgba(255,255,255,0.85);}
.sidebar-subgroup.open>.sidebar-subgroup__header .arrow{transform:rotate(90deg);}
.sidebar-subgroup__body{display:none;padding-left:16px;}
.sidebar-subgroup.open>.sidebar-subgroup__body{display:block;}
.sidebar-subgroup__body .sidebar-item{padding-left:36px;font-size:12px;}
.main{margin-left:240px;margin-top:56px;padding:24px;min-height:calc(100vh - 56px);}
.btn{height:36px;padding:0 16px;border-radius:var(--radius-sm);font-size:var(--font-sm);cursor:pointer;border:none;transition:all .2s;display:inline-flex;align-items:center;gap:6px;}
.btn-primary{background:var(--primary);color:#fff;}.btn-primary:hover{background:#40a9ff;}
.btn-primary:disabled{background:var(--primary);color:#fff;opacity:.5;cursor:not-allowed;}
.btn-primary:disabled:hover{background:var(--primary);}
.btn-default{background:#fff;color:var(--text-primary);border:1px solid var(--border);}.btn-default:hover{color:var(--primary);border-color:var(--primary);}
.filter-item{display:flex;align-items:center;gap:8px;}
.filter-item label{font-size:var(--font-sm);color:var(--text-secondary);white-space:nowrap;}
.filter-input{height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff;min-width:160px;}
.filter-input:focus{border-color:var(--primary);}
.filter-select{height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 28px 0 12px;font-size:var(--font-sm);color:var(--text-primary);outline:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 10px center;appearance:none;cursor:pointer;min-width:120px;}
/* 级联选择器 */
.cascader{position:relative;display:inline-block;}
.cascader__trigger{display:flex;align-items:center;gap:4px;height:36px;padding:0 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#fff;cursor:pointer;font-size:var(--font-sm);min-width:160px;justify-content:space-between;color:var(--text-primary);}
.cascader__trigger:hover{border-color:var(--primary);}
.cascader__arrow{font-size:10px;color:var(--text-secondary);transition:transform .2s;}
.cascader.open .cascader__arrow{transform:rotate(180deg);}
.cascader__panel{display:none;position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-md);border:1px solid var(--border);z-index:100;white-space:nowrap;}
.cascader.open .cascader__panel{display:flex;}
.cascader__col{min-width:120px;max-height:240px;overflow-y:auto;padding:4px 0;border-right:1px solid var(--border);}
.cascader__col:last-child{border-right:none;}
.cascader__col-item{padding:6px 16px;cursor:pointer;font-size:var(--font-sm);color:var(--text-primary);}
.cascader__col-item:hover{background:#F8FBFF;}
.cascader__col-item.active{color:var(--primary);background:var(--primary-light);}
/* 页面信息栏 */
.page-header-bar{display:flex;align-items:center;justify-content:space-between;background:#fff;border-radius:var(--radius-md);padding:16px 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);}
.page-header-bar__left{display:flex;align-items:center;gap:12px;}
.page-header-bar__title{font-size:var(--font-lg);font-weight:700;color:var(--text-primary);}
.page-header-bar__meta{font-size:var(--font-sm);color:var(--text-placeholder);}
.page-header-bar__right{display:flex;align-items:center;gap:12px;}
/* 充值配置区 */
.recharge-config-card{background:#fff;border-radius:var(--radius-md);padding:20px 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);}
.recharge-config-card__title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);margin-bottom:16px;display:flex;align-items:center;gap:6px;}
.recharge-config__row{display:grid;grid-template-columns:1fr 1fr 1.6fr;gap:16px;margin-bottom:16px;}
.recharge-config__item{display:flex;flex-direction:column;gap:6px;}
.recharge-config__item label{font-size:var(--font-sm);color:var(--text-secondary);}
.recharge-config__item select,
.recharge-config__item input{height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 10px;font-size:var(--font-sm);outline:none;width:100%;background:#fff;}
.recharge-config__item select{padding-right:28px;appearance:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 10px center;cursor:pointer;}
.recharge-config__item input:focus,
.recharge-config__item select:focus{border-color:var(--primary);}
.recharge-config__amount{display:flex;align-items:center;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 10px;background:#fff;}
.recharge-config__amount:focus-within{border-color:var(--primary);}
.recharge-config__amount .currency{font-size:var(--font-base);font-weight:600;color:var(--text-primary);margin-right:2px;}
.recharge-config__amount input{border:none;font-size:var(--font-base);font-weight:600;outline:none;width:100%;padding:0;background:transparent;height:36px;}
/* 实时汇总条(含确认充值按钮) */
.recharge-summary-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;background:var(--primary-light);border-radius:var(--radius-sm);padding:12px 16px;font-size:var(--font-sm);border:1px solid #BAE7FF;}
.recharge-summary{display:flex;align-items:center;gap:24px;flex:1;min-width:0;flex-wrap:wrap;}
.recharge-summary__count{color:var(--text-secondary);}
.recharge-summary__count b{color:var(--primary);font-weight:600;margin:0 2px;}
.recharge-summary__total{display:flex;align-items:center;gap:8px;}
.recharge-summary__total-label{color:var(--text-secondary);}
.recharge-summary__total-before{color:var(--text-secondary);}
.recharge-summary__total-arrow{color:var(--text-placeholder);}
.recharge-summary__total-after{color:var(--primary);font-weight:600;font-size:var(--font-base);}
.recharge-summary-bar__actions{display:flex;align-items:center;gap:8px;flex-shrink:0;}
.recharge-summary-bar__actions .btn{height:40px;padding:0 24px;font-size:var(--font-base);}
.recharge-summary-bar__hint{font-size:var(--font-xs);color:var(--text-secondary);margin-right:4px;}
/* 筛选与表格区 */
.filter-bar{background:#fff;border-radius:var(--radius-md);padding:16px 24px;margin-bottom:16px;box-shadow:var(--shadow-sm);display:flex;align-items:center;gap:16px;flex-wrap:wrap;}
.table-card{background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-sm);overflow:hidden;}
.table-card__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);}
.table-card__title{font-size:var(--font-base);font-weight:600;}
.table-card__actions{display:flex;gap:8px;align-items:center;}
.table-card__content{overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;}
.table-card__content table{min-width:100%;white-space:nowrap;}
table{width:100%;border-collapse:collapse;}
thead th{background:#FAFAFA;padding:12px 16px;text-align:left;font-size:var(--font-sm);color:var(--text-secondary);font-weight:500;border-bottom:1px solid var(--border);white-space:nowrap;}
tbody td{padding:14px 16px;font-size:var(--font-sm);color:var(--text-primary);border-bottom:1px solid var(--border);white-space:nowrap;}
tbody tr:last-child td{border-bottom:none;}
tbody tr:hover{background:#F8FBFF;}
.recharge-table input[type="checkbox"]{cursor:pointer;width:16px;height:16px;vertical-align:middle;}
.recharge-table .balance-before{color:var(--text-secondary);}
.recharge-table .balance-after{color:var(--primary);font-weight:600;}
.recharge-table .balance-arrow{color:var(--text-placeholder);padding:0 4px;}
.recharge-table .row-empty{text-align:center;padding:40px 24px;color:var(--text-placeholder);font-size:var(--font-sm);}
.tag{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:var(--font-xs);}
.tag-blue{background:var(--primary-light);color:var(--primary);}
.tag-gray{background:#F5F5F5;color:#999;}
.tag-purple{background:#F9F0FF;color:#722ED1;}
.pagination{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;gap:8px;border-top:1px solid var(--border);}
.pagination__info{font-size:var(--font-sm);color:var(--text-secondary);}
.pagination__pages{display:flex;align-items:center;gap:8px;}
.page-btn{min-width:32px;height:32px;padding:0 8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#fff;cursor:pointer;font-size:var(--font-sm);display:inline-flex;align-items:center;justify-content:center;transition:all .2s;}
.page-btn:hover:not(.disabled):not(.active){color:var(--primary);border-color:var(--primary);}
.page-btn.active{background:var(--primary);color:#fff;border-color:var(--primary);}
.page-btn.disabled{color:var(--text-placeholder);cursor:not-allowed;background:#FAFAFA;}
/* 已选数量标签(主页表格标题处复用) */
.view-switcher__count{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:18px;padding:0 6px;background:var(--primary-light);color:var(--primary);border-radius:9px;font-size:var(--font-xs);margin-left:4px;vertical-align:middle;}
/* 全选按钮样式(button 元素) */
.select-all-btn{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 14px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#fff;cursor:pointer;font-size:var(--font-sm);color:var(--text-primary);transition:all .2s;}
.select-all-btn:hover{color:var(--primary);border-color:var(--primary);}
.select-all-btn input{margin:0;cursor:pointer;width:15px;height:15px;pointer-events:none;}
.select-all-btn.is-all-selected{color:var(--primary);border-color:var(--primary);background:var(--primary-light);}
.select-all-btn__count{color:var(--text-secondary);font-size:var(--font-xs);margin-left:2px;}
/* 已选清单行操作(移除单个) */
.recharge-table .td-remove{font-size:var(--font-sm);}
.recharge-table .remove-link{color:var(--danger);cursor:pointer;}
.recharge-table .remove-link:hover{text-decoration:underline;}
.recharge-table .row-empty{text-align:center;padding:40px 24px;color:var(--text-placeholder);font-size:var(--font-sm);}
/* 选择用户弹窗 */
.modal-mask{position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:1000;display:none;align-items:center;justify-content:center;}
.modal-mask.open{display:flex;}
.modal{width:1080px;max-width:calc(100vw - 48px);max-height:calc(100vh - 96px);background:#fff;border-radius:var(--radius-md);box-shadow:var(--shadow-md);display:flex;flex-direction:column;overflow:hidden;}
.modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);}
.modal__title{font-size:var(--font-lg);font-weight:600;color:var(--text-primary);}
.modal__close{font-size:var(--font-lg);color:var(--text-secondary);cursor:pointer;line-height:1;}
.modal__close:hover{color:var(--text-primary);}
.modal__body{padding:16px 24px;overflow-y:auto;flex:1;}
.modal__body .filter-bar{box-shadow:none;padding:0;margin-bottom:12px;background:#FAFAFA;border-radius:var(--radius-sm);}
.modal__body .table-card{box-shadow:none;border:1px solid var(--border);}
.modal__footer{display:flex;align-items:center;gap:12px;padding:16px 24px;border-top:1px solid var(--border);background:#FAFAFA;}
.modal__footer-info{flex:1;font-size:var(--font-sm);color:var(--text-secondary);}
.modal__footer-info b{color:var(--primary);font-weight:600;margin:0 2px;}
/* Toast */
.toast{position:fixed;top:80px;left:50%;transform:translateX(-50%) translateY(-20px);background:rgba(0,0,0,.8);color:#fff;padding:10px 24px;border-radius:var(--radius-sm);font-size:var(--font-sm);z-index:9999;opacity:0;pointer-events:none;transition:all .25s;}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
</style>
</head>
<body>
<!-- 顶部导航 -->
<header class="top-bar">
<div class="top-bar__logo">
<svg viewBox="0 0 28 28" fill="none"><rect width="28" height="28" rx="6" fill="#1890FF"/><path d="M7 14h14M14 7v14" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/></svg>
营养管理
</div>
<nav class="top-bar__nav">
<a href="trace-query.html">溯源总览</a>
<a href="arc-employee.html">档案</a>
<a href="farm-land.html">绿色种采</a>
<a href="sup-hygiene-check.html">卫生供应</a>
<a href="prod-qc-daily.html">健康生产</a>
<a href="din-dict-mgmt.html" class="active">营养用餐</a>
<a href="ano-dirty.html">异常事件</a>
</nav>
<div class="top-bar__right">
<a class="btn-back" href="../../index.html">← 返回首页</a>
<div class="avatar"></div>
<span>系统管理员 ▾</span>
</div>
</header>
<!-- 侧边栏 -->
<aside class="sidebar">
<div class="sidebar-menu__title">营养用餐</div>
<a href="din-canteen.html" class="sidebar-item"><span class="mi">🏢</span>场所管理</a>
<a href="din-dict-mgmt.html" class="sidebar-item"><span class="mi">📖</span>字典管理</a>
<a href="prod-qc-nutrition.html" class="sidebar-item"><span class="mi">🔬</span>营养检验</a>
<a href="din-ingredient.html" class="sidebar-item"><span class="mi">🥦</span>食物成分</a>
<a href="din-dish-all.html" class="sidebar-item"><span class="mi">🍲</span>餐品总库</a>
<a href="din-dish-own.html" class="sidebar-item"><span class="mi">📦</span>自有餐品库</a>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
📐 算法标准
</div>
<div class="sidebar-subgroup__body">
<a href="din-algo-population.html" class="sidebar-item"><span class="mi">👥</span>人群基础标准</a>
<a href="din-algo-vip.html" class="sidebar-item"><span class="mi">💎</span>VIP定制标准</a>
</div>
</div>
<a href="din-recipe-mgmt.html" class="sidebar-item"><span class="mi">📋</span>食谱管理</a>
<a href="din-order-meal.html" class="sidebar-item"><span class="mi">🛒</span>营养点餐</a>
<a href="din-user-mgmt.html" class="sidebar-item active"><span class="mi">🧑</span>用户管理</a>
<a href="din-order-line.html" class="sidebar-item"><span class="mi">⚖️</span>餐线管理</a>
<a href="din-order-pickup.html" class="sidebar-item"><span class="mi">🍽️</span>取餐管理</a>
<a href="din-data-consume.html" class="sidebar-item"><span class="mi">💰</span>消费管理</a>
<a href="din-data-nutri.html" class="sidebar-item"><span class="mi">📊</span>营养管理</a>
<a href="din-data-benefit.html" class="sidebar-item"><span class="mi">📈</span>餐材效益</a>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
🖥️ 终端管理
</div>
<div class="sidebar-subgroup__body">
<a href="din-terminal.html" class="sidebar-item"><span class="mi">📟</span>设备管理</a>
<a href="din-content-center.html" class="sidebar-item"><span class="mi">📦</span>内容中心</a>
</div>
</div>
<div class="sidebar-subgroup">
<div class="sidebar-subgroup__header" onclick="toggleSubgroup(this)">
<svg class="arrow" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke-width="1.5"/></svg>
🤖 模型管理
</div>
<div class="sidebar-subgroup__body">
<a href="din-model-llm.html" class="sidebar-item"><span class="mi">🧠</span>大模型管理</a>
<a href="din-model-small.html" class="sidebar-item"><span class="mi">⚙️</span>小模型管理</a>
</div>
</div>
<a href="din-innovation.html" class="sidebar-item"><span class="mi">🌟</span>味养创新</a>
</aside>
<!-- 主内容区 -->
<div class="main">
<!-- 页面信息栏 -->
<div class="page-header-bar">
<div class="page-header-bar__left">
<a href="din-user-mgmt.html" class="btn btn-default">← 返回用户管理</a>
<span class="page-header-bar__title">批量充值</span>
<span class="page-header-bar__meta">个人账户 · 单笔金额对所有勾选用户生效</span>
</div>
<div class="page-header-bar__right"></div>
</div>
<!-- 充值配置区(位于列表上方) -->
<div class="recharge-config-card">
<div class="recharge-config-card__title">💰 充值配置</div>
<div class="recharge-config__row">
<div class="recharge-config__item">
<label>充值金额(每人)<span style="color:var(--danger);">*</span></label>
<div class="recharge-config__amount">
<span class="currency">¥</span>
<input type="number" id="rcAmount" placeholder="0.00" min="0.01" step="0.01" oninput="onAmountChange()">
</div>
</div>
<div class="recharge-config__item">
<label>支付方式<span style="color:var(--danger);">*</span></label>
<select id="rcMethod">
<option value="现金支付">现金支付</option>
<option value="微信支付">微信支付</option>
<option value="支付宝支付">支付宝支付</option>
</select>
</div>
<div class="recharge-config__item">
<label>备注</label>
<input type="text" id="rcRemark" placeholder="选填,例如:2026年5月餐补">
</div>
</div>
<div class="recharge-summary-bar">
<div class="recharge-summary" id="rcSummary">
<div class="recharge-summary__count">未选择任何用户</div>
</div>
<div class="recharge-summary-bar__actions">
<a href="din-user-mgmt.html" class="btn btn-default">取消</a>
<button class="btn btn-primary" id="rcSubmit" onclick="submitRecharge()" disabled>确认充值</button>
</div>
</div>
</div>
<!-- 已选用户清单(主页表格:仅展示已勾选的用户) -->
<div class="table-card">
<div class="table-card__header">
<div class="table-card__title">已选用户清单 <span id="rcSelectedCountTag" class="view-switcher__count">0</span></div>
<div class="table-card__actions">
<button class="btn btn-primary" onclick="openUserPicker()"> 选择用户</button>
<button class="btn btn-default" id="rcClearAllBtn" onclick="clearAllSelected()">🗑 清空已选</button>
</div>
</div>
<div class="table-card__content">
<table class="recharge-table">
<thead>
<tr>
<th>姓名</th>
<th>手机号</th>
<th>用户编号</th>
<th>人员类别</th>
<th>单位部门</th>
<th style="width:220px;">充值前后余额</th>
<th style="width:80px;">操作</th>
</tr>
</thead>
<tbody id="rcTableBody"></tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination">
<div class="pagination__info" id="rcPageInfo"></div>
<div class="pagination__pages" id="rcPageBtns"></div>
</div>
</div>
</div>
<!-- 选择用户弹窗 -->
<div class="modal-mask" id="pickerMask">
<div class="modal">
<div class="modal__header">
<span class="modal__title">选择用户</span>
<span class="modal__close" onclick="closeUserPicker()"></span>
</div>
<div class="modal__body">
<!-- 筛选区 -->
<div class="filter-bar">
<div class="filter-item"><label>姓名</label><input class="filter-input" type="text" id="rcFname" placeholder="请输入姓名" oninput="onFilterChange()"></div>
<div class="filter-item"><label>用户编号</label><input class="filter-input" type="text" id="rcFcode" placeholder="请输入编号" oninput="onFilterChange()"></div>
<div class="filter-item">
<label>单位部门</label>
<div class="cascader" id="rcCascader">
<div class="cascader__trigger" onclick="toggleCascader(event)"><span class="cascader__text">全部</span><span class="cascader__arrow"></span></div>
<div class="cascader__panel">
<div class="cascader__col cascader__col--left" id="cascaderLeft"></div>
<div class="cascader__col cascader__col--right" id="cascaderRight"></div>
</div>
</div>
</div>
<div class="filter-item"><label>人员类别</label>
<select class="filter-select" id="rcFcat" onchange="onFilterChange()">
<option value="">全部</option>
<option>会员用户</option>
<option>普通用户</option>
<option>员工</option>
<option>工作人员</option>
<option>外部人员</option>
</select>
</div>
<button class="btn btn-default" onclick="resetFilter()">重置</button>
</div>
<!-- 候选用户表 -->
<div class="table-card">
<div class="table-card__header">
<div class="table-card__title">候选用户</div>
<div class="table-card__actions">
<button class="select-all-btn" id="rcSelectAllBtn" onclick="onSelectAllFiltered()">
<input type="checkbox" id="selectAllCb">
<span id="rcSelectAllText">全选所有符合条件</span>
<span class="select-all-btn__count" id="selectAllCbCount"></span>
</button>
</div>
</div>
<div class="table-card__content">
<table class="recharge-table">
<thead>
<tr>
<th style="width:48px;"></th>
<th>姓名</th>
<th>手机号</th>
<th>用户编号</th>
<th>人员类别</th>
<th>单位部门</th>
<th>余额</th>
</tr>
</thead>
<tbody id="rcPickerBody"></tbody>
</table>
</div>
<div class="pagination">
<div class="pagination__info" id="rcPickerPageInfo"></div>
<div class="pagination__pages" id="rcPickerPageBtns"></div>
</div>
</div>
</div>
<div class="modal__footer">
<span class="modal__footer-info">本次将加入 <b id="rcPickerPending">0</b></span>
<button class="btn btn-default" onclick="closeUserPicker()">取消</button>
<button class="btn btn-primary" onclick="confirmPicker()">确定加入</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
/* ========== 侧边栏折叠 ========== */
function toggleSubgroup(header){header.parentElement.classList.toggle('open');}
(function(){var active=document.querySelector('.sidebar-item.active');if(!active)return;var sub=active.closest('.sidebar-subgroup');if(sub)sub.classList.add('open');})();
/* ========== 级联选择器 ========== */
var CASCADER_DATA={
'总部机关':['运维部','后勤部','研发部','市场部','运营部','财务部','人事部','行政部','销售部'],
'物业中心':['安保部','保洁部','绿化部','维修部'],
'外部单位':['访客组','合作方']
};
var cascaderEl=document.getElementById('rcCascader');
var cascaderLeft=document.getElementById('cascaderLeft');
var cascaderRight=document.getElementById('cascaderRight');
var selectedUnit='',selectedDept='';
function renderCascaderLeft(){
var html='<div class="cascader__col-item'+(selectedUnit===''?' active':'')+'" data-val="">全部</div>';
Object.keys(CASCADER_DATA).forEach(function(u){
html+='<div class="cascader__col-item'+(selectedUnit===u?' active':'')+'" data-val="'+u+'">'+u+'</div>';
});
cascaderLeft.innerHTML=html;
cascaderLeft.querySelectorAll('.cascader__col-item').forEach(function(item){
item.onclick=function(e){e.stopPropagation();selectUnit(this.dataset.val);};
});
}
function renderCascaderRight(){
if(!selectedUnit){cascaderRight.innerHTML='';return;}
var depts=CASCADER_DATA[selectedUnit]||[];
var html='<div class="cascader__col-item'+(selectedDept===''?' active':'')+'" data-val="">全部</div>';
depts.forEach(function(d){
html+='<div class="cascader__col-item'+(selectedDept===d?' active':'')+'" data-val="'+d+'">'+d+'</div>';
});
cascaderRight.innerHTML=html;
cascaderRight.querySelectorAll('.cascader__col-item').forEach(function(item){
item.onclick=function(e){e.stopPropagation();selectDept(this.dataset.val);};
});
}
function selectUnit(val){
selectedUnit=val;selectedDept='';
rcFilter.unit=val;rcFilter.dept='';
renderCascaderLeft();renderCascaderRight();
rcPickerPage=1;
renderPickerTable();renderPickerPagination();syncPickerUI();
if(!val){updateCascaderText();closeCascader();}
}
function selectDept(val){
selectedDept=val;rcFilter.dept=val;
renderCascaderRight();updateCascaderText();closeCascader();
rcPickerPage=1;
renderPickerTable();renderPickerPagination();syncPickerUI();
}
function updateCascaderText(){
var txt='全部';
if(selectedUnit&&selectedDept)txt=selectedUnit+'/'+selectedDept;
else if(selectedUnit)txt=selectedUnit;
cascaderEl.querySelector('.cascader__text').textContent=txt;
}
function toggleCascader(e){
if(e)e.stopPropagation();
var isOpen=cascaderEl.classList.contains('open');
if(isOpen){closeCascader();}else{renderCascaderLeft();renderCascaderRight();cascaderEl.classList.add('open');}
}
function closeCascader(){cascaderEl.classList.remove('open');}
document.addEventListener('click',function(e){if(!cascaderEl.contains(e.target))closeCascader();});
/* ========== 数据源 ========== */
// 候选用户池(示例数据:覆盖分页、人员类别、单位部门等场景)
var rcUsers = [
{name:'张伟', phone:'13812345601', code:'EMP003', category:'会员用户', unit:'', dept:'', balance:256.50},
{name:'李娜', phone:'13912345602', code:'EMP001', category:'会员用户', unit:'', dept:'', balance:512.00},
{name:'王磊', phone:'18812345603', code:'EMP002', category:'普通用户', unit:'', dept:'', balance:0},
{name:'赵敏', phone:'13712345604', code:'EMP004', category:'会员用户', unit:'', dept:'', balance:320.00},
{name:'陈刚', phone:'15812345605', code:'EMP005', category:'普通用户', unit:'', dept:'', balance:0},
{name:'刘芳', phone:'13612345606', code:'EMP006', category:'会员用户', unit:'', dept:'', balance:678.00},
{name:'孙磊', phone:'18612345607', code:'EMP007', category:'普通用户', unit:'', dept:'', balance:0},
{name:'钱芳', phone:'15912345608', code:'EMP008', category:'会员用户', unit:'', dept:'', balance:0},
{name:'张运维', phone:'13512345609', code:'EMP009', category:'员工', unit:'总部机关', dept:'运维部', balance:0},
{name:'李后勤', phone:'18712345610', code:'EMP010', category:'员工', unit:'总部机关', dept:'后勤部', balance:0},
{name:'王保安', phone:'15612345611', code:'WK003', category:'工作人员', unit:'', dept:'', balance:0},
{name:'赵清洁', phone:'18912345612', code:'WK004', category:'工作人员', unit:'', dept:'', balance:0},
{name:'林教授', phone:'13312345613', code:'EXT001', category:'外部人员', unit:'外部单位', dept:'访客组', balance:0}
];
/* ========== 状态:已选清单 / 弹窗筛选 / 弹窗勾选 / 分页 ========== */
var rcSelected={}; // 主页已选清单 { code: true },跨弹窗持久保留
var rcFilter={name:'', code:'', category:'', unit:'', dept:''}; // 弹窗内筛选条件
var rcPickerChecked={}; // 弹窗内勾选状态 { code: true },仅本会话生效
var rcMainPage=1; // 主页已选清单分页
var rcPickerPage=1; // 弹窗候选表分页
var rcPageSize=10;
/* ========== 筛选(弹窗内) ========== */
function onFilterChange(){
rcFilter.name=document.getElementById('rcFname').value.trim();
rcFilter.code=document.getElementById('rcFcode').value.trim();
rcFilter.category=document.getElementById('rcFcat').value;
rcPickerPage=1;
renderPickerTable();
renderPickerPagination();
syncPickerUI();
}
function resetFilter(){
rcFilter={name:'', code:'', category:'', unit:'', dept:''};
selectedUnit='';selectedDept='';
document.getElementById('rcFname').value='';
document.getElementById('rcFcode').value='';
document.getElementById('rcFcat').value='';
renderCascaderLeft();renderCascaderRight();updateCascaderText();
rcPickerPage=1;
renderPickerTable();
renderPickerPagination();
syncPickerUI();
}
function getFilteredUsers(){
return rcUsers.filter(function(u){
if(rcFilter.name && u.name.indexOf(rcFilter.name)<0) return false;
if(rcFilter.code && u.code.toLowerCase().indexOf(rcFilter.code.toLowerCase())<0) return false;
if(rcFilter.category && u.category!==rcFilter.category) return false;
if(rcFilter.unit && u.unit!==rcFilter.unit) return false;
if(rcFilter.dept && u.dept!==rcFilter.dept) return false;
return true;
});
}
function getSelectedUsers(){
return rcUsers.filter(function(u){return rcSelected[u.code];});
}
/* ========== 弹窗:打开 / 关闭 / 确定 / 取消 ========== */
function openUserPicker(){
// 打开时清空勾选与筛选,展示完整候选池
rcPickerChecked={};
rcFilter={name:'', code:'', category:'', unit:'', dept:''};
selectedUnit='';selectedDept='';
document.getElementById('rcFname').value='';
document.getElementById('rcFcode').value='';
document.getElementById('rcFcat').value='';
renderCascaderLeft();renderCascaderRight();updateCascaderText();
rcPickerPage=1;
document.getElementById('pickerMask').classList.add('open');
renderPickerTable();
renderPickerPagination();
syncPickerUI();
}
function closeUserPicker(){
document.getElementById('pickerMask').classList.remove('open');
}
// 确定加入:增量合并到已选清单(仅在主页已选基础上新增,不剔除已存在项)
function confirmPicker(){
var added=0;
Object.keys(rcPickerChecked).forEach(function(code){
if(!rcSelected[code]){
rcSelected[code]=true;
added++;
}
});
if(added>0){
showToast('已加入 '+added+' 名用户');
}
document.getElementById('pickerMask').classList.remove('open');
// 主页页码溢出修正
var pages=Math.max(1, Math.ceil(getSelectedUsers().length/rcPageSize));
if(rcMainPage>pages) rcMainPage=pages;
renderMainTable();
renderMainPagination();
updateSummary();
}
// 弹窗底部信息:本次将加入的人数(仅统计尚未在主页已选清单中的)
function syncPickerUI(){
var added=0;
Object.keys(rcPickerChecked).forEach(function(code){
if(!rcSelected[code]) added++;
});
document.getElementById('rcPickerPending').textContent=added;
}
/* ========== 分页 ========== */
function gotoMainPage(p){rcMainPage=p;renderMainTable();renderMainPagination();}
function gotoPickerPage(p){rcPickerPage=p;renderPickerTable();renderPickerPagination();syncPickerUI();}
function renderMainPagination(){
var list=getSelectedUsers();
var total=list.length;
var pages=Math.max(1, Math.ceil(total/rcPageSize));
var start=(rcMainPage-1)*rcPageSize+1;
var end=Math.min(rcMainPage*rcPageSize, total);
document.getElementById('rcPageInfo').textContent=
total===0 ? ('共 0 条') : ('显示第 '+start+'-'+end+' 条,共 '+total+' 条');
var html='';
html+='<button class="page-btn '+(rcMainPage===1?'disabled':'')+'" '+(rcMainPage===1?'':'onclick="gotoMainPage('+(rcMainPage-1)+')"')+'></button>';
for(var i=1;i<=pages;i++){
html+='<button class="page-btn '+(i===rcMainPage?'active':'')+'" onclick="gotoMainPage('+i+')">'+i+'</button>';
}
html+='<button class="page-btn '+(rcMainPage===pages?'disabled':'')+'" '+(rcMainPage===pages?'':'onclick="gotoMainPage('+(rcMainPage+1)+')"')+'</button>';
document.getElementById('rcPageBtns').innerHTML=html;
}
function renderPickerPagination(){
var list=getFilteredUsers();
var total=list.length;
var pages=Math.max(1, Math.ceil(total/rcPageSize));
var start=(rcPickerPage-1)*rcPageSize+1;
var end=Math.min(rcPickerPage*rcPageSize, total);
document.getElementById('rcPickerPageInfo').textContent=
total===0 ? ('共 0 条') : ('显示第 '+start+'-'+end+' 条,共 '+total+' 条');
var html='';
html+='<button class="page-btn '+(rcPickerPage===1?'disabled':'')+'" '+(rcPickerPage===1?'':'onclick="gotoPickerPage('+(rcPickerPage-1)+')"')+'></button>';
for(var i=1;i<=pages;i++){
html+='<button class="page-btn '+(i===rcPickerPage?'active':'')+'" onclick="gotoPickerPage('+i+')">'+i+'</button>';
}
html+='<button class="page-btn '+(rcPickerPage===pages?'disabled':'')+'" '+(rcPickerPage===pages?'':'onclick="gotoPickerPage('+(rcPickerPage+1)+')"')+'</button>';
document.getElementById('rcPickerPageBtns').innerHTML=html;
}
/* ========== 主页表格(已选清单) ========== */
function renderMainTable(){
var list=getSelectedUsers();
var start=(rcMainPage-1)*rcPageSize;
var pageItems=list.slice(start, start+rcPageSize);
var amount=getCurrentAmount();
var tbody=document.getElementById('rcTableBody');
// 标题处数量标签
document.getElementById('rcSelectedCountTag').textContent=list.length;
// 清空按钮:无已选时禁用
document.getElementById('rcClearAllBtn').disabled=(list.length===0);
document.getElementById('rcClearAllBtn').style.opacity=(list.length===0)?0.5:1;
document.getElementById('rcClearAllBtn').style.cursor=(list.length===0)?'not-allowed':'pointer';
if(pageItems.length===0){
tbody.innerHTML='<tr><td colspan="7" class="row-empty">尚未选择任何用户,点击右上「选择用户」开始添加</td></tr>';
return;
}
var html='';
pageItems.forEach(function(u){
var afterBalance=(u.balance+amount).toFixed(2);
var balanceCell='<span class="balance-before">¥'+u.balance.toFixed(2)+'</span>'
+ '<span class="balance-arrow">→</span>'
+ '<span class="balance-after">¥'+afterBalance+'</span>';
var deptText=(u.unit&&u.dept)?(u.unit+'/'+u.dept):(u.unit||'—');
var catTag=categoryTag(u.category);
html+='<tr>'
+ '<td>'+u.name+'</td>'
+ '<td>'+(u.phone||'—')+'</td>'
+ '<td>'+u.code+'</td>'
+ '<td>'+catTag+'</td>'
+ '<td>'+deptText+'</td>'
+ '<td>'+balanceCell+'</td>'
+ '<td class="td-remove"><span class="remove-link" onclick="removeSelected(\''+u.code+'\')">移除</span></td>'
+ '</tr>';
});
tbody.innerHTML=html;
}
/* ========== 弹窗候选表 ========== */
function renderPickerTable(){
var filtered=getFilteredUsers();
var start=(rcPickerPage-1)*rcPageSize;
var pageItems=filtered.slice(start, start+rcPageSize);
var tbody=document.getElementById('rcPickerBody');
// 全选按钮状态:基于所有筛选命中是否全部已在弹窗勾选
var allChecked=filtered.length>0 && filtered.every(function(u){return rcPickerChecked[u.code];});
document.getElementById('selectAllCb').checked=allChecked;
document.getElementById('rcSelectAllBtn').classList.toggle('is-all-selected', allChecked);
document.getElementById('rcSelectAllText').textContent=allChecked?'已全选 '+filtered.length+' 人,点击取消':'全选所有符合条件';
document.getElementById('selectAllCbCount').textContent=''+filtered.length+'';
if(pageItems.length===0){
tbody.innerHTML='<tr><td colspan="7" class="row-empty">暂无符合条件的用户</td></tr>';
return;
}
var html='';
pageItems.forEach(function(u){
var checked=rcPickerChecked[u.code]?'checked':'';
var deptText=(u.unit&&u.dept)?(u.unit+'/'+u.dept):(u.unit||'—');
var catTag=categoryTag(u.category);
html+='<tr>'
+ '<td><input type="checkbox" '+checked+' onchange="onPickerRow(\''+u.code+'\', this.checked)"></td>'
+ '<td>'+u.name+'</td>'
+ '<td>'+(u.phone||'—')+'</td>'
+ '<td>'+u.code+'</td>'
+ '<td>'+catTag+'</td>'
+ '<td>'+deptText+'</td>'
+ '<td><span class="balance-before">¥'+u.balance.toFixed(2)+'</span></td>'
+ '</tr>';
});
tbody.innerHTML=html;
}
function categoryTag(cat){
if(cat==='会员用户') return '<span class="tag tag-blue">会员用户</span>';
if(cat==='员工') return '<span class="tag tag-blue">员工</span>';
if(cat==='外部人员') return '<span class="tag tag-purple">外部人员</span>';
return '<span class="tag tag-gray">'+cat+'</span>';
}
/* ========== 弹窗勾选:单条 / 全选所有符合条件 ========== */
function onPickerRow(code, checked){
if(checked) rcPickerChecked[code]=true;
else delete rcPickerChecked[code];
renderPickerTable();
syncPickerUI();
}
// 全选/取消全选:作用于"所有符合筛选条件的用户"(不限于当前页)
function onSelectAllFiltered(){
var filtered=getFilteredUsers();
var allChecked=filtered.length>0 && filtered.every(function(u){return rcPickerChecked[u.code];});
if(allChecked){
filtered.forEach(function(u){delete rcPickerChecked[u.code];});
}else{
filtered.forEach(function(u){rcPickerChecked[u.code]=true;});
}
renderPickerTable();
syncPickerUI();
}
/* ========== 主页:移除 / 清空 ========== */
function removeSelected(code){
delete rcSelected[code];
// 主页页码溢出修正
var pages=Math.max(1, Math.ceil(getSelectedUsers().length/rcPageSize));
if(rcMainPage>pages) rcMainPage=pages;
renderMainTable();
renderMainPagination();
updateSummary();
}
function clearAllSelected(){
var count=getSelectedUsers().length;
if(count===0) return;
rcSelected={};
rcMainPage=1;
showToast('已清空 '+count+' 名已选用户');
renderMainTable();
renderMainPagination();
updateSummary();
}
/* ========== 金额与汇总 ========== */
function getCurrentAmount(){
var v=document.getElementById('rcAmount').value;
var n=parseFloat(v);
return (isNaN(n)?0:n);
}
function onAmountChange(){
renderMainTable();
updateSummary();
}
function updateSummary(){
var amount=getCurrentAmount();
var selected=getSelectedUsers();
var count=selected.length;
var totalBefore=selected.reduce(function(s,u){return s+u.balance;},0);
var totalAfter=totalBefore+amount*count;
var method=document.getElementById('rcMethod').value;
var summaryEl=document.getElementById('rcSummary');
if(count===0){
summaryEl.innerHTML='<div class="recharge-summary__count">未选择任何用户</div>';
}else{
summaryEl.innerHTML=''
+ '<div class="recharge-summary__count">已选 <b>'+count+'</b> 人 · '+method+' · 单笔 <b>¥'+amount.toFixed(2)+'</b></div>'
+ '<div class="recharge-summary__total">'
+ ' <span class="recharge-summary__total-label">合计充值:</span>'
+ ' <span class="recharge-summary__total-before">¥'+totalBefore.toFixed(2)+'</span>'
+ ' <span class="recharge-summary__total-arrow">→</span>'
+ ' <span class="recharge-summary__total-after">¥'+totalAfter.toFixed(2)+'</span>'
+ '</div>';
}
// 提交按钮:需选了人 + 填了金额(已选清单即核对,无需额外防呆)
var submitBtn=document.getElementById('rcSubmit');
var valid=count>0 && amount>0;
submitBtn.disabled=!valid;
submitBtn.textContent='确认充值';
}
/* ========== 提交 ========== */
function submitRecharge(){
var amount=getCurrentAmount();
if(amount<=0){ showToast('请输入有效的充值金额'); return; }
var count=getSelectedUsers().length;
if(count===0){ showToast('请至少选择一个用户'); return; }
var method=document.getElementById('rcMethod').value;
var totalAdd=(amount*count).toFixed(2);
showToast('批量充值成功!已为 '+count+' 名用户充值 '+method+' 共 ¥'+totalAdd);
// 提交后清空已选
rcSelected={};
rcMainPage=1;
document.getElementById('rcAmount').value='';
renderMainTable();
renderMainPagination();
updateSummary();
}
/* ========== Toast ========== */
function showToast(msg){
var el=document.getElementById('toast');
el.textContent=msg;
el.classList.add('show');
setTimeout(function(){el.classList.remove('show');},2200);
}
/* ========== 初始化 ========== */
renderCascaderLeft();
renderCascaderRight();
updateCascaderText();
renderMainTable();
renderMainPagination();
updateSummary();
</script>
</body>
</html>
+40 -1
View File
@@ -1,14 +1,53 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-06]
### 后台营养管理
- **`din-user-recharge.html`**(新增 · 批量充值):从 `din-user-mgmt.html` 抽离为独立页面。
- **背景**:原批量充值以弹窗形式内嵌于用户管理页,CSS/JS 体量大(≈800 行),导致 `din-user-mgmt.html` 臃肿且职责混杂。现按「独立文件粒度」约束拆为独立原型页。
- **迁移内容**:用户筛选区(关键字/部门/账户状态级联)、候选用户表(多选 + 充值前余额 → 充值后余额预览)、底部配置区(充值金额、支付方式级联、备注)、汇总栏(选中人数、合计变动)、提交校验。`index.html` SITE_MAP 在「用户管理」后追加「批量充值」入口。
- **`din-user-mgmt.html`**(用户管理):清理批量充值弹窗。
- **删除**`.batch-recharge-modal` 相关全部 CSS、`openBatchRecharge()` / `renderBr*` / `toggleBr*` / `submitBatchRecharge()` 等全部 JS、`Modal.close` 中的级联监听清理代码、`batchRechargeUsers` 数据池、候选用户表格 HTML。
- **替换**:操作栏「💰 批量充值」按钮由 `onclick="openBatchRecharge()"` 改为 `<a href="din-user-recharge.html">` 跳转。
- **`din-user-account.html`**(用户账户-充值弹窗):支付方式精简。
- 删除「银行转账」「后台挂账」两个 option,保留「微信支付 / 现金支付 / 支付宝支付」三种。
- **`me-account.html`**(小程序 · 我的-账号信息):姓名行增加可编辑态。
- 与手机号行保持一致:加 `editable` 类与 `onclick` 提示,右侧展示 `✏️` 编辑图标。
- **`din-dish-add.html`**(餐品档案-新增):修复「成本核算」按钮无响应问题 + 交互优化。
- **根因**`calcCostPrice()` 在熟重或食材成本为 0 时仅 alert 提示并 `return`,未回显任何核算内容,导致新增页面(默认无食材/熟重)点击后看起来「没反应」。
- **修复方案**:保留数据完整时的真实计算逻辑,新增演示数据 fallback——当熟重 ≤ 0 默认 350g、食材成本 ≤ 0 默认 ¥12.00、食材列表为空时使用 4 条演示食材(鸡胸肉/西兰花/橄榄油/食盐),始终在按钮下方回显成本金额、可点击的小问号明细弹窗、「应用到称重基础价」按钮。
- **布局调整**:「🧮 成本核算」按钮从单独 form-group(视觉居中偏下)改为与「食材成本」输入框同组的内嵌按钮(`.input-with-btn` 容器,input 自适应 + 按钮紧贴右侧),使其真正跟随三个输入框。「应用到称重基础价」按钮从 `margin-left:auto` 右对齐改为紧跟小问号 `.cost-help` 后面(移除 `.cost-result__apply { margin-left:auto }`)。明细弹窗 `.cost-detail-pop` 定位调整为 `top:100%;left:0` 锚定到成本价行下方。
- **交互精简**:移除 `calcCostPrice()` 中的演示数据 alert 提示,移除 `applyCostToBasePrice()` 中成功应用 alert;两个按钮点击均改为静默操作(应用结果直接写入 `basePriceInput`)。
## [2026-08-05]
### 后台营养管理
- **`din-dish-add.html` / `din-dish-own.html` / `din-dish-all.html`**(餐品档案):定价区改造 + 营养指标样式精简 + 营养等级移除。
- **餐品定价区去特殊化**:原蓝色渐变 `.pricing-section` / `.pricing-card` 卡片样式废弃,改为与其他类目一致的普通 `.info-section`add 页改为 `.form-subtitle + .form-grid`)。位置由「基本信息下方」上移至「餐品规格」上方,紧邻餐品规格便于价格联动。
- **定价字段精简**:原 4 列(基础价/食材成本/每百克成本/利润率)+ 利润率输入/默认规格重量/建议售价计算行精简为 4 个单元格:称重基础价、利润率、食材成本(只读)、成本核算按钮。原「按利润率自动定价」按钮、「建议售价(默认规格)」、「计算公式」行同步移除。
- **成本核算新交互**:新增「🧮 成本核算」按钮,点击后 `calcCostPrice()` / `calcDetailCost(dishIdx)` 计算 `每百克成本 = 食材成本 ÷ 熟重 × 100` 并展示成本价行(蓝色大字 + 「/100g」单位)。成本价右侧带小问号 `.cost-help`,点击 `toggleCostDetailPop()` 弹出 `.cost-detail-pop` 浮窗,浮窗内含食材成本明细表(食材名称/用量g/单价元/kg/小计元)+ 食材总成本 / 熟重 / 每百克成本三行汇总。成本价右侧带「应用到称重基础价」按钮 `applyCostToBasePrice()` / `applyDetailCost(dishIdx)`,按 `称重基础价 = 每百克成本 × (1 + 利润率%)` 计算并写入 `pricing.basePrice`。详情页点击外部自动关闭浮窗。
- **四大项营养指标样式精简**:原 `.nutri-hero__cell--calorie/protein/carb/fat` 四色渐变(红/绿/蓝/橙 + 左侧 3px 渐变条带)改为统一的 `.nutri-hero__cell`#FAFBFC 浅灰背景 + 1px 浅灰边框)。营养值字号从 24px / 700 调整为 `--font-lg` / 600,去除装饰性元素。
- **营养等级移除**:删除「餐品构成」Tab 下方的「营养等级」字段(add 页 `.form-group` 下拉、详情页营养指标底部「营养等级:」行)。`INNOV.nutriGrade``EDIT_DATA.nutri.nutriGrade` 字段虽保留但已不再渲染。
- **add 页 fillEditData 适配**Step 0 输入框索引重排为 `[0]餐品名称 / [1]过敏原 / [2]称重基础价 / [3]利润率 / [4]食材成本`Step 1 营养指标 20 项由 input readonly 改为 `.form-readonly` / span 只读文本,`fillEditData` 改用 `textContent` 写入。
- **add 页 recalcAddNutri 简化**:删除食材变化时联动 `recalcAddPrice()` 的调用(定价改由按钮触发),仅保留营养指标自动计算 + 食材成本同步。
## [2026-08-04]
### 后台营养管理
- **`din-biz-analysis.html`(经营分析 · 新增页面)**:营养用餐侧边栏新增「数据分析」子菜单组,下设「经营分析」「餐品分析」两项。经营分析页按真实系统报表模块制作:营业日期区间筛选 + 查询/重置;数据总览 4 KPI(成交总金额/成交订单量/成交人数/退款人数,带 ? 帮助图标);交易分析 13 列宽表(按消费区间统计临时/会员用户数、订单数、金额、平均克重、百克单价、客单价);餐次分析/订单分析/支付分析/异常分析四面板(异常分析含人脸图片占位)。同步更新全部 29 个营养用餐分组页面的侧边栏及 `index.html` SITE_MAP。
- **`din-meal-analysis.html`(餐品分析 · 新增页面)**:查询日期 + 餐次筛选;数据总览 5 KPI(成交订单量/用餐人数/开餐总重量(g)/能量总摄入/人均能量摄入);按秤类型多卡片区块(营养秤 + 档口秤×2),每卡内嵌分类筛选 + 11 列可排序明细表(餐品名称/所属秤号/制作重量/实销耗量/异常损耗/挂账耗量/收银删除耗量/剩余重量/下单人数/订单数量/总消耗热量)。
- **`sup-biz-analysis.html`(加工经营分析 · 新增页面)**:卫生供应侧边栏新增「数据分析」子菜单组,下设「经营分析」「加工分析」两项。经营分析页按加工厂场景制作:营业日期筛选 + 数据总览 4 KPI(加工产值/生产批次/原料投入/质检合格率,带 ? 帮助图标);生产分析 9 列宽表(产品分类/生产批次/投料重量/产出重量/原料成本/加工成本/产值/单位成本/毛利率);班次分析/质量分析/成本分析/异常分析四面板。同步更新全部 8 个卫生供应页面的侧边栏及 `index.html` SITE_MAP。
- **`sup-process-analysis.html`(加工分析 · 新增页面)**:查询日期 + 班次筛选;数据总览 5 KPI(生产批次/加工品种/加工总重量(kg)/人均加工量(kg)/产能利用率);按产线多卡片区块(清洗线 + 切配线 + 包装线),每卡内嵌分类筛选 + 11 列可排序明细表(原材料名称/所属产线/投料重量/产出重量/加工损耗/不合格品/返工品/剩余重量/操作人数/生产批次/加工时长)。
- **`din-dish-add.html` / `din-dish-own.html` / `din-dish-all.html`**(餐品档案):4 项核心改造同步落地——宣传视频多视频上传、营养指标自动计算、烹饪工艺精简、餐品定价独立分区。
- **宣传视频多视频上传**:宣传视频区从单个上传区改为「多视频卡片列表」交互模式。新增/编辑页 Step 0 改用 `.promo-video-list` 容器 + 「+ 添加宣传视频」按钮(`addPromoVideoRow()`),每个视频项 `.promo-video-item` 含上传触发区、文件名展示、删除按钮、独立启用开关(`.promo-toggle` 蓝色胶囊);维护临时数组 `promoVideoList` 与渲染函数 `renderPromoVideoList()` / `toggleAddPromoVideo()` / `removePromoVideoRow()` / `triggerPromoVideoUpload()`。详情页「基本档案」Tab 新增 `renderPromoVideosSection(d)` 渲染函数,多视频以 `.promo-video-card` 网格卡片展示,每个卡片右上角独立启用开关 `togglePromoVideo(el, dishIdx, videoIdx)`,section 标题右侧展示「已启用 X / 共 Y 个」统计。数据结构原 `promoVideoEnabled` boolean 字段废弃,改用 `promoVideos: [{name, size, enabled}]` 数组。
- **营养指标自动计算 + 四大项醒目**:营养指标改为不可手填,由食材清单自动累加计算。新增/编辑页 Step 1 营养指标区改为「4 大醒目卡(热量🔥/蛋白质💪/碳水🌾/脂肪🥑)」+ 「查看更多」折叠区(剩余 16 项),所有 input 设为 readonly 并加 🔒 锁图标,顶部提示「ⓘ 营养指标根据食材清单自动计算,不可手动修改」。新增 `recalcAddNutri()` 函数:根据食材分量按比例累加为每百克营养值,并同步 `#ingredientCostInput` 食材成本与触发 `recalcAddPrice()``confirmAddIngredient()` 末尾追加 `recalcAfterIngredientChange()` 联动。详情页「餐品构成」Tab 内嵌营养指标区重构为 `renderNutriSection(d)`4 列 `.nutri-hero` 渐变色卡片(`.nutri-hero__cell--calorie/protein/carb/fat`,红/绿/蓝/橙四色)+ `.nutri-more` 折叠的 18 项次级指标。
- **烹饪工艺精简**:删除「用油量」「用盐量」「出品量」3 个字段。新增/编辑页 Step 2 仅保留主烹饪法/辅烹饪法/火候/烹饪时长/烹饪温度/工艺步骤数 6 项 + 新增「烹饪工艺描述」textarea;`fillEditData` 中 Step 2 的 vals 数组从 9 项缩减为 6 项(mainMethod/subMethod/heat/duration/temp/steps)。详情页 `renderCook(d)` 同步精简为 6 项 info-grid。`EDIT_DATA.cook``INNOV_MAP[*].cook``DISHES[*].cook` 数据字段移除 oil/salt/output。
- **基本档案定价改造**:删除「称重会员价」字段;将「称重基础价」从原 info 单元独立为「餐品定价」类目(`.pricing-section` / `.pricing-card`,蓝色渐变背景),位置上移至「餐品规格」上方。定价区含 4 列只读信息(基础价/食材成本/每百克成本/利润率)+ 计算行:利润率输入框 + 默认规格重量 + 「建议售价(元/份)」展示 + 「按利润率自动定价」按钮。计算公式:**建议售价 = 每百克成本 × (1 + 利润率%) ÷ 100 × 默认规格重量(g)**。新增 `recalcSuggestedPrice()` 实时联动、`applyAutoPrice(dishIdx)``applyAutoPriceToSpec()` 将建议价写入默认规格的 `price` 字段。DISHES 数据新增 `pricing: {basePrice, ingredientCost, per100gCost, profitRate}` 嵌套对象,移除 `weightBasePrice` / `weightVipPrice`
- **演示数据扩展**`din-dish-own.html``din-dish-all.html` 的 5 条餐品示例数据统一补充 `pricing``promoVideos` 字段;`din-dish-add.html``EDIT_DATA` / `INNOV_MAP` 同步补齐新字段结构。
## [2026-08-03]