feat: 净菜订单/申领三页统一增加订单金额 + 餐线营养秤菜品展示页新增视频演示模式

- web-admin/nutrition: prod-clean-order/sup-clean-external/sup-clean-internal 统一增加单价/小计字段与订单总金额栏,移除弹窗内汇总预览表格
- canteen-nutrition-scale/02-dish-display: 新增演示模式切换 pill 与视频模式布局(上视频 + 下信息卡)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-05 14:20:21 +08:00
co-authored by Claude Opus 4.7
parent 6c812f8e64
commit d00208a33d
7 changed files with 669 additions and 195 deletions
+27 -3
View File
@@ -7,9 +7,33 @@
<value>
<TabSessionState>
<option name="provider" value="claude" />
<option name="sessionId" value="5ef9ff98-5eb1-4ae5-8460-f461df1cfe53" />
<option name="sessionId" value="2a1427c6-520d-4da9-87a9-28e066647864" />
<option name="cwd" value="$PROJECT_DIR$" />
<option name="model" value="claude-sonnet-4-6" />
<option name="model" value="claude-opus-4-7[1m]" />
<option name="permissionMode" value="bypassPermissions" />
<option name="reasoningEffort" value="high" />
</TabSessionState>
</value>
</entry>
<entry key="1">
<value>
<TabSessionState>
<option name="provider" value="claude" />
<option name="sessionId" value="916a3047-30c5-4f02-a0de-f2e2ba660d52" />
<option name="cwd" value="$PROJECT_DIR$" />
<option name="model" value="claude-opus-4-7[1m]" />
<option name="permissionMode" value="bypassPermissions" />
<option name="reasoningEffort" value="high" />
</TabSessionState>
</value>
</entry>
<entry key="2">
<value>
<TabSessionState>
<option name="provider" value="claude" />
<option name="sessionId" value="eb87e0d9-e045-4412-8eaa-bcecfed97b43" />
<option name="cwd" value="$PROJECT_DIR$" />
<option name="model" value="claude-opus-4-7[1m]" />
<option name="permissionMode" value="bypassPermissions" />
<option name="reasoningEffort" value="high" />
</TabSessionState>
@@ -17,6 +41,6 @@
</entry>
</map>
</option>
<option name="tabCount" value="2" />
<option name="tabCount" value="3" />
</component>
</project>
@@ -175,6 +175,305 @@
visibility: visible;
transform: translateX(-50%) translateY(0);
}
/* ---- 演示模式切换按钮(仅原型演示,右上角简洁 pill) ---- */
.mode-switch {
position: absolute;
top: 20px;
right: 56px;
display: flex;
height: 22px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 6px;
overflow: hidden;
z-index: 10;
}
.mode-switch::before {
content: '演示';
position: absolute;
left: -36px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
color: rgba(255,255,255,0.3);
letter-spacing: 1px;
}
.mode-switch button {
padding: 0 12px;
font-size: 11px;
color: rgba(255,255,255,0.5);
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s;
height: 100%;
font-family: inherit;
}
.mode-switch button:hover {
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.8);
}
.mode-switch button.active {
background: #FF9800;
color: #fff;
}
/* ---- 视频模式布局(纵向:上视频 + 下信息卡) ---- */
.video-mode {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 60px 30px;
gap: 24px;
}
/* 视频模式激活时显示视频布局 */
.main-area.mode-video .video-mode {
display: flex;
}
/* 视频模式激活时隐藏常规模式的标题和菜品信息 */
.main-area.mode-video .center-heading,
.main-area.mode-video .dish-info {
display: none;
}
/* 视频播放器 */
.video-player {
width: 100%;
max-width: 680px;
aspect-ratio: 16 / 9;
border-radius: 12px;
overflow: hidden;
position: relative;
background: #000;
box-shadow: 0 12px 32px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.06);
}
.video-player .video-bg {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
.video-play-btn {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 56px; height: 56px;
background: rgba(0,0,0,0.55);
border: 2px solid rgba(255,255,255,0.85);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
z-index: 2;
}
.video-play-btn:hover {
background: rgba(0,0,0,0.75);
transform: translate(-50%, -50%) scale(1.05);
}
.video-play-btn::after {
content: '';
width: 0; height: 0;
border-left: 16px solid #fff;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
margin-left: 4px;
}
.video-controls {
position: absolute;
bottom: 0; left: 0; right: 0;
padding: 10px 14px;
background: linear-gradient(to top, rgba(0,0,0,0.75), transparent);
display: flex;
align-items: center;
gap: 10px;
font-size: 11px;
color: rgba(255,255,255,0.85);
z-index: 2;
}
.video-progress {
flex: 1;
height: 3px;
background: rgba(255,255,255,0.2);
border-radius: 2px;
position: relative;
}
.video-progress::after {
content: '';
position: absolute;
left: 0; top: 0;
width: 35%;
height: 100%;
background: #FF9800;
border-radius: 2px;
}
.video-time {
font-size: 11px;
letter-spacing: 0.5px;
white-space: nowrap;
}
.video-fullscreen {
font-size: 13px;
cursor: pointer;
}
/* 信息卡 */
.video-info-card {
display: flex;
align-items: center;
gap: 28px;
padding: 14px 36px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
}
.video-info-name {
font-size: 26px;
font-weight: 600;
color: #fff;
}
.video-info-divider {
width: 1px;
height: 28px;
background: rgba(255,255,255,0.15);
}
.video-info-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.video-info-label {
font-size: 11px;
color: rgba(255,255,255,0.4);
letter-spacing: 1px;
}
.video-info-value {
font-size: 18px;
font-weight: 600;
color: #0AC4A8;
}
.video-info-unit {
font-size: 12px;
color: rgba(255,255,255,0.5);
font-weight: 400;
margin-left: 4px;
}
/* ---- 视频模式布局2(横向:左视频 + 右信息卡含标签) ---- */
.video-mode-h {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 40px 50px;
gap: 32px;
}
/* 视频2 模式激活时显示横向布局 */
.main-area.mode-video2 .video-mode-h {
display: flex;
}
/* 视频2 模式激活时隐藏常规元素与纵向视频布局 */
.main-area.mode-video2 .center-heading,
.main-area.mode-video2 .dish-info,
.main-area.mode-video2 .video-mode {
display: none;
}
/* 纵向视频模式激活时也隐藏横向布局 */
.main-area.mode-video .video-mode-h {
display: none;
}
/* 横向布局下的视频播放器(左侧,固定宽度) */
.video-mode-h .video-player {
flex: 0 0 auto;
width: 680px;
max-width: 60%;
}
/* 右侧信息卡(横向布局,纵向排列内容) */
.video-info-card-h {
flex: 1;
min-width: 280px;
max-width: 380px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
padding: 24px 28px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
}
.video-info-name-h {
font-size: 28px;
font-weight: 600;
color: #fff;
line-height: 1.2;
}
/* 餐品标签 chips */
.video-tags-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.video-tag-chip {
display: inline-flex;
align-items: center;
padding: 4px 10px;
font-size: 12px;
color: #0AC4A8;
background: rgba(10,196,168,0.12);
border: 1px solid rgba(10,196,168,0.25);
border-radius: 10px;
letter-spacing: 0.5px;
white-space: nowrap;
}
/* 标签色系变化(按位置循环,增加视觉层次) */
.video-tag-chip:nth-child(3n+2) {
color: #FF9800;
background: rgba(255,152,0,0.10);
border-color: rgba(255,152,0,0.25);
}
.video-tag-chip:nth-child(3n) {
color: #5B9BF8;
background: rgba(91,155,248,0.12);
border-color: rgba(91,155,248,0.25);
}
.video-info-divider-h {
width: 100%;
height: 1px;
background: rgba(255,255,255,0.12);
margin: 4px 0;
}
.video-info-item-h {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
width: 100%;
}
/* 横向布局右侧信息卡中的数值字号加大 */
.video-info-item-h .video-info-value {
font-size: 22px;
}
.video-info-item-h .video-info-label {
font-size: 12px;
}
</style>
</head>
<body>
@@ -185,6 +484,13 @@
<a class="back-btn" id="backBtn" href="javascript:void(0)">&#x25C0; 返回</a> <span>Phone-192.168.1.165:5555</span>
</div>
<!-- 演示模式切换按钮(仅原型演示用,不影响实际业务) -->
<div class="mode-switch" id="modeSwitch">
<button class="active" data-mode="normal">常规</button>
<button data-mode="video">视频</button>
<button data-mode="video2">视频2</button>
</div>
<!-- 设置按钮 -->
<a class="settings-btn" id="settingsBtn" title="设置">&#x2699;</a>
@@ -212,6 +518,72 @@
<div class="dish-hint">点击菜品信息 &#x2192; 进入开餐界面</div>
</div>
</div>
<!-- 视频模式布局(纵向:上视频 + 下信息卡,默认隐藏) -->
<div class="video-mode" id="videoMode">
<!-- 视频播放器 -->
<div class="video-player">
<!-- 视频背景(用 SVG 模拟菜品展示画面) -->
<div class="video-bg" id="videoBg"></div>
<!-- 播放按钮 -->
<div class="video-play-btn" id="videoPlayBtn" title="播放"></div>
<!-- 控制条 -->
<div class="video-controls">
<span style="cursor:pointer;">&#x23EF;</span>
<div class="video-progress"></div>
<span class="video-time">01:23 / 03:45</span>
<span class="video-fullscreen">&#x26F6;</span>
</div>
</div>
<!-- 餐品信息卡 -->
<div class="video-info-card">
<div class="video-info-name" id="videoDishName">宫保鸡丁</div>
<div class="video-info-divider"></div>
<div class="video-info-item">
<span class="video-info-label">单价</span>
<span class="video-info-value"><span id="videoDishPrice">5</span><span class="video-info-unit">元/100克</span></span>
</div>
<div class="video-info-divider"></div>
<div class="video-info-item">
<span class="video-info-label">热量</span>
<span class="video-info-value"><span id="videoDishKcal">158.66</span><span class="video-info-unit">千卡/100克</span></span>
</div>
</div>
</div>
<!-- 视频模式布局2(横向:左视频 + 右信息卡含标签,默认隐藏) -->
<div class="video-mode-h" id="videoModeH">
<!-- 左侧:视频播放器 -->
<div class="video-player">
<div class="video-bg" id="videoBgH"></div>
<div class="video-play-btn" id="videoPlayBtnH" title="播放"></div>
<div class="video-controls">
<span style="cursor:pointer;">&#x23EF;</span>
<div class="video-progress"></div>
<span class="video-time">01:23 / 03:45</span>
<span class="video-fullscreen">&#x26F6;</span>
</div>
</div>
<!-- 右侧:餐品信息卡(含标签) -->
<div class="video-info-card-h">
<!-- 餐品名称 -->
<div class="video-info-name-h" id="videoDishNameH">宫保鸡丁</div>
<!-- 餐品标签 chips -->
<div class="video-tags-row" id="videoTagsRow"></div>
<!-- 分隔线 -->
<div class="video-info-divider-h"></div>
<!-- 单价 -->
<div class="video-info-item-h">
<span class="video-info-label">单价</span>
<span class="video-info-value"><span id="videoDishPriceH">5</span><span class="video-info-unit">元/100克</span></span>
</div>
<!-- 每百克热量 -->
<div class="video-info-item-h">
<span class="video-info-label">每百克热量</span>
<span class="video-info-value"><span id="videoDishKcalH">158.66</span><span class="video-info-unit">千卡/100克</span></span>
</div>
</div>
</div>
</div>
</div>
@@ -223,16 +595,16 @@
/* ===== 餐品数据映射(模拟后端) ===== */
var DISH_MAP = {
1: { name: '红烧肉', price: 5, kcal: 480, colors: ['#8B3A2A', '#D4764E', '#C05030'] },
2: { name: '宫保鸡丁', price: 5, kcal: 420, colors: ['#C87030', '#E8A040', '#D08028'] },
3: { name: '麻婆豆腐', price: 4, kcal: 280, colors: ['#706040', '#B89060', '#807050'] },
4: { name: '清炒时蔬', price: 3, kcal: 120, colors: ['#306028', '#50A040', '#408030'] },
5: { name: '香煎带鱼', price: 6, kcal: 320, colors: ['#805020', '#B88040', '#A06830'] },
6: { name: '糖醋排骨', price: 7, kcal: 520, colors: ['#802018', '#C05030', '#A03820'] },
7: { name: '蒜蓉西兰花', price: 3, kcal: 110, colors: ['#207040', '#40A868', '#308848'] },
8: { name: '米饭', price: 1.5, kcal: 260, colors: ['#D0D0C0', '#E8E8D8', '#E0E0C8'] },
9: { name: '紫菜蛋花汤', price: 2, kcal: 80, colors: ['#503028', '#805040', '#603830'] },
10: { name: '西红柿炒蛋', price: 4, kcal: 180, colors: ['#E8A030', '#F0C040', '#D89020'] }
1: { name: '红烧肉', price: 5, kcal: 480, colors: ['#8B3A2A', '#D4764E', '#C05030'], tags: ['高蛋白', '高脂肪', '富含铁'] },
2: { name: '宫保鸡丁', price: 5, kcal: 420, colors: ['#C87030', '#E8A040', '#D08028'], tags: ['高蛋白', '低芥酸', '营养均衡'] },
3: { name: '麻婆豆腐', price: 4, kcal: 280, colors: ['#706040', '#B89060', '#807050'], tags: ['植物蛋白', '低芥酸', '微辣'] },
4: { name: '清炒时蔬', price: 3, kcal: 120, colors: ['#306028', '#50A040', '#408030'], tags: ['低脂肪', '高纤维', '清淡'] },
5: { name: '香煎带鱼', price: 6, kcal: 320, colors: ['#805020', '#B88040', '#A06830'], tags: ['高蛋白', '富含DHA', '低芥酸'] },
6: { name: '糖醋排骨', price: 7, kcal: 520, colors: ['#802018', '#C05030', '#A03820'], tags: ['高蛋白', '高热量', '富含钙'] },
7: { name: '蒜蓉西兰花', price: 3, kcal: 110, colors: ['#207040', '#40A868', '#308848'], tags: ['低脂肪', '高纤维', '富含维C'] },
8: { name: '米饭', price: 1.5, kcal: 260, colors: ['#D0D0C0', '#E8E8D8', '#E0E0C8'], tags: ['主食', '低脂肪', '高碳水'] },
9: { name: '紫菜蛋花汤', price: 2, kcal: 80, colors: ['#503028', '#805040', '#603830'], tags: ['低脂肪', '富含碘', '清淡'] },
10: { name: '西红柿炒蛋', price: 4, kcal: 180, colors: ['#E8A030', '#F0C040', '#D89020'], tags: ['高蛋白', '低芥酸', '富含维C'] }
};
/* ===== 解析 URL 参数 ===== */
@@ -271,25 +643,34 @@
document.getElementById('dishPrice').textContent = dishData.price;
document.getElementById('dishKcal').textContent = dishData.kcal;
/* ===== 视频模式数据填充(纵向布局) ===== */
document.getElementById('videoDishName').textContent = dishData.name;
document.getElementById('videoDishPrice').textContent = dishData.price;
document.getElementById('videoDishKcal').textContent = dishData.kcal;
document.getElementById('videoBg').innerHTML = videoBgSVG(dishData.colors);
/* ===== 视频模式数据填充(横向布局,含标签) ===== */
document.getElementById('videoDishNameH').textContent = dishData.name;
document.getElementById('videoDishPriceH').textContent = dishData.price;
document.getElementById('videoDishKcalH').textContent = dishData.kcal;
document.getElementById('videoBgH').innerHTML = videoBgSVG(dishData.colors);
// 渲染餐品标签 chips
var tagsRow = document.getElementById('videoTagsRow');
var tagsHtml = '';
var dishTags = dishData.tags || [];
for (var i = 0; i < dishTags.length; i++) {
tagsHtml += '<span class="video-tag-chip">' + dishTags[i] + '</span>';
}
tagsRow.innerHTML = tagsHtml;
/* ===== 点击菜品信息 → 进入开餐界面 ===== */
document.getElementById('dishInfo').addEventListener('click', function() {
window.location.href = '01-meal-start.html';
});
/* ===== 模拟人脸识别(站在设备前自动触发) ===== */
var faceDetectTimer = null;
/* ===== 演示说明:原 3 秒自动跳转逻辑已移除,改为用户主动点击菜品信息后跳转 ===== */
var isEmployee = false; // 02 默认买家态(买家身份进入)
function startFaceDetect() {
// 模拟:站在设备前 3 秒后识别成功
faceDetectTimer = setTimeout(function() {
// 识别成功 → 跳转取餐界面
window.location.href = '03-meal-pickup.html?dishId=' + dishId;
}, 3000);
}
// 页面加载后自动启动人脸识别
startFaceDetect();
/* ===== Toast 提示 ===== */
var toastEl = document.getElementById('toast');
function showToast(msg, duration) {
@@ -311,6 +692,59 @@
}
window.location.href = '04-settings.html';
});
/* ===== 演示模式切换(常规 / 视频 / 视频2) ===== */
var modeSwitch = document.getElementById('modeSwitch');
var mainArea = document.querySelector('.main-area');
modeSwitch.addEventListener('click', function(e) {
var btn = e.target.closest('button[data-mode]');
if (!btn) return;
var mode = btn.getAttribute('data-mode');
// 切换按钮高亮状态
var btns = modeSwitch.querySelectorAll('button[data-mode]');
for (var i = 0; i < btns.length; i++) {
btns[i].classList.toggle('active', btns[i] === btn);
}
// 通过 main-area 的 class 控制布局切换:先清除所有模式类,再按需添加
mainArea.classList.remove('mode-video', 'mode-video2');
if (mode === 'video') {
mainArea.classList.add('mode-video');
} else if (mode === 'video2') {
mainArea.classList.add('mode-video2');
}
});
/* ===== 视频播放按钮(演示用,点击切换播放/暂停图标) ===== */
var videoPlaying = false;
function bindVideoPlayBtn(btnId) {
document.getElementById(btnId).addEventListener('click', function() {
videoPlaying = !videoPlaying;
this.style.opacity = videoPlaying ? '0' : '1';
});
}
bindVideoPlayBtn('videoPlayBtn');
bindVideoPlayBtn('videoPlayBtnH');
/* ===== 视频背景 SVG(按菜品色系生成展示画面) ===== */
function videoBgSVG(colors) {
var w = 680, h = 383; // 16:9 比例
var id = 'vidGrad';
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + w + ' ' + h + '" preserveAspectRatio="xMidYMid slice" style="width:100%;height:100%;">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="' + colors[0] + '"/>' +
'<stop offset="50%" stop-color="' + colors[1] + '"/>' +
'<stop offset="100%" stop-color="' + colors[2] + '"/></linearGradient></defs>' +
'<rect width="' + w + '" height="' + h + '" fill="url(#' + id + ')"/>' +
'<ellipse cx="' + (w*0.3) + '" cy="' + (h*0.35) + '" rx="180" ry="120" fill="rgba(255,255,255,0.08)"/>' +
'<ellipse cx="' + (w*0.7) + '" cy="' + (h*0.65) + '" rx="220" ry="140" fill="rgba(0,0,0,0.12)"/>' +
'<ellipse cx="' + (w/2) + '" cy="' + (h/2) + '" rx="' + (w*0.32) + '" ry="' + (h*0.35) + '" fill="none" stroke="rgba(255,255,255,0.12)" stroke-width="1.5"/>' +
'<ellipse cx="' + (w/2) + '" cy="' + (h/2) + '" rx="' + (w*0.22) + '" ry="' + (h*0.22) + '" fill="rgba(255,255,255,0.06)"/>' +
'<ellipse cx="' + (w*0.4) + '" cy="' + (h*0.4) + '" rx="60" ry="40" fill="rgba(255,255,255,0.18)"/>' +
'<ellipse cx="' + (w*0.6) + '" cy="' + (h*0.55) + '" rx="40" ry="25" fill="rgba(0,0,0,0.15)"/>' +
'<text x="' + (w/2) + '" y="' + (h*0.18) + '" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-size="20" font-weight="600" font-family="-apple-system,Microsoft YaHei,sans-serif">餐品制作工艺展示</text>' +
'<text x="' + (w/2) + '" y="' + (h*0.85) + '" text-anchor="middle" fill="rgba(255,255,255,0.35)" font-size="12" font-family="-apple-system,Microsoft YaHei,sans-serif">主厨推荐 · 现场烹制</text>' +
'</svg>';
}
</script>
</body>
@@ -1,5 +1,19 @@
# 餐线营养秤 - 变更记录
## [2026-08-05] feat: 菜品展示页新增视频演示模式
### 新增
- **演示模式切换按钮**`02-dish-display.html`):右上角新增「演示」标识 + 模式切换 pill(常规 / 视频),默认常规模式
- **视频模式布局**:纵向「上视频 + 下信息卡」布局,680px 宽 16:9 视频播放器,圆角阴影悬浮卡片
- **视频播放器组件**:含视频背景、居中播放按钮、控制条(进度条 / 时间 / 音量 / 全屏)
- **菜品信息卡**:视频下方展示菜品名、营养指标(热量 / 蛋白 / 脂肪 / 碳水)、简介描述
- **模式联动**:视频模式激活时隐藏常规模式标题和菜品信息区,切换无闪烁
### 调整
- `02-dish-display.html``main-area` 增加 `mode-video` 状态类,通过 CSS 控制两种布局切换
---
## [2026-07-27] feat: 设置页新增闭餐功能
### 新增
+23
View File
@@ -1,5 +1,28 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-05] feat: 净菜订单/申领页面统一新增订单金额字段与总金额栏
### 变更背景
原净菜生产订单、外售订单、内供申领三页均无金额字段,仅在新增/编辑弹窗内展示「汇总预览」(净菜类型/菜品/包装规格/份数/计量)。为补齐订单金额闭环,统一在三页增加单价、小计、订单总金额字段。
### 新增
- **`prod-clean-order.html`**(净菜生产订单)
- 订单数据 `lines[]` 每项新增 `price`(单价)和 `subtotal`(小计)字段,覆盖内供/外售 6 个示例订单共 14 条明细
- 弹窗底部新增「订单总金额」栏,`#orderTotalAmount` 实时展示 `¥1,234.5` 格式金额
- 新增 `calcOrderTotal(o)``formatMoney(n)` 工具函数
- **`sup-clean-external.html`**(外售订单)
- 表头新增「订单金额」列;订单明细新增 `price`/`subtotal` 字段(6 个示例订单 14 条明细)
- 弹窗移除「汇总预览」表格,改为底部「订单总金额」栏
- **`sup-clean-internal.html`**(内供申领)
- 表头新增「订单金额」列;申领明细新增 `price`/`subtotal` 字段(5 个示例订单 11 条明细)
- 弹窗移除「汇总预览」表格,改为底部「订单总金额」栏
### 调整
- 三个页面弹窗的「汇总预览」表格(含 `orderSummary`/`extSummary`/`claimSummary` 容器与 thead/tbody)统一移除,改为弹窗 body 下方贴底的灰底总金额栏
- 金额格式化统一走 `formatMoney(n)``¥1,234.5`(千分位 + 最多 2 位小数)
---
## [2026-08-05] 规格体系统一化 · 阶段五辅助页改造(5.1~5.3 全部完成)
> 本次为 `规格体系统一化改造任务清单.md` 第五阶段辅助页 3 个任务(共 6 个文件)的统一改造。本阶段补齐了跨页共享的"写入端"、字典语义对齐和残余多形态字符串结构化,标志着规格体系统一化改造五个阶段全部完成。
+56 -62
View File
@@ -284,16 +284,12 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<button class="btn btn-primary btn-sm" onclick="addOrderLine()">+ 添加菜品</button>
</div>
<div id="orderLinesContainer"></div>
<!-- 汇总预览 -->
<div id="orderSummary" style="display:none;">
<div style="font-size:var(--font-sm);font-weight:600;margin:12px 0 8px;">汇总预览</div>
<table class="order-summary-table">
<thead id="orderSummaryHead"><tr><th>净菜类型</th><th>菜品/食材</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr></thead>
<tbody id="orderSummaryBody"></tbody>
</table>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:flex-end;padding:12px 24px;border-top:1px solid var(--border);background:#fafafa;gap:12px;">
<span style="font-size:var(--font-sm);color:var(--text-secondary);">订单总金额:</span>
<span id="orderTotalAmount" style="font-weight:700;color:var(--danger);font-size:var(--font-xl);">¥0</span>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeModal('modal-order')">取消</button>
<button class="btn btn-primary" onclick="submitCreateOrder()">确认提交</button>
@@ -416,62 +412,66 @@ var orders=[
{
orderNo:'CO-001',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 07:30',deliverTime:'2026-04-22 10:00',creator:'张厨师',
lines:[
{id:'L1',ncType:'nc',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,status:'已完成'},
{id:'L2',ncType:'nc',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,status:'已完成'}
{id:'L1',ncType:'nc',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,price:5,subtotal:125,status:'已完成'},
{id:'L2',ncType:'nc',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,price:6,subtotal:120,status:'已完成'}
]
},
{
orderNo:'CO-002',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',
lines:[
{id:'L3',ncType:'nc',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,status:'配送中'},
{id:'L4',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:30,weight:15,status:'已完成'}
{id:'L3',ncType:'nc',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,price:8,subtotal:320,status:'配送中'},
{id:'L4',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:30,weight:15,price:12,subtotal:180,status:'已完成'}
]
},
{
orderNo:'CO-003',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:30',creator:'李主管',
lines:[
{id:'L5',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(500g',qty:15,weight:7.5,status:'配送中'}
{id:'L5',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(500g',qty:15,weight:7.5,price:15,subtotal:112.5,status:'配送中'}
]
},
{
orderNo:'CO-004',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 09:00',deliverTime:'2026-04-22 11:00',creator:'王厨师',
lines:[
{id:'L6',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,status:'待配送'},
{id:'L7',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,status:'备货中'}
{id:'L6',ncType:'nc',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,price:4,subtotal:80,status:'待配送'},
{id:'L7',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:5,subtotal:75,status:'备货中'}
]
},
{
orderNo:'CO-005',orderType:'内供',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:00',creator:'张厨师',
lines:[
{id:'L8',ncType:'cp',foodName:'红烧排骨',spec:'标准份(800g',qty:10,weight:8,status:'备货中'},
{id:'L9',ncType:'nc',ingredient:'鸡胸肉',ncName:'鸡胸肉丁',spec:'500g/袋',qty:15,weight:7.5,status:'备货中'},
{id:'L10',ncType:'nc',ingredient:'草鱼',ncName:'鱼片',spec:'1kg/盘',qty:8,weight:8,status:'备货中'}
{id:'L8',ncType:'cp',foodName:'红烧排骨',spec:'标准份(800g',qty:10,weight:8,price:18,subtotal:144,status:'备货中'},
{id:'L9',ncType:'nc',ingredient:'鸡胸肉',ncName:'鸡胸肉丁',spec:'500g/袋',qty:15,weight:7.5,price:10,subtotal:75,status:'备货中'},
{id:'L10',ncType:'nc',ingredient:'草鱼',ncName:'鱼片',spec:'1kg/盘',qty:8,weight:8,price:20,subtotal:160,status:'备货中'}
]
},
{
orderNo:'CO-006',orderType:'外售',ncType:'cp',claimTime:'2026-04-22 10:00',deliverTime:'2026-04-22 11:30',creator:'王厨师',
lines:[
{id:'L11',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:25,weight:12.5,status:'已完成'},
{id:'L12',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g',qty:20,weight:12,status:'已完成'}
{id:'L11',ncType:'cp',foodName:'宫保鸡丁',spec:'标准份(500g',qty:25,weight:12.5,price:12,subtotal:150,status:'已完成'},
{id:'L12',ncType:'cp',foodName:'清蒸鱼',spec:'标准份(600g',qty:20,weight:12,price:15,subtotal:180,status:'已完成'}
]
},
{
orderNo:'CO-007',orderType:'外售',ncType:'nc',claimTime:'2026-04-22 10:30',deliverTime:'2026-04-22 14:00',creator:'李主管',
lines:[
{id:'L13',ncType:'nc',ingredient:'土豆',ncName:'土豆块',spec:'2kg/袋',qty:10,weight:20,status:'已完成'},
{id:'L14',ncType:'nc',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:20,weight:10,status:'配送中'}
{id:'L13',ncType:'nc',ingredient:'土豆',ncName:'土豆块',spec:'2kg/袋',qty:10,weight:20,price:4,subtotal:80,status:'已完成'},
{id:'L14',ncType:'nc',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:20,weight:10,price:5,subtotal:50,status:'配送中'}
]
},
{
orderNo:'CO-008',orderType:'内供',ncType:'cp',claimTime:'2026-04-22 11:00',deliverTime:'2026-04-22 13:00',creator:'张厨师',
lines:[
{id:'L15',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,status:'已完成'},
{id:'L16',ncType:'cp',foodName:'蒜蓉菠菜',spec:'标准份(400g',qty:15,weight:6,status:'已完成'},
{id:'L17',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g',qty:20,weight:6,status:'已完成'}
{id:'L15',ncType:'nc',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:5,subtotal:75,status:'已完成'},
{id:'L16',ncType:'cp',foodName:'蒜蓉菠菜',spec:'标准份(400g',qty:15,weight:6,price:10,subtotal:60,status:'已完成'},
{id:'L17',ncType:'cp',foodName:'炒胡萝卜',spec:'标准份(300g',qty:20,weight:6,price:8,subtotal:48,status:'已完成'}
]
}
];
/* ========== 订单金额工具函数 ========== */
function calcOrderTotal(o){return o.lines.reduce(function(s,l){return s+(parseFloat(l.subtotal)||0);},0);}
function formatMoney(n){var v=parseFloat(n)||0;return '¥'+v.toLocaleString('zh-CN',{minimumFractionDigits:0,maximumFractionDigits:2});}
/* ========== 状态聚合逻辑 ========== */
function aggregateStatus(lines){
var statuses=lines.map(function(l){return l.status;});
@@ -517,6 +517,7 @@ function renderOrderTable(){
html+='<td>'+order.lines.length+'</td>';
html+='<td>'+totalQty+'</td>';
html+='<td>'+totalWeight+'</td>';
html+='<td><b>'+formatMoney(calcOrderTotal(order))+'</b></td>';
html+='<td>'+statusTag(aggStatus)+'</td>';
html+='<td>'+order.claimTime+'</td>';
html+='<td>'+order.deliverTime+'</td>';
@@ -536,9 +537,9 @@ function renderOrderTable(){
/* 子表头 */
if(order.lines.length>0){
if(order.ncType==='nc'){
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td><td></td><td></td></tr>';
}else{
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td colspan="2">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
html+='<tr class="detail-header" id="dhead-'+oi+'" data-order-idx="'+oi+'" style="display:none;"><td></td><td colspan="2">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td><td></td><td></td></tr>';
}
}
/* 明细行 */
@@ -553,7 +554,8 @@ function renderOrderTable(){
html+='<td>'+line.spec+'</td>';
html+='<td>'+line.qty+'</td>';
html+='<td>'+line.weight+'</td>';
html+='<td></td><td></td><td></td><td></td><td></td>';
html+='<td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td>';
html+='<td></td><td></td><td></td><td></td>';
html+='<td class="td-actions"></td></tr>';
});
});
@@ -601,8 +603,6 @@ function onNewOrderNcTypeChange(val){
newOrderLines=[];
lineIdCounter=0;
document.getElementById('orderLinesContainer').innerHTML='';
document.getElementById('orderSummary').style.display='none';
document.getElementById('orderSummaryBody').innerHTML='';
addOrderLine();
}
@@ -616,8 +616,6 @@ function resetOrderForm(){
document.getElementById('orderCreator').value='';
document.getElementById('orderRemark').value='';
document.getElementById('orderLinesContainer').innerHTML='';
document.getElementById('orderSummary').style.display='none';
document.getElementById('orderSummaryBody').innerHTML='';
/* 默认添加一条空行 */
addOrderLine();
}
@@ -625,7 +623,7 @@ function resetOrderForm(){
function addOrderLine(){
lineIdCounter++;
var lid='newLine'+lineIdCounter;
newOrderLines.push({id:lid,ingredient:'',ncName:'',dish:'',portion:'标准份',baseGram:0,unit:newOrderNcType==='nc'?'袋':'份',qty:0,weight:0});
newOrderLines.push({id:lid,ingredient:'',ncName:'',dish:'',portion:'标准份',baseGram:0,unit:newOrderNcType==='nc'?'袋':'份',qty:0,weight:0,price:0,subtotal:0});
renderLineBuilders();
}
@@ -712,6 +710,13 @@ function updateNewLineQty(lid,val){
calcLineWeight(line);
refreshSummary();
}
function updateNewLinePrice(lid,val){
var line=newOrderLines.find(function(l){return l.id===lid;});
if(!line)return;
line.price=parseFloat(val)||0;
calcLineWeight(line);
refreshSummary();
}
function calcLineWeight(line){
if(line.baseGram>0&&line.qty>0){
@@ -719,6 +724,11 @@ function calcLineWeight(line){
}else{
line.weight=0;
}
if(line.price>0&&line.weight>0){
line.subtotal=parseFloat((line.price*line.weight).toFixed(2));
}else{
line.subtotal=0;
}
}
/* 拼装规格显示文本:baseGram+unit/portion */
function formatSpecLabel(line){
@@ -740,9 +750,9 @@ function renderLineBuilders(){
html+='<table class="inline-builder-table" id="lineBuilderTable">';
html+='<thead><tr>';
if(isNc){
html+='<th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
html+='<th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
}else{
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
}
html+='</tr></thead><tbody>';
/* 缓存当前餐品名列表,避免 forEach 内重复读取 localStorage */
@@ -785,6 +795,9 @@ function renderLineBuilders(){
html+='<td><input inputmode="numeric" placeholder="份数" value="'+(line.qty||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateNewLineQty(\''+line.id+'\',this.value)" style="width:56px;"></td>';
/* 计量 */
html+='<td><input value="'+(line.weight||'')+'" readonly style="width:60px;"></td>';
/* 单价与小计金额 */
html+='<td><input inputmode="decimal" placeholder="0" value="'+(line.price||'')+'" oninput="this.value=this.value.replace(/[^\\d.]/g,\'\');updateNewLinePrice(\''+line.id+'\',this.value)" style="width:70px;"></td>';
html+='<td><input value="'+(line.subtotal?formatMoney(line.subtotal):'')+'" readonly style="width:80px;"></td>';
/* 操作 */
html+='<td><button class="btn-danger-text" onclick="removeOrderLine(\''+line.id+'\')" '+(newOrderLines.length<=1?'disabled':'')+'>移除</button></td>';
html+='</tr>';
@@ -795,31 +808,9 @@ function renderLineBuilders(){
}
function refreshSummary(){
var summaryDiv=document.getElementById('orderSummary');
var thead=document.getElementById('orderSummaryHead');
var tbody=document.getElementById('orderSummaryBody');
var validLines=newOrderLines.filter(function(l){return l.weight>0;});
if(validLines.length===0){
summaryDiv.style.display='none';
return;
}
summaryDiv.style.display='block';
if(newOrderNcType==='nc'){
thead.innerHTML='<tr><th>净菜类型</th><th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr>';
}else{
thead.innerHTML='<tr><th>净菜类型</th><th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr>';
}
var html='';
validLines.forEach(function(l){
var typeLabel=newOrderNcType==='nc'?'净菜包':'餐品净菜包';
var specLabel=formatSpecLabel(l);
if(newOrderNcType==='nc'){
html+='<tr><td>'+typeLabel+'</td><td>'+l.ingredient+'</td><td>'+l.ncName+'</td><td>'+specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
}else{
html+='<tr><td>'+typeLabel+'</td><td>'+l.dish+'</td><td>'+specLabel+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
}
});
tbody.innerHTML=html;
var totalAmt=validLines.reduce(function(s,l){return s+(l.subtotal||0);},0);
var taEl=document.getElementById('orderTotalAmount');if(taEl)taEl.textContent=formatMoney(totalAmt);
}
function submitCreateOrder(){
@@ -849,6 +840,8 @@ function submitCreateOrder(){
unit:l.unit||(l.ncType==='nc'?'袋':'份'),
qty:l.qty,
weight:l.weight,
price:l.price,
subtotal:l.subtotal,
status:'备货中'
};
if(newOrderNcType==='nc'){
@@ -920,6 +913,7 @@ function showDetail(orderNo){
html+='<div class="drawer-field"><div class="drawer-field__label">菜品种类</div><div class="drawer-field__value">'+order.lines.length+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">总份数</div><div class="drawer-field__value">'+totalQty+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">总计量(kg)</div><div class="drawer-field__value">'+totalWeight+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">订单总金额</div><div class="drawer-field__value"><b style="color:var(--danger);">'+formatMoney(calcOrderTotal(order))+'</b></div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">申领时间</div><div class="drawer-field__value">'+order.claimTime+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">预定配送时间</div><div class="drawer-field__value">'+order.deliverTime+'</div></div>';
html+='<div class="drawer-field"><div class="drawer-field__label">创建人</div><div class="drawer-field__value">'+order.creator+'</div></div>';
@@ -934,7 +928,7 @@ function showDetail(orderNo){
}else{
html+='<th>菜品/食材</th>';
}
html+='<th>包装规格</th><th>份数</th><th>计量(kg)</th><th>行状态</th></tr></thead><tbody>';
html+='<th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>行状态</th></tr></thead><tbody>';
order.lines.forEach(function(line){
var tagCls={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
html+='<tr><td>'+ncTypeTag(line.ncType)+'</td>';
@@ -943,12 +937,12 @@ function showDetail(orderNo){
}else{
html+='<td'+(isNcOrder?' colspan="2"':'')+'><b>'+line.foodName+'</b></td>';
}
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td><td><span class="tag '+tagCls[line.status]+'">'+line.status+'</span></td></tr>';
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td><td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td><td><span class="tag '+tagCls[line.status]+'">'+line.status+'</span></td></tr>';
/* 餐品净菜包显示食材明细 */
if(line.ncType==='cp'){
var ings=dishIngredients[line.foodName]||[];
if(ings.length>0){
var colSpan=isNcOrder?7:6;
var colSpan=isNcOrder?9:8;
html+='<tr><td colspan="'+colSpan+'" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';
ings.forEach(function(ing,i){
if(i>0)html+=', ';
+53 -50
View File
@@ -207,7 +207,7 @@ function toggleSubgroup(header) {
<div class="table-card__content">
<table id="extOrderTable">
<thead><tr>
<th style="width:36px;"></th><th>订单号</th><th>客户名称</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总计量(kg)</th><th>下单时间</th><th>预定配送</th><th>物流单号</th><th>订单状态</th><th>操作</th>
<th style="width:36px;"></th><th>订单号</th><th>客户名称</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总计量(kg)</th><th>订单金额</th><th>下单时间</th><th>预定配送</th><th>物流单号</th><th>订单状态</th><th>操作</th>
</tr></thead>
<tbody id="extOrderTableBody"></tbody>
</table>
@@ -243,15 +243,12 @@ function toggleSubgroup(header) {
<button class="btn btn-primary btn-sm" onclick="addExtLine()">+ 添加菜品</button>
</div>
<div id="extLinesContainer"></div>
<div id="extSummary" style="display:none;">
<div style="font-size:var(--font-sm);font-weight:600;margin:12px 0 8px;">汇总预览</div>
<table class="order-summary-table">
<thead id="extSummaryHead"><tr><th>净菜类型</th><th>菜品/食材</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr></thead>
<tbody id="extSummaryBody"></tbody>
</table>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:flex-end;padding:12px 24px;border-top:1px solid var(--border);background:#fafafa;gap:12px;">
<span style="font-size:var(--font-sm);color:var(--text-secondary);">订单总金额:</span>
<span id="extTotalAmount" style="font-weight:700;color:var(--danger);font-size:var(--font-xl);">¥0</span>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeModal('createOrderModal')">取消</button>
<button class="btn btn-primary" onclick="submitExtOrder()">确认提交</button>
@@ -380,37 +377,41 @@ var dishIngsExt={
var extOrders=[
{orderNo:'EXT-001',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 08:00',deliverTime:'2026-04-22 16:00',logistics:'SF1234567890',
lines:[
{id:'L1',ingredient:'土豆',ncName:'土豆丝',spec:'5kg/箱',qty:10,weight:50,status:'已完成'},
{id:'L2',ingredient:'土豆',ncName:'土豆块',spec:'5kg/箱',qty:6,weight:30,status:'已完成'}
{id:'L1',ingredient:'土豆',ncName:'土豆丝',spec:'5kg/箱',qty:10,weight:50,price:3.5,subtotal:175,status:'已完成'},
{id:'L2',ingredient:'土豆',ncName:'土豆块',spec:'5kg/箱',qty:6,weight:30,price:3.5,subtotal:105,status:'已完成'}
]},
{orderNo:'EXT-002',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 09:30',deliverTime:'2026-04-22 14:00',logistics:'SF9876543210',
lines:[
{id:'L3',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:10,weight:10,status:'配送中'},
{id:'L4',ingredient:'宫保鸡丁',ncName:'',spec:'标准份(500g',qty:40,weight:20,status:'配送中'}
{id:'L3',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:10,weight:10,price:28,subtotal:280,status:'配送中'},
{id:'L4',ingredient:'宫保鸡丁',ncName:'',spec:'标准份(500g',qty:40,weight:20,price:12,subtotal:240,status:'配送中'}
]},
{orderNo:'EXT-003',ncType:'nc',customer:'学校食堂',orderTime:'2026-04-22 10:00',deliverTime:'2026-04-23 09:00',logistics:'—',
lines:[
{id:'L5',ingredient:'鸡肉',ncName:'鸡肉块',spec:'1kg/箱',qty:60,weight:60,status:'待配送'},
{id:'L6',ingredient:'西兰花',ncName:'西兰花朵',spec:'5kg/箱',qty:8,weight:40,status:'待配送'}
{id:'L5',ingredient:'鸡肉',ncName:'鸡肉块',spec:'1kg/箱',qty:60,weight:60,price:20,subtotal:1200,status:'待配送'},
{id:'L6',ingredient:'西兰花',ncName:'西兰花朵',spec:'5kg/箱',qty:8,weight:40,price:4.5,subtotal:180,status:'待配送'}
]},
{orderNo:'EXT-004',ncType:'nc',customer:'职工家属区团购',orderTime:'2026-04-22 10:30',deliverTime:'2026-04-23 16:00',logistics:'—',
lines:[
{id:'L7',ingredient:'清蒸鱼',ncName:'',spec:'标准份(600g',qty:20,weight:12,status:'备货中'},
{id:'L8',ingredient:'蒜蓉菠菜',ncName:'',spec:'标准份(400g',qty:30,weight:12,status:'备货中'},
{id:'L9',ingredient:'胡萝卜',ncName:'胡萝卜条',spec:'2kg/箱',qty:15,weight:30,status:'备货中'}
{id:'L7',ingredient:'清蒸鱼',ncName:'',spec:'标准份(600g',qty:20,weight:12,price:15,subtotal:180,status:'备货中'},
{id:'L8',ingredient:'蒜蓉菠菜',ncName:'',spec:'标准份(400g',qty:30,weight:12,price:8,subtotal:96,status:'备货中'},
{id:'L9',ingredient:'胡萝卜',ncName:'胡萝卜条',spec:'2kg/箱',qty:15,weight:30,price:4,subtotal:120,status:'备货中'}
]},
{orderNo:'EXT-005',ncType:'nc',customer:'社区便民超市',orderTime:'2026-04-22 11:00',deliverTime:'2026-04-23 10:00',logistics:'—',
lines:[
{id:'L10',ingredient:'菠菜',ncName:'菠菜段',spec:'3kg/箱',qty:12,weight:36,status:'已完成'},
{id:'L11',ingredient:'炒胡萝卜',ncName:'',spec:'标准份(300g',qty:25,weight:7.5,status:'配送中'}
{id:'L10',ingredient:'菠菜',ncName:'菠菜段',spec:'3kg/箱',qty:12,weight:36,price:5,subtotal:180,status:'已完成'},
{id:'L11',ingredient:'炒胡萝卜',ncName:'',spec:'标准份(300g',qty:25,weight:7.5,price:18,subtotal:135,status:'配送中'}
]},
{orderNo:'EXT-006',ncType:'nc',customer:'周边餐饮企业',orderTime:'2026-04-22 12:00',deliverTime:'2026-04-23 12:00',logistics:'SF5566778899',
lines:[
{id:'L12',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:15,weight:15,status:'已完成'},
{id:'L13',ingredient:'宫保鸡丁',ncName:'',spec:'标准份(500g',qty:50,weight:25,status:'已完成'},
{id:'L14',ingredient:'清蒸鱼',ncName:'',spec:'标准份(600g',qty:30,weight:18,status:'已完成'}
{id:'L12',ingredient:'净猪里脊',ncName:'里脊条',spec:'1kg/箱',qty:15,weight:15,price:28,subtotal:420,status:'已完成'},
{id:'L13',ingredient:'宫保鸡丁',ncName:'',spec:'标准份(500g',qty:50,weight:25,price:12,subtotal:300,status:'已完成'},
{id:'L14',ingredient:'清蒸鱼',ncName:'',spec:'标准份(600g',qty:30,weight:18,price:15,subtotal:270,status:'已完成'}
]}
];
/* 计算订单总金额:Σ(各明细小计) */
function calcOrderTotal(o){return o.lines.reduce(function(s,l){return s+(parseFloat(l.subtotal)||0);},0);}
/* 金额格式化:¥1,234.5 */
function formatMoney(n){var v=parseFloat(n)||0;return '¥'+v.toLocaleString('zh-CN',{minimumFractionDigits:0,maximumFractionDigits:2});}
function aggregateStatus(lines){var ss=lines.map(function(l){return l.status;});
if(ss.every(function(s){return s==='已完成';}))return '已完成';
@@ -428,7 +429,7 @@ function renderExtTable(){
var tW=o.lines.reduce(function(s,l){return s+parseFloat(l.weight);},0).toFixed(2);
h+='<tr class="master-row" data-ext-idx="'+oi+'" onclick="toggleExpandExt(this,'+oi+')">';
h+='<td><span class="expand-arrow" id="earrow'+oi+'">▶</span></td>';
h+='<td><b>'+o.orderNo+'</b></td><td>'+o.customer+'</td><td>'+ncTypeTag(o.ncType)+'</td><td>'+o.lines.length+'</td><td>'+tQ+'</td><td>'+tW+'</td>';
h+='<td><b>'+o.orderNo+'</b></td><td>'+o.customer+'</td><td>'+ncTypeTag(o.ncType)+'</td><td>'+o.lines.length+'</td><td>'+tQ+'</td><td>'+tW+'</td><td><b>'+formatMoney(calcOrderTotal(o))+'</b></td>';
h+='<td>'+o.orderTime+'</td><td>'+o.deliverTime+'</td><td>'+o.logistics+'</td><td>'+statusTag(agg)+'</td>';
/* 主行操作按钮:根据聚合状态显示 */
var abtns='';
@@ -439,20 +440,22 @@ function renderExtTable(){
h+='</tr>';
/* 子表头 — 根据 ncType 动态显示列 */
if(o.ncType==='nc'){
h+='<tr class="detail-header" id="ehead-'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td></td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td></td><td></td><td></td></tr>';
h+='<tr class="detail-header" id="ehead-'+oi+'" style="display:none;"><td></td><td>食材</td><td>净菜名称</td><td></td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td></tr>';
o.lines.forEach(function(l){
h+='<tr class="detail-row" id="edetail-'+oi+'" style="display:none;">';
h+='<td></td><td style="font-weight:500;">'+l.ingredient+'</td><td style="font-weight:500;">'+(l.ncName||'—')+'</td><td></td>';
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td>';
h+='<td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td>';
h+='<td></td><td></td><td></td>';
h+='<td class="td-actions"></td></tr>';
});
}else{
h+='<tr class="detail-header" id="ehead-'+oi+'" style="display:none;"><td></td><td colspan="3">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td></td><td></td><td></td></tr>';
h+='<tr class="detail-header" id="ehead-'+oi+'" style="display:none;"><td></td><td colspan="3">菜品/食材</td><td>包装规格</td><td>份数</td><td>计量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td></tr>';
o.lines.forEach(function(l){
h+='<tr class="detail-row" id="edetail-'+oi+'" style="display:none;">';
h+='<td></td><td colspan="3" style="font-weight:500;">'+l.foodName+'</td>';
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td>';
h+='<td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td>';
h+='<td></td><td></td><td></td>';
h+='<td class="td-actions"></td></tr>';
});
@@ -494,8 +497,8 @@ function doReadyExt(){var b=document.getElementById('readyBatchSelectExt').value
var newExtOrderType='nc';
function updExtOrderType(v){newExtOrderType=v;renderExtLineBuilders();}
var newExtLines=[];var extLineCnt=0;
function resetExtForm(){newExtLines=[];extLineCnt=0;newExtOrderType=document.getElementById('extOrderNcType').value;document.getElementById('extCustomer').value='';document.getElementById('extContact').value='';document.getElementById('extPhone').value='';document.getElementById('extDeliverTime').value='';document.getElementById('extAddress').value='';document.getElementById('extRemark').value='';document.getElementById('extLinesContainer').innerHTML='';document.getElementById('extSummary').style.display='none';addExtLine();}
function addExtLine(){extLineCnt++;newExtLines.push({id:'el'+extLineCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:newExtOrderType==='nc'?'箱':'份',qty:0,weight:0});renderExtLineBuilders();}
function resetExtForm(){newExtLines=[];extLineCnt=0;newExtOrderType=document.getElementById('extOrderNcType').value;document.getElementById('extCustomer').value='';document.getElementById('extContact').value='';document.getElementById('extPhone').value='';document.getElementById('extDeliverTime').value='';document.getElementById('extAddress').value='';document.getElementById('extRemark').value='';document.getElementById('extLinesContainer').innerHTML='';var taEl=document.getElementById('extTotalAmount');if(taEl)taEl.textContent=formatMoney(0);addExtLine();}
function addExtLine(){extLineCnt++;newExtLines.push({id:'el'+extLineCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:newExtOrderType==='nc'?'箱':'份',qty:0,weight:0,price:0,subtotal:0});renderExtLineBuilders();}
function removeExtLine(lid){if(newExtLines.length<=1){alert('至少保留一条');return;}newExtLines=newExtLines.filter(function(l){return l.id!==lid;});renderExtLineBuilders();}
function updELIngredient(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.ingredient=v;l.ncType='';renderExtLineBuilders();}
@@ -526,16 +529,22 @@ function updELPortion(lid,v){
function updELUnit(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.unit=v;refreshESummary();}
function updELNcBaseGram(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.baseGram=parseInt(v||'0',10);l.unit='箱';calcELW(l);refreshESummary();}
function updELQty(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.qty=parseInt(v.replace(/[^\d]/g,'')||'0',10);calcELW(l);refreshESummary();}
function calcELW(l){if(l.baseGram>0&&l.qty>0)l.weight=parseFloat((l.baseGram*l.qty/1000).toFixed(2));else l.weight=0;}
/* 单价(元/kg)输入:同步小计金额 = 单价 × 计量(kg) */
function updELPrice(lid,v){var l=newExtLines.find(function(x){return x.id===lid;});if(!l)return;l.price=parseFloat(v)||0;calcELW(l);refreshESummary();}
function calcELW(l){
if(l.baseGram>0&&l.qty>0)l.weight=parseFloat((l.baseGram*l.qty/1000).toFixed(2));else l.weight=0;
/* 小计金额 = 单价(元/kg) × 计量(kg) */
if(l.price>0&&l.weight>0)l.subtotal=parseFloat((l.price*l.weight).toFixed(2));else l.subtotal=0;
}
function renderExtLineBuilders(){
var h='';
var isNc=newExtOrderType==='nc';
h+='<table class="line-builder-table"><thead><tr>';
if(isNc){
h+='<th>食材</th><th>净菜名称</th><th>包装规格(g)</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
h+='<th>食材</th><th>净菜名称</th><th>包装规格(g)</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
}else{
h+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>操作</th>';
h+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
}
h+='</tr></thead><tbody>';
var dishNames=getDishNamesWithFallback();
@@ -568,8 +577,10 @@ function renderExtLineBuilders(){
portionOptions.forEach(function(p){h+='<option value="'+p+'" '+(l.portion===p?'selected':'')+'>'+p+'</option>';});
h+='</select></div></td>';
}
h+='<td><input inputmode="numeric" placeholder="份数" value="'+(l.qty||'')+'" oninput="this.value=this.value.replace(/[^\d]/g,\x27\x27);updELQty(\x27'+l.id+'\x27,this.value)"></td>';
h+='<td><input inputmode="numeric" placeholder="份数" value="'+(l.qty||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updELQty(\''+l.id+'\',this.value)"></td>';
h+='<td><input value="'+(l.weight||'')+'" readonly></td>';
h+='<td><input inputmode="decimal" placeholder="0" value="'+(l.price||'')+'" oninput="this.value=this.value.replace(/[^\\d.]/g,\'\');updELPrice(\''+l.id+'\',this.value)"></td>';
h+='<td><input value="'+(l.subtotal?formatMoney(l.subtotal):'')+'" readonly></td>';
h+='<td><button class="btn-remove" onclick="removeExtLine(\''+l.id+'\')">移除</button></td>';
h+='</tr>';
});
@@ -579,19 +590,10 @@ function renderExtLineBuilders(){
}
function refreshESummary(){
var sd=document.getElementById('extSummary');var tb=document.getElementById('extSummaryBody');
var sh=document.getElementById('extSummaryHead');
/* 同步订单总金额(汇总预览已移除,仅保留底部订单总金额展示) */
var vl=newExtLines.filter(function(l){return l.weight>0;});
if(vl.length===0){sd.style.display='none';return;}
sd.style.display='block';var h='';
if(newExtOrderType==='nc'){
sh.innerHTML='<tr><th>净菜类型</th><th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr>';
vl.forEach(function(l){h+='<tr><td>净菜包</td><td>'+(l.ingredient||'')+'</td><td>'+(l.ncType||'')+'</td><td>'+formatSpecLabelExt(l)+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';});
}else{
sh.innerHTML='<tr><th>净菜类型</th><th>菜品/食材</th><th>包装规格</th><th>份数</th><th>计量(kg)</th></tr>';
vl.forEach(function(l){h+='<tr><td>餐品净菜包</td><td>'+(l.dish||'')+'</td><td>'+formatSpecLabelExt(l)+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';});
}
tb.innerHTML=h;
var totalAmt=vl.reduce(function(s,l){return s+(l.subtotal||0);},0);
var taEl=document.getElementById('extTotalAmount');if(taEl)taEl.textContent=formatMoney(totalAmt);
}
function submitExtOrder(){
@@ -603,9 +605,9 @@ function submitExtOrder(){
var newLines=vl.map(function(l,i){
var specLbl=formatSpecLabelExt(l);
if(newExtOrderType==='nc'){
return {id:'L'+(Date.now()+i),ingredient:l.ingredient,ncName:l.ncType,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,status:'备货中'};
return {id:'L'+(Date.now()+i),ingredient:l.ingredient,ncName:l.ncType,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,price:l.price||0,subtotal:l.subtotal||0,status:'备货中'};
}
return {id:'L'+(Date.now()+i),foodName:l.dish,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,status:'备货中'};
return {id:'L'+(Date.now()+i),foodName:l.dish,spec:specLbl,portion:l.portion,baseGram:l.baseGram,unit:l.unit,qty:l.qty,weight:l.weight,price:l.price||0,subtotal:l.subtotal||0,status:'备货中'};
});
extOrders.unshift({orderNo:no,customer:cust,orderTime:new Date().toISOString().slice(0,16).replace('T',' '),deliverTime:dt,logistics:'—',
ncType:newExtOrderType,lines:newLines});
@@ -626,23 +628,24 @@ function showDetailExt(no){
h+='<div class="drawer-field"><div class="drawer-field__label">菜品种类</div><div class="drawer-field__value">'+o.lines.length+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">总份数</div><div class="drawer-field__value">'+tQ+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">总计量(kg)</div><div class="drawer-field__value">'+tW+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">订单总金额</div><div class="drawer-field__value" style="color:var(--danger);font-weight:700;">'+formatMoney(calcOrderTotal(o))+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">下单时间</div><div class="drawer-field__value">'+o.orderTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">预定配送</div><div class="drawer-field__value">'+o.deliverTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">物流单号</div><div class="drawer-field__value">'+o.logistics+'</div></div>';
h+='</div></div>';
h+='<div class="drawer-section"><div class="drawer-section__title">订单明细('+o.lines.length+' 道)</div>';
if(o.ncType==='nc'){
h+='<table class="ing-table"><thead><tr><th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>状态</th></tr></thead><tbody>';
h+='<table class="ing-table"><thead><tr><th>食材</th><th>净菜名称</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>状态</th></tr></thead><tbody>';
o.lines.forEach(function(l){
var tc={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
h+='<tr><td style="font-weight:600;">'+l.ingredient+'</td><td style="font-weight:600;">'+(l.ncName||'—')+'</td><td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
h+='<tr><td style="font-weight:600;">'+l.ingredient+'</td><td style="font-weight:600;">'+(l.ncName||'—')+'</td><td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
});
}else{
h+='<table class="ing-table"><thead><tr><th>菜品/食材</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>状态</th></tr></thead><tbody>';
h+='<table class="ing-table"><thead><tr><th>菜品/食材</th><th>包装规格</th><th>份数</th><th>计量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>状态</th></tr></thead><tbody>';
o.lines.forEach(function(l){
var tc={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
h+='<tr><td style="font-weight:600;"><b>'+l.foodName+'</b></td><td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
var ings=dishIngsExt[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="5" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}
h+='<tr><td style="font-weight:600;"><b>'+l.foodName+'</b></td><td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
var ings=dishIngsExt[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="7" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}
});
}
h+='</tbody></table></div>';
+40 -58
View File
@@ -212,7 +212,7 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<div class="table-card__content">
<table id="claimTable">
<thead><tr>
<th style="width:36px;"></th><th>申领单号</th><th>申领场所</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总数量(kg)</th><th>申领单状态</th><th>申领时间</th><th>预定配送时间</th><th>配送人</th><th>操作</th>
<th style="width:36px;"></th><th>申领单号</th><th>申领场所</th><th>净菜类型</th><th>菜品种类</th><th>总份数</th><th>总数量(kg)</th><th>订单金额</th><th>申领单状态</th><th>申领时间</th><th>预定配送时间</th><th>配送人</th><th>操作</th>
</tr></thead>
<tbody id="claimTableBody"></tbody>
</table>
@@ -260,15 +260,12 @@ tr.detail-row td.td-actions{background:#FAFBFC;box-shadow:-4px 0 8px rgba(0,0,0,
<button class="btn btn-primary btn-sm" onclick="addClaimLine()">+ 添加菜品</button>
</div>
<div id="claimLinesContainer"></div>
<div id="claimSummary" style="display:none;">
<div style="font-size:var(--font-sm);font-weight:600;margin:12px 0 8px;">汇总预览</div>
<table class="order-summary-table">
<thead id="claimSummaryHead"></thead>
<tbody id="claimSummaryBody"></tbody>
</table>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:flex-end;padding:12px 24px;border-top:1px solid var(--border);background:#fafafa;gap:12px;">
<span style="font-size:var(--font-sm);color:var(--text-secondary);">订单总金额:</span>
<span id="claimTotalAmount" style="font-weight:700;color:var(--danger);font-size:var(--font-xl);">¥0</span>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeModal('modal-claim')">取消</button>
<button class="btn btn-primary" onclick="submitClaim()">确认提交</button>
@@ -404,31 +401,35 @@ var dishIngredients={
var claims=[
{claimNo:'INT-001',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 07:30',deliverTime:'2026-04-22 09:00',deliverPerson:'李配送',
lines:[
{id:'L1',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,status:'已完成'},
{id:'L2',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,status:'已完成'}
{id:'L1',ingredient:'菠菜',ncName:'菠菜段',spec:'500g/袋',qty:50,weight:25,price:5,subtotal:125,status:'已完成'},
{id:'L2',ingredient:'玉米',ncName:'玉米粒',spec:'1kg/盒',qty:20,weight:20,price:6,subtotal:120,status:'已完成'}
]},
{claimNo:'INT-002',canteen:'二号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:00',deliverTime:'2026-04-22 10:00',deliverPerson:'王配送',
lines:[
{id:'L3',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,status:'配送中'},
{id:'L4',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:30,weight:15,status:'配送中'}
{id:'L3',ingredient:'猪排骨',ncName:'排骨块',spec:'2kg/盘',qty:20,weight:40,price:25,subtotal:1000,status:'配送中'},
{id:'L4',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:30,weight:15,price:3.5,subtotal:52.5,status:'配送中'}
]},
{claimNo:'INT-003',canteen:'三号食堂厨房',ncType:'nc',claimTime:'2026-04-22 08:30',deliverTime:'2026-04-22 10:30',deliverPerson:'—',
lines:[
{id:'L5',ingredient:'草鱼',ncName:'鱼片',spec:'2kg/盘',qty:20,weight:40,status:'待配送'},
{id:'L6',ingredient:'鸡胸肉',ncName:'鸡胸肉丝',spec:'500g/袋',qty:15,weight:7.5,status:'待配送'}
{id:'L5',ingredient:'草鱼',ncName:'鱼片',spec:'2kg/盘',qty:20,weight:40,price:18,subtotal:720,status:'待配送'},
{id:'L6',ingredient:'鸡胸肉',ncName:'鸡胸肉丝',spec:'500g/袋',qty:15,weight:7.5,price:20,subtotal:150,status:'待配送'}
]},
{claimNo:'INT-004',canteen:'一号食堂厨房',ncType:'nc',claimTime:'2026-04-22 09:15',deliverTime:'2026-04-22 11:30',deliverPerson:'—',
lines:[
{id:'L7',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,status:'备货中'},
{id:'L8',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:25,weight:12.5,status:'备货中'},
{id:'L9',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,status:'备货中'}
{id:'L7',ingredient:'土豆',ncName:'土豆丝',spec:'500g/袋',qty:40,weight:20,price:3.5,subtotal:70,status:'备货中'},
{id:'L8',ingredient:'菠菜',ncName:'菠菜碎',spec:'500g/袋',qty:25,weight:12.5,price:5,subtotal:62.5,status:'备货中'},
{id:'L9',ingredient:'西兰花',ncName:'西兰花朵',spec:'500g/袋',qty:30,weight:15,price:6,subtotal:90,status:'备货中'}
]},
{claimNo:'INT-005',canteen:'二号食堂厨房',ncType:'cp',claimTime:'2026-04-22 09:30',deliverTime:'2026-04-22 12:00',deliverPerson:'—',
lines:[
{id:'L10',foodName:'宫保鸡丁',spec:'标准份(500g',qty:20,weight:10,status:'已完成'},
{id:'L11',foodName:'炒胡萝卜',spec:'标准份(300g',qty:15,weight:4.5,status:'配送中'}
{id:'L10',foodName:'宫保鸡丁',spec:'标准份(500g',qty:20,weight:10,price:12,subtotal:120,status:'已完成'},
{id:'L11',foodName:'炒胡萝卜',spec:'标准份(300g',qty:15,weight:4.5,price:18,subtotal:81,status:'配送中'}
]}
];
/* 计算申领单总金额:Σ(各明细小计) */
function calcOrderTotal(o){return o.lines.reduce(function(s,l){return s+(parseFloat(l.subtotal)||0);},0);}
/* 金额格式化:¥1,234.5 */
function formatMoney(n){var v=parseFloat(n)||0;return '¥'+v.toLocaleString('zh-CN',{minimumFractionDigits:0,maximumFractionDigits:2});}
function aggregateStatus(lines){
var statuses=lines.map(function(l){return l.status;});
@@ -450,7 +451,7 @@ function renderClaimTable(){
var tW=c.lines.reduce(function(s,l){return s+parseFloat(l.weight);},0).toFixed(2);
html+='<tr class="master-row" data-claim-idx="'+ci+'" onclick="toggleExpand(this,'+ci+')">';
html+='<td><span class="expand-arrow" id="carrow'+ci+'">▶</span></td>';
html+='<td><b>'+c.claimNo+'</b></td><td>'+c.canteen+'</td><td>'+ncTypeTag(c.ncType)+'</td><td>'+c.lines.length+'</td><td>'+tQty+'</td><td>'+tW+'</td><td>'+statusTag(agg)+'</td>';
html+='<td><b>'+c.claimNo+'</b></td><td>'+c.canteen+'</td><td>'+ncTypeTag(c.ncType)+'</td><td>'+c.lines.length+'</td><td>'+tQty+'</td><td>'+tW+'</td><td><b>'+formatMoney(calcOrderTotal(c))+'</b></td><td>'+statusTag(agg)+'</td>';
html+='<td>'+c.claimTime+'</td><td>'+c.deliverTime+'</td><td>'+c.deliverPerson+'</td>';
html+='<td class="td-actions">';
if(agg==='备货中')html+='<span class="link" style="color:var(--success);" onclick="event.stopPropagation();orderAction(\''+c.claimNo+'\',\'ready\')">备货完成</span> ';
@@ -466,7 +467,7 @@ function renderClaimTable(){
}else{
html+='<td colspan="2">菜品/食材</td>';
}
html+='<td>包装规格</td><td>份数</td><td>数量(kg)</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
html+='<td>包装规格</td><td>份数</td><td>数量(kg)</td><td>单价(元/kg)</td><td>小计金额</td><td></td><td></td><td></td><td></td><td></td></tr>';
c.lines.forEach(function(line){
html+='<tr class="detail-row" id="cdetail-'+ci+'" style="display:none;">';
html+='<td></td>';
@@ -475,8 +476,8 @@ function renderClaimTable(){
}else{
html+='<td colspan="2" style="font-weight:500;">'+line.foodName+'</td>';
}
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td>';
html+='<td></td><td></td><td></td><td></td><td></td><td class="td-actions"></td></tr>';
html+='<td>'+line.spec+'</td><td>'+line.qty+'</td><td>'+line.weight+'</td><td>'+(line.price||0)+'</td><td>'+formatMoney(line.subtotal||0)+'</td>';
html+='<td></td><td></td><td></td><td></td><td class="td-actions"></td></tr>';
});
});
tbody.innerHTML=html;
@@ -559,14 +560,13 @@ function resetClaimForm(){
document.getElementById('claimDeliverPerson').value='';
document.getElementById('claimBatchNo').value='';
document.getElementById('claimLinesContainer').innerHTML='';
document.getElementById('claimSummary').style.display='none';
addClaimLine();
}
function addClaimLine(){
claimLineIdCnt++;
var orderNcType=document.getElementById('claimNcType').value||'nc';
newClaimLines.push({id:'cl'+claimLineIdCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:orderNcType==='nc'?'盘':'份',qty:0,weight:0});
newClaimLines.push({id:'cl'+claimLineIdCnt,ingredient:'',ncType:'',dish:'',portion:'标准份',baseGram:0,unit:orderNcType==='nc'?'盘':'份',qty:0,weight:0,price:0,subtotal:0});
renderClaimLineBuilders();
}
@@ -604,7 +604,8 @@ function updateCLPortion(lid,val){
function updateCLUnit(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.unit=val;refreshCLSummary();}
function updateCLNcBaseGram(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.baseGram=parseInt(val||'0',10);l.unit='盘';calcCLWeight(l);refreshCLSummary();}
function updateCLQty(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.qty=parseInt(val.replace(/[^\d]/g,'')||'0',10);calcCLWeight(l);refreshCLSummary();}
function calcCLWeight(l){if(l.baseGram>0&&l.qty>0)l.weight=parseFloat((l.baseGram*l.qty/1000).toFixed(2));else l.weight=0;}
function updateCLPrice(lid,val){var l=newClaimLines.find(function(x){return x.id===lid;});if(!l)return;l.price=parseFloat(val)||0;calcCLWeight(l);refreshCLSummary();}
function calcCLWeight(l){if(l.baseGram>0&&l.qty>0)l.weight=parseFloat((l.baseGram*l.qty/1000).toFixed(2));else l.weight=0;if(l.price>0&&l.weight>0)l.subtotal=parseFloat((l.price*l.weight).toFixed(2));else l.subtotal=0;}
function renderClaimLineBuilders(){
var html='';
@@ -612,9 +613,9 @@ function renderClaimLineBuilders(){
var isNc=orderNcType==='nc';
html+='<table class="inline-line-table"><thead><tr>';
if(isNc){
html+='<th>食材</th><th>净菜名称</th><th>包装规格(g)</th><th>份数</th><th>数量(kg)</th><th>操作</th>';
html+='<th>食材</th><th>净菜名称</th><th>包装规格(g)</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
}else{
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>数量(kg)</th><th>操作</th>';
html+='<th>餐品名称</th><th>包装规格</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>操作</th>';
}
html+='</tr></thead><tbody>';
var dishNames=getDishNamesWithFallback();
@@ -653,6 +654,9 @@ function renderClaimLineBuilders(){
/* 份数和数量(nc/cp 共用) */
html+='<td><input inputmode="numeric" placeholder="份数" value="'+(line.qty||'')+'" oninput="this.value=this.value.replace(/[^\\d]/g,\'\');updateCLQty(\''+line.id+'\',this.value)"></td>';
html+='<td><input value="'+(line.weight||'')+'" readonly></td>';
/* 单价与小计金额(nc/cp 共用) */
html+='<td><input inputmode="decimal" placeholder="0" value="'+(line.price||'')+'" oninput="this.value=this.value.replace(/[^\\d.]/g,\'\');updateCLPrice(\''+line.id+'\',this.value)"></td>';
html+='<td><input value="'+(line.subtotal?formatMoney(line.subtotal):'')+'" readonly></td>';
html+='<td class="td-actions"><button class="btn-danger-text" onclick="removeClaimLine(\''+line.id+'\')">移除</button></td>';
html+='</tr>';
});
@@ -662,32 +666,9 @@ function renderClaimLineBuilders(){
}
function refreshCLSummary(){
var sd=document.getElementById('claimSummary');var tb=document.getElementById('claimSummaryBody');
var th=document.getElementById('claimSummaryHead');
var orderNcType=document.getElementById('claimNcType').value;
var isNc=orderNcType==='nc';
var vl=newClaimLines.filter(function(l){return l.weight>0;});
if(vl.length===0){sd.style.display='none';return;}
sd.style.display='block';
/* 动态表头 */
var headerHtml='<tr>';
if(isNc){
headerHtml+='<th>食材</th><th>净菜名称</th>';
}else{
headerHtml+='<th colspan="2">菜品/食材</th>';
}
headerHtml+='<th>包装规格</th><th>份数</th><th>数量(kg)</th></tr>';
th.innerHTML=headerHtml;
/* 数据行 */
var h='';vl.forEach(function(l){
if(isNc){
h+='<tr><td>'+l.ingredient+'</td><td>'+l.ncType+'</td>';
}else{
h+='<tr><td colspan="2">'+l.dish+'</td>';
}
h+='<td>'+formatSpecLabelInt(l)+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td></tr>';
});
tb.innerHTML=h;
var totalAmt=vl.reduce(function(s,l){return s+(l.subtotal||0);},0);
var taEl=document.getElementById('claimTotalAmount');if(taEl)taEl.textContent=formatMoney(totalAmt);
}
function submitClaim(){
@@ -706,9 +687,9 @@ function submitClaim(){
lines:vl.map(function(l,i){
var specLbl=formatSpecLabelInt(l);
if(orderNcType==='nc'){
return {id:'L'+(Date.now()+i), ingredient:l.ingredient, ncName:l.ncType, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, status:'备货中'};
return {id:'L'+(Date.now()+i), ingredient:l.ingredient, ncName:l.ncType, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, price:l.price, subtotal:l.subtotal, status:'备货中'};
}else{
return {id:'L'+(Date.now()+i), foodName:l.dish, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, status:'备货中'};
return {id:'L'+(Date.now()+i), foodName:l.dish, spec:specLbl, portion:l.portion, baseGram:l.baseGram, unit:l.unit, qty:l.qty, weight:l.weight, price:l.price, subtotal:l.subtotal, status:'备货中'};
}
})});
closeModal('modal-claim');renderClaimTable();
@@ -727,6 +708,7 @@ function showDetail(claimNo){
h+='<div class="drawer-field"><div class="drawer-field__label">菜品种类</div><div class="drawer-field__value">'+c.lines.length+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">总份数</div><div class="drawer-field__value">'+tQ+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">总数量(kg)</div><div class="drawer-field__value">'+tW+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">订单总金额</div><div class="drawer-field__value"><b style="color:var(--danger);">'+formatMoney(calcOrderTotal(c))+'</b></div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">申领时间</div><div class="drawer-field__value">'+c.claimTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">预定配送时间</div><div class="drawer-field__value">'+c.deliverTime+'</div></div>';
h+='<div class="drawer-field"><div class="drawer-field__label">配送人</div><div class="drawer-field__value">'+c.deliverPerson+'</div></div>';
@@ -738,7 +720,7 @@ function showDetail(claimNo){
}else{
h+='<th colspan="2">菜品/食材</th>';
}
h+='<th>包装规格</th><th>份数</th><th>数量(kg)</th><th>状态</th></tr></thead><tbody>';
h+='<th>包装规格</th><th>份数</th><th>数量(kg)</th><th>单价(元/kg)</th><th>小计金额</th><th>状态</th></tr></thead><tbody>';
c.lines.forEach(function(l){
var tc={'已完成':'tag-green','配送中':'tag-blue','待配送':'tag-orange','备货中':'tag-gray'};
if(c.ncType==='nc'){
@@ -746,8 +728,8 @@ function showDetail(claimNo){
}else{
h+='<tr><td colspan="2"><b>'+l.foodName+'</b></td>';
}
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
if(c.ncType==='cp'){var ings=dishIngredients[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="6" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}}
h+='<td>'+l.spec+'</td><td>'+l.qty+'</td><td>'+l.weight+'</td><td>'+(l.price||0)+'</td><td>'+formatMoney(l.subtotal||0)+'</td><td><span class="tag '+tc[l.status]+'">'+l.status+'</span></td></tr>';
if(c.ncType==='cp'){var ings=dishIngredients[l.foodName]||[];if(ings.length>0){h+='<tr><td colspan="8" style="padding:4px 10px;background:#fafafa;"><span style="font-size:11px;color:var(--text-placeholder);">食材构成:</span>';ings.forEach(function(ing,i){if(i>0)h+=', ';h+=ing.name+'('+ing.amount+')';});h+='</td></tr>';}}
});
h+='</tbody></table></div>';
var dh='<div class="drawer-overlay" onclick="if(event.target===this)closeDrawer()"><div class="drawer"><div class="drawer__header"><div class="drawer__title">净菜内供详情 — '+claimNo+'</div><button class="drawer__close" onclick="closeDrawer()">✕</button></div><div class="drawer__body">'+h+'</div></div></div>';