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:
co-authored by
Claude Opus 4.7
parent
459cd40de2
commit
6a505f2d84
@@ -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>
|
||||
|
||||
@@ -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||'';
|
||||
|
||||
@@ -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');})();
|
||||
|
||||
// 初始化
|
||||
|
||||
@@ -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');})();
|
||||
|
||||
// 初始化
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user