feat: 配送H5订单详情取货方式拆分(自助/到店取货) + 营养管理端餐线管理重构与餐盘柜餐盘编码

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-08-20 14:28:31 +08:00
co-authored by Claude Opus 4.7
parent d20f3913f6
commit c8322529cb
10 changed files with 1283 additions and 88 deletions
+2
View File
@@ -879,6 +879,8 @@ const SITE_MAP = [
{label:'派单接听 · 核心', file:'02-dispatch-incoming.html'},
{label:'抢单大厅', file:'03-grab-pool.html'},
{label:'订单详情', file:'04-order-detail.html'},
{label:'订单详情 · 自助取货弹窗', file:'04-order-detail-selfpickup-dialog.html'},
{label:'订单详情 · 到店取货弹窗', file:'04-order-detail-storepickup-dialog.html'},
{label:'配送中', file:'05-delivering.html'},
{label:'送达签收', file:'06-finish-sign.html'},
{label:'历史订单', file:'07-history.html'},
+1 -1
View File
@@ -510,7 +510,7 @@
<div class="dispatch-banner" onclick="location.href='02-dispatch-incoming.html'">
<div class="dispatch-banner__icon">📲</div>
<div class="dispatch-banner__text">
<div class="dispatch-banner__title">您有新的派单 · 倒计时 25s</div>
<div class="dispatch-banner__title">您有新的派单 · 已等待 5 分钟</div>
<div class="dispatch-banner__desc">味之都食堂 → 高新区科技路 12 号 · 1.8km</div>
</div>
<div class="dispatch-banner__arrow"></div>
@@ -0,0 +1,367 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自助取货弹窗 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--success: #10b981;
--info: #3b82f6;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
}
body {
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
/* 遮罩:模拟弹窗悬浮于订单详情页之上 */
.mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
/* 底部弹层 */
.sheet {
position: absolute;
left: 0; right: 0; bottom: 0;
background: var(--background);
border-radius: 20px 20px 0 0;
padding: 12px 16px 40px;
z-index: 11;
display: flex;
flex-direction: column;
gap: 12px;
}
.sheet__grab {
width: 36px; height: 4px;
background: var(--border);
border-radius: 2px;
margin: 0 auto;
}
.sheet__title {
text-align: center;
font-size: 16px;
font-weight: 700;
}
.card {
background: white;
border-radius: 14px;
padding: 14px;
box-shadow: var(--shadow-sm);
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.card__title {
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
gap: 6px;
}
.card__title::before {
content: '';
width: 4px; height: 14px;
background: var(--primary);
border-radius: 2px;
}
/* 货柜信息 */
.locker-row {
display: flex;
gap: 12px;
align-items: center;
}
.locker-icon {
width: 52px; height: 52px;
border-radius: 12px;
background: linear-gradient(135deg, #dbeafe, #eff6ff);
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
flex-shrink: 0;
}
.locker-info { flex: 1; }
.locker-name {
font-size: 14px;
font-weight: 700;
margin-bottom: 3px;
}
.locker-addr {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.locker-cells {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--border);
display: flex;
gap: 8px;
}
.cell-tag {
flex: 1;
background: var(--primary-light);
border: 1px dashed var(--primary);
border-radius: 10px;
padding: 8px;
text-align: center;
}
.cell-tag__label {
font-size: 10px;
color: var(--text-secondary);
display: block;
margin-bottom: 2px;
}
.cell-tag__value {
font-size: 14px;
font-weight: 700;
color: var(--primary-dark);
}
/* 取货码 */
.pickup-code-card {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
border-radius: 14px;
padding: 16px;
color: white;
text-align: center;
}
.pickup-code-card__label {
font-size: 12px;
opacity: 0.9;
margin-bottom: 8px;
}
.pickup-code {
font-size: 40px;
font-weight: 800;
letter-spacing: 10px;
font-family: "SF Mono", Menlo, Consolas, monospace;
margin-bottom: 8px;
}
.pickup-code-card__tip {
font-size: 11px;
opacity: 0.85;
}
/* 拍照留证 */
.photo-grid {
display: flex;
gap: 10px;
}
.photo-thumb {
width: 96px; height: 96px;
border-radius: 10px;
background: linear-gradient(135deg, #fef3c7, #fed7aa);
display: flex;
align-items: center;
justify-content: center;
font-size: 34px;
position: relative;
flex-shrink: 0;
}
.photo-thumb__tag {
position: absolute;
bottom: 0; left: 0; right: 0;
background: rgba(0, 0, 0, 0.45);
color: white;
font-size: 10px;
padding: 3px 0;
border-radius: 0 0 10px 10px;
}
.photo-add {
width: 96px; height: 96px;
border-radius: 10px;
border: 1.5px dashed var(--text-tertiary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--text-secondary);
font-size: 11px;
cursor: pointer;
background: white;
flex-shrink: 0;
}
.photo-add__plus { font-size: 26px; color: var(--text-tertiary); line-height: 1; }
.photo-tip {
font-size: 11px;
color: var(--text-secondary);
margin-top: 8px;
}
/* 操作按钮 */
.btn {
height: 48px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
}
.btn--primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
box-shadow: 0 6px 12px rgba(255, 109, 58, 0.25);
}
.btn--ghost {
background: transparent;
color: var(--text-secondary);
height: 36px;
font-size: 13px;
font-weight: 500;
}
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
z-index: 20;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
white-space: nowrap;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<!-- 半透明遮罩,点击返回订单详情页 -->
<div class="mask" onclick="location.href='04-order-detail.html'"></div>
<!-- 交互说明:自助取货弹窗(底部弹层)。
场景:加工厂已将做好的订单餐品放入智能货柜,骑手到货柜处凭取货码开柜自取。
功能:1.查看货柜位置与格口编号 2.查看取货码 3.拍照对物品确认 -->
<div class="sheet">
<div class="sheet__grab"></div>
<div class="sheet__title">📦 自助取货</div>
<!-- 货柜详情 -->
<div class="card">
<div class="card__head">
<div class="card__title">货柜详情</div>
<span style="font-size:12px;color:var(--success);font-weight:600">● 餐品已入柜</span>
</div>
<div class="locker-row">
<div class="locker-icon">🗄️</div>
<div class="locker-info">
<div class="locker-name">味之都食堂·高新店 智能货柜</div>
<div class="locker-addr">高新区科技三路 8 号 · 店门东侧 5 米(临街)</div>
</div>
</div>
<div class="locker-cells">
<div class="cell-tag">
<span class="cell-tag__label">格口编号</span>
<span class="cell-tag__value">A-12</span>
</div>
<div class="cell-tag">
<span class="cell-tag__label">格口类型</span>
<span class="cell-tag__value">保温格口</span>
</div>
<div class="cell-tag">
<span class="cell-tag__label">入柜时间</span>
<span class="cell-tag__value">14:32</span>
</div>
</div>
</div>
<!-- 取货码 -->
<div class="pickup-code-card">
<div class="pickup-code-card__label">取货码(在货柜屏幕输入后开柜)</div>
<div class="pickup-code">862491</div>
<div class="pickup-code-card__tip">🧊 保温中 · 请在 15:20 前取货</div>
</div>
<!-- 拍照确认 -->
<div class="card">
<div class="card__head">
<div class="card__title">拍照确认物品</div>
<span style="font-size:12px;color:var(--text-secondary)">1/3 张</span>
</div>
<div class="photo-grid">
<!-- 交互说明:已拍摄照片缩略图示意 -->
<div class="photo-thumb">🍱<span class="photo-thumb__tag">14:35 已拍</span></div>
<!-- 交互说明:点击调起相机,开柜后对餐品拍照留证 -->
<div class="photo-add" onclick="toast('已调起相机')">
<span class="photo-add__plus"></span>
<span>拍摄餐品</span>
</div>
</div>
<div class="photo-tip">📸 请拍摄开柜后餐品状态,作为取货确认凭证</div>
</div>
<!-- 交互说明:点击「拍照确认取货」→ 提示取货成功并进入配送中 -->
<button class="btn btn--primary" onclick="confirmPickup()">📸 确认取货</button>
<button class="btn btn--ghost" onclick="location.href='04-order-detail.html'">返回订单详情</button>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
// 交互说明:确认取货后跳转配送中页面
function confirmPickup() {
toast('取货成功,开始配送');
setTimeout(() => location.href = '05-delivering.html', 1200);
}
</script>
</body>
</html>
@@ -0,0 +1,315 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>到店取货弹窗 | 营养餐品配送</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #ff6d3a;
--primary-dark: #e85a28;
--primary-light: #fff0e8;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--background: #f6f8fb;
--success: #10b981;
--shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
}
body {
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
background: #2a2a2a;
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
}
.phone-shell {
position: relative;
width: 375px;
height: 812px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--background);
border-radius: 44px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), inset 0 0 0 2px #e0e0e0;
}
/* 遮罩:模拟弹窗悬浮于订单详情页之上 */
.mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
/* 底部弹层 */
.sheet {
position: absolute;
left: 0; right: 0; bottom: 0;
background: var(--background);
border-radius: 20px 20px 0 0;
padding: 12px 16px 40px;
z-index: 11;
display: flex;
flex-direction: column;
gap: 12px;
}
.sheet__grab {
width: 36px; height: 4px;
background: var(--border);
border-radius: 2px;
margin: 0 auto;
}
.sheet__title {
text-align: center;
font-size: 16px;
font-weight: 700;
}
.card {
background: white;
border-radius: 14px;
padding: 14px;
box-shadow: var(--shadow-sm);
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.card__title {
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
gap: 6px;
}
.card__title::before {
content: '';
width: 4px; height: 14px;
background: var(--primary);
border-radius: 2px;
}
/* 商家信息 */
.store-row {
display: flex;
gap: 12px;
align-items: center;
}
.store-icon {
width: 52px; height: 52px;
border-radius: 12px;
background: linear-gradient(135deg, #fef3c7, #fde68a);
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
flex-shrink: 0;
}
.store-info { flex: 1; }
.store-name {
font-size: 14px;
font-weight: 700;
margin-bottom: 3px;
}
.store-addr {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
}
.store-actions {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--border);
display: flex;
gap: 8px;
}
.contact-tag {
background: var(--primary-light);
color: var(--primary-dark);
padding: 5px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
/* 拍照留存 */
.photo-grid {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.photo-thumb {
width: 96px; height: 96px;
border-radius: 10px;
background: linear-gradient(135deg, #fef3c7, #fed7aa);
display: flex;
align-items: center;
justify-content: center;
font-size: 34px;
position: relative;
flex-shrink: 0;
}
.photo-thumb__tag {
position: absolute;
bottom: 0; left: 0; right: 0;
background: rgba(0, 0, 0, 0.45);
color: white;
font-size: 10px;
padding: 3px 0;
border-radius: 0 0 10px 10px;
}
.photo-add {
width: 96px; height: 96px;
border-radius: 10px;
border: 1.5px dashed var(--text-tertiary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--text-secondary);
font-size: 11px;
cursor: pointer;
background: white;
flex-shrink: 0;
}
.photo-add__plus { font-size: 26px; color: var(--text-tertiary); line-height: 1; }
.photo-tip {
font-size: 11px;
color: var(--text-secondary);
margin-top: 8px;
line-height: 1.6;
}
/* 操作按钮 */
.btn {
height: 48px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
}
.btn--primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
box-shadow: 0 6px 12px rgba(255, 109, 58, 0.25);
}
.btn--ghost {
background: transparent;
color: var(--text-secondary);
height: 36px;
font-size: 13px;
font-weight: 500;
}
.home-indicator {
position: absolute;
bottom: 8px; left: 50%;
transform: translateX(-50%);
width: 134px; height: 5px;
background: #1f2937;
border-radius: 3px;
z-index: 20;
}
.toast {
position: absolute;
top: 60px; left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
white-space: nowrap;
}
.toast.show { opacity: 1; }
</style>
</head>
<body>
<div class="phone-shell">
<!-- 半透明遮罩,点击返回订单详情页 -->
<div class="mask" onclick="location.href='04-order-detail.html'"></div>
<!-- 交互说明:到店取货弹窗(底部弹层)。
场景:骑手到商家门店现场取货。
功能:1.查看商家位置/联系商家 2.拍照留存取货照片 3.开始配送 -->
<div class="sheet">
<div class="sheet__grab"></div>
<div class="sheet__title">🏪 到店取货</div>
<!-- 商家信息 -->
<div class="card">
<div class="card__head">
<div class="card__title">取货门店</div>
<span style="font-size:12px;color:var(--success);font-weight:600">● 餐品已出餐</span>
</div>
<div class="store-row">
<div class="store-icon">🏢</div>
<div class="store-info">
<div class="store-name">味之都食堂·高新店</div>
<div class="store-addr">高新区科技三路 8 号 · 联系人:王经理</div>
</div>
</div>
<div class="store-actions">
<a href="javascript:void(0)" class="contact-tag" onclick="toast('正在呼叫商家')">📞 呼叫商家</a>
<a href="javascript:void(0)" class="contact-tag" onclick="toast('已打开导航')">📍 导航到店</a>
</div>
</div>
<!-- 拍照留存 -->
<div class="card">
<div class="card__head">
<div class="card__title">取货拍照留存</div>
<span style="font-size:12px;color:var(--text-secondary)">1/3 张</span>
</div>
<div class="photo-grid">
<!-- 交互说明:已拍摄照片缩略图示意 -->
<div class="photo-thumb">🍱<span class="photo-thumb__tag">14:36 已拍</span></div>
<!-- 交互说明:点击调起相机,对取货餐品/交接现场拍照留证 -->
<div class="photo-add" onclick="toast('已调起相机')">
<span class="photo-add__plus"></span>
<span>拍摄取货现场</span>
</div>
</div>
<div class="photo-tip">📸 请拍摄餐品包装与交接现场,作为取货留存凭证;拍摄完成后即可开始配送</div>
</div>
<!-- 交互说明:点击「拍照并开始配送」→ 跳转配送中页面 -->
<button class="btn btn--primary" onclick="location.href='05-delivering.html'">📸 开始配送</button>
<button class="btn btn--ghost" onclick="location.href='04-order-detail.html'">返回订单详情</button>
</div>
<div class="home-indicator"></div>
<div class="toast" id="toast"></div>
</div>
<script>
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 1800);
}
</script>
</body>
</html>
@@ -359,6 +359,17 @@
color: white;
box-shadow: 0 6px 12px rgba(255, 109, 58, 0.25);
}
.btn--outline {
background: white;
color: var(--primary);
border: 1.5px solid var(--primary);
}
.btn__sub {
display: block;
font-size: 10px;
font-weight: 400;
opacity: 0.85;
}
.tag-list {
display: flex;
@@ -572,6 +583,10 @@
<span class="info-label">收货方式</span>
<span class="info-value" style="color:var(--primary)">🔢 验证码收货</span>
</div>
<div class="info-row">
<span class="info-label">取货方式</span>
<span class="info-value" style="color:var(--info)">📦 自助取货 / 🏪 到店取货</span>
</div>
</div>
</div>
@@ -587,8 +602,12 @@
</div>
<!-- 交互说明:底部双按钮,取货方式二选一。
自助取货 = 加工厂已将餐品放入智能货柜,骑手凭验证码开柜自取;
到店取货 = 骑手到商家门店现场取货 -->
<div class="action-bar">
<button class="btn btn--primary" onclick="location.href='05-delivering.html'">🚀 到店报到</button>
<button class="btn btn--primary" onclick="location.href='04-order-detail-selfpickup-dialog.html'">📦 自助取货</button>
<button class="btn btn--outline" onclick="location.href='04-order-detail-storepickup-dialog.html'">🏪 到店取货</button>
</div>
<div class="home-indicator"></div>
@@ -1,5 +1,30 @@
# 营养管理餐品配送 H5 模块变更日志
## 2026-08-19 · 订单详情取货方式拆分(自助取货 / 到店取货)
### 新增
- **`04-order-detail-selfpickup-dialog.html` 自助取货弹窗**:加工厂将做好的餐品放入智能货柜,骑手凭取货码开柜自取
- 货柜详情:货柜名称/位置/格口编号(A-12 保温格口)/入柜时间
- 取货码展示:大字号验证码(862491),提示在货柜屏幕输入开柜,含取货时限
- 拍照确认物品:已拍缩略图 + 调起相机卡槽
- 「拍照确认取货」→ 提示取货成功并跳转配送中
- **`04-order-detail-storepickup-dialog.html` 到店取货弹窗**:骑手到商家门店现场取货
- 取货门店信息:商家名/地址/呼叫商家/导航到店
- 取货拍照留存:餐品包装与交接现场拍照
- 「拍照并开始配送」→ 跳转配送中
### 优化
- **`04-order-detail.html` 底部按钮拆分**:原「到店报到」单按钮拆为「📦 自助取货(智能货柜·凭码开柜)」+「🏪 到店取货(现场拍照·开始配送)」双按钮
- **`04-order-detail.html` 订单信息新增「取货方式」行**
- **`01-home.html` 派单横幅文案调整**:「倒计时 25s」→「已等待 5 分钟」
### 业务约定更新
- 取货方式二选一:自助取货(智能货柜验证码开柜) / 到店取货(门店现场交接)
- 自助取货流程:加工厂入柜 → 骑手查看货柜位置/格口 → 凭取货码开柜 → 拍照确认 → 开始配送
- 到店取货流程:骑手到店 → 拍照留存取货凭证 → 开始配送
---
## 2026-08-14 · 派单/配送流程降噪与信息补全
### 优化
+459 -46
View File
@@ -66,23 +66,78 @@ tr:hover td{background:#FAFAFA;}
.pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 20px;font-size:var(--font-xs);color:var(--text-secondary);}
.page-btn{width:28px;height:28px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#fff;cursor:pointer;font-size:var(--font-xs);}
.page-btn.active{background:var(--primary);color:#fff;border-color:var(--primary);}
/* 标签 */
.tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:var(--font-xs);}
.tag-blue{background:#E6F7FF;color:#1890FF;}
.tag-green{background:#F6FFED;color:#52C41A;}
.tag-orange{background:#FFF7E6;color:#FA8C16;}
.tag-gray{background:#F5F5F5;color:#999;}
/* 胶囊开关 */
.pill-switch{display:inline-block;min-width:52px;height:22px;line-height:22px;padding:0 10px;border-radius:11px;font-size:var(--font-xs);text-align:center;cursor:pointer;user-select:none;transition:all .2s;color:#fff;}
.pill-switch.on{background:var(--success);}
.pill-switch.off{background:#BFBFBF;}
/* 设备数量(可点击数字) */
.dev-count{color:var(--primary);cursor:pointer;font-weight:600;text-decoration:none;border-bottom:1px dashed var(--primary);}
.dev-count:hover{color:#40a9ff;}
/* 弹窗 */
.modal-mask{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:1000;align-items:center;justify-content:center;}
.modal-mask.show{display:flex;}
.modal{background:#fff;border-radius:var(--radius-md);width:480px;box-shadow:var(--shadow-md);}
.modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);}
.modal{background:#fff;border-radius:var(--radius-md);width:480px;box-shadow:var(--shadow-md);display:flex;flex-direction:column;max-height:88vh;}
.modal.modal-lg{width:900px;}
.modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);flex-shrink:0;}
.modal__title{font-size:var(--font-base);font-weight:600;}
.modal__close{font-size:22px;cursor:pointer;color:var(--text-secondary);line-height:1;}.modal__close:hover{color:var(--text-primary);}
.modal__body{padding:24px;}
.modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 24px;border-top:1px solid var(--border);}
.modal__body{padding:24px;overflow-y:auto;flex:1;}
.modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 24px;border-top:1px solid var(--border);flex-shrink:0;}
.form-row{margin-bottom:16px;}
.form-row:last-child{margin-bottom:0;}
.form-label{display:block;font-size:var(--font-sm);color:var(--text-primary);margin-bottom:6px;}
.form-label em{color:var(--danger);font-style:normal;}
.form-input{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);}
.form-input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.1);}
.form-input,.form-select{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);background:#fff;}
.form-input:focus,.form-select:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 2px rgba(24,144,255,0.1);}
.form-row-inline{display:flex;gap:16px;align-items:flex-start;}
.form-row-inline .form-row{flex:1;}
.form-row-inline .form-row.status-row{flex:0 0 140px;}
/* 分区标题 */
.form-section-title{font-size:var(--font-base);font-weight:600;color:var(--text-primary);margin:20px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.form-section-title:first-child{margin-top:0;}
.form-section-title .btn-link{font-size:var(--font-sm);font-weight:400;border:1px dashed var(--primary);color:var(--primary);background:transparent;padding:4px 12px;border-radius:var(--radius-sm);cursor:pointer;}
.form-section-title .btn-link:hover{background:var(--primary-light);}
/* 批量设置条 */
.batch-bar{display:flex;align-items:center;gap:8px;background:#F8FBFF;border:1px dashed #91D5FF;border-radius:var(--radius-sm);padding:10px 14px;margin-bottom:12px;font-size:var(--font-sm);}
.batch-bar label{color:var(--text-secondary);white-space:nowrap;}
.batch-bar .form-select{width:auto;min-width:120px;}
.batch-bar .form-input{width:110px;}
/* 收费模式矩阵 */
.charge-table-wrap{overflow-x:auto;}
.charge-table{min-width:760px;border-collapse:collapse;}
.charge-table th{background:#FAFAFA;font-weight:600;text-align:center;padding:8px;border:1px solid var(--border);font-size:var(--font-sm);}
.charge-table td{padding:6px;border:1px solid #f0f0f0;text-align:center;}
.charge-table .ct-week{background:#FAFAFA;font-weight:500;color:var(--text-secondary);width:52px;}
.cm-mode{width:100px;height:28px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--font-xs);background:#fff;outline:none;}
.cm-mode:focus{border-color:var(--primary);}
.cm-price{width:64px;height:28px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--font-xs);padding:0 6px;text-align:center;outline:none;margin-left:4px;}
.cm-price:focus{border-color:var(--primary);}
.cm-price:disabled{background:#F5F5F5;color:#999;cursor:not-allowed;}
.cm-unit{font-size:var(--font-xs);color:var(--text-secondary);margin-left:2px;}
/* 弹窗内表格 */
.modal-table{width:100%;border-collapse:collapse;font-size:var(--font-sm);}
.modal-table th{background:#FAFAFA;font-weight:600;padding:9px 12px;text-align:left;border-bottom:1px solid var(--border);white-space:nowrap;color:var(--text-secondary);}
.modal-table td{padding:9px 12px;border-bottom:1px solid #f0f0f0;color:var(--text-primary);white-space:nowrap;}
.modal-table tr:last-child td{border-bottom:none;}
.modal-table tr:hover td{background:#FAFAFA;}
/* 弹窗内分页 */
.modal-pagination{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:12px;font-size:var(--font-xs);color:var(--text-secondary);}
/* 详情回显字段 */
.detail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 24px;}
.detail-field{display:flex;flex-direction:column;gap:4px;}
.detail-field__label{font-size:var(--font-xs);color:var(--text-secondary);}
.detail-field__value{font-size:var(--font-sm);color:var(--text-primary);font-weight:500;}
/* 只读收费矩阵 */
.charge-read td{font-size:var(--font-xs);}
.cr-byMeal{color:#1890FF;}
.cr-byWeight{color:#FA8C16;}
.cr-free{color:#999;}
</style>
</head>
<body>
@@ -210,53 +265,95 @@ tr:hover td{background:#FAFAFA;}
<!-- 餐线列表 -->
<div class="action-bar">
<button class="btn btn-primary" onclick="openModal('add')"> 新增餐线</button>
<button class="btn btn-primary" onclick="openLineModal(null)"> 新增餐线</button>
</div>
<div class="table-card">
<div class="table-card__header">
<div class="table-card__title">餐线列表</div>
<span style="font-size:var(--font-xs);color:var(--text-secondary);">3</span>
<span style="font-size:var(--font-xs);color:var(--text-secondary);"><span id="lineCount">0</span></span>
</div>
<div style="overflow-x:auto;">
<table>
<thead><tr><th>餐线名称</th><th>设备数量</th><th>操作</th></tr></thead>
<tbody>
<tr>
<td>A餐线(自选餐)</td>
<td>6 台</td>
<td><span class="link" onclick="openModal('edit','A餐线(自选餐)')">编辑</span><span class="link link-danger">删除</span></td>
</tr>
<tr>
<td>B餐线(营养点餐)</td>
<td>4 台</td>
<td><span class="link" onclick="openModal('edit','B餐线(营养点餐)')">编辑</span><span class="link link-danger">删除</span></td>
</tr>
<tr>
<td>C餐线(清真)</td>
<td>2 台</td>
<td><span class="link" onclick="openModal('edit','C餐线(清真)')">编辑</span><span class="link link-danger">删除</span></td>
</tr>
</tbody>
<thead><tr><th>餐线名称</th><th>结算方式</th><th>设备数量</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="lineTbody"></tbody>
</table>
</div>
<div class="pagination"><span>显示 3 条,共 3</span><button class="page-btn active">1</button></div>
<div class="pagination"><span id="linePager">显示 0 条,共 0</span><button class="page-btn active">1</button></div>
</div>
</main>
<!-- 新增/编辑餐线弹窗 -->
<div class="modal-mask" id="lineModal" onclick="closeModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal__header"><span class="modal__title" id="modalTitle">新增餐线</span><span class="modal__close" onclick="closeModal()">&times;</span></div>
<!-- 餐线设置 / 新增餐线 弹窗 -->
<div class="modal-mask" id="lineModal" onclick="closeLineModal(event)">
<div class="modal modal-lg" onclick="event.stopPropagation()">
<div class="modal__header"><span class="modal__title" id="lineModalTitle">新增餐线</span><span class="modal__close" onclick="closeLineModal()">&times;</span></div>
<div class="modal__body">
<div class="form-row">
<label class="form-label"><em>*</em> 餐线名称</label>
<input class="form-input" id="lineName" placeholder="请输入餐线名称">
<!-- 基础信息 -->
<div class="form-section-title">基础信息</div>
<div class="form-row-inline">
<div class="form-row">
<label class="form-label"><em>*</em> 餐线名称</label>
<input class="form-input" id="lineName" placeholder="请输入餐线名称">
</div>
<div class="form-row">
<label class="form-label"><em>*</em> 结算方式</label>
<select class="form-select" id="lineSettle">
<option value="auto">自动结算</option>
<option value="manual">手动结算</option>
</select>
</div>
<div class="form-row status-row">
<label class="form-label">状态</label>
<span class="pill-switch on" id="lineStatusSwitch" onclick="toggleLineStatusSwitch()">启用</span>
</div>
</div>
<!-- 收费模式 -->
<div class="form-section-title">
<span>收费模式</span>
<button class="btn-link" onclick="toggleBatchBar()">⚡ 批量设置</button>
</div>
<div class="batch-bar" id="batchBar" style="display:none;">
<label>收费模式</label>
<select class="form-select" id="batchMode" onchange="onBatchModeChange()">
<option value="byMeal">按餐计费</option>
<option value="byWeight">称重计费</option>
<option value="free">免费</option>
</select>
<label id="batchPriceLabel">计费金额</label>
<input class="form-input" id="batchPrice" placeholder="金额(元)">
<button class="btn btn-primary" onclick="applyBatch()">应用到全部餐次</button>
<button class="btn btn-default" onclick="toggleBatchBar()">收起</button>
</div>
<div class="charge-table-wrap">
<table class="charge-table" id="chargeTable"></table>
</div>
</div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeModal()">取消</button>
<button class="btn btn-primary">确定</button>
<button class="btn btn-default" onclick="closeLineModal()">取消</button>
<button class="btn btn-primary" onclick="confirmLine()">确定</button>
</div>
</div>
</div>
<!-- 设备列表概况弹窗 -->
<div class="modal-mask" id="deviceModal" onclick="closeDeviceModal(event)">
<div class="modal" style="width:640px;" onclick="event.stopPropagation()">
<div class="modal__header"><span class="modal__title" id="deviceModalTitle">设备列表</span><span class="modal__close" onclick="closeDeviceModal()">&times;</span></div>
<div class="modal__body" id="deviceModalBody"></div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeDeviceModal()">关闭</button>
</div>
</div>
</div>
<!-- 餐线详情弹窗 -->
<div class="modal-mask" id="detailModal" onclick="closeDetailModal(event)">
<div class="modal modal-lg" onclick="event.stopPropagation()">
<div class="modal__header"><span class="modal__title" id="detailModalTitle">餐线详情</span><span class="modal__close" onclick="closeDetailModal()">&times;</span></div>
<div class="modal__body" id="detailModalBody"></div>
<div class="modal__footer">
<button class="btn btn-default" onclick="closeDetailModal()">关闭</button>
</div>
</div>
</div>
@@ -265,23 +362,339 @@ tr:hover td{background:#FAFAFA;}
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');})();
function openModal(mode, name){
var modal=document.getElementById('lineModal');
var title=document.getElementById('modalTitle');
var input=document.getElementById('lineName');
if(mode==='edit'){
title.textContent='编辑餐线';
input.value=name||'';
/* ========== 常量 ========== */
var MEALS=['早餐','午餐','晚餐','加餐'];
var WEEKDAYS=['周一','周二','周三','周四','周五','周六','周日'];
var MODE_OPTIONS=[
{value:'byMeal',label:'按餐计费'},
{value:'byWeight',label:'称重计费'},
{value:'free',label:'免费'}
];
/* 生成收费模式矩阵:7天×4餐次,同一模式 */
function genCharging(mode, prices){
return WEEKDAYS.map(function(){
return MEALS.map(function(_, mi){
return {mode:mode, price: mode==='byMeal' ? (prices&&prices[mi]||'') : ''};
});
});
}
/* ========== 餐线数据 ========== */
var lines=[
{
id:'L1', name:'A餐线(自选餐)', settleType:'auto', status:'on',
charging: genCharging('byMeal', [8,15,15,6]),
devices:[
{type:'营养秤', code:'CL-A01', name:'餐线营养秤1号', days:326, online:true},
{type:'营养秤', code:'CL-A02', name:'餐线营养秤2号', days:326, online:true},
{type:'营养秤', code:'CL-A03', name:'餐线营养秤3号', days:210, online:false},
{type:'结算终端', code:'CL-A04', name:'结算终端1号', days:326, online:true},
{type:'结算终端', code:'CL-A05', name:'结算终端2号', days:300, online:true},
{type:'餐盘机', code:'CL-A06', name:'智能餐盘机1号', days:120, online:true}
]
},
{
id:'L2', name:'B餐线(营养点餐)', settleType:'auto', status:'on',
charging: genCharging('byWeight'),
devices:[
{type:'营养秤', code:'CL-B01', name:'餐线营养秤4号', days:400, online:true},
{type:'营养秤', code:'CL-B02', name:'餐线营养秤5号', days:400, online:true},
{type:'结算终端', code:'CL-B03', name:'结算终端3号', days:400, online:false},
{type:'餐盘机', code:'CL-B04', name:'智能餐盘机2号', days:180, online:true}
]
},
{
id:'L3', name:'C餐线(清真)', settleType:'manual', status:'off',
charging: genCharging('free'),
devices:[
{type:'营养秤', code:'CL-C01', name:'餐线营养秤6号', days:90, online:true},
{type:'结算终端', code:'CL-C02', name:'结算终端4号', days:90, online:false}
]
}
];
/* ========== 列表渲染 ========== */
function renderLines(){
document.getElementById('lineCount').textContent=lines.length;
document.getElementById('linePager').textContent='显示 '+lines.length+' 条,共 '+lines.length+' 条';
document.getElementById('lineTbody').innerHTML=lines.map(function(l){
var settle=l.settleType==='auto'?'自动结算':'手动结算';
var settleCls=l.settleType==='auto'?'tag-green':'tag-gray';
var status=l.status==='on'
? '<span class="pill-switch on" onclick="toggleLineStatus(\''+l.id+'\')">启用</span>'
: '<span class="pill-switch off" onclick="toggleLineStatus(\''+l.id+'\')">停用</span>';
var devCount='<a class="dev-count" onclick="openDeviceModal(\''+l.id+'\')">'+l.devices.length+'</a>';
return '<tr>'+
'<td>'+l.name+'</td>'+
'<td><span class="tag '+settleCls+'">'+settle+'</span></td>'+
'<td>'+devCount+'</td>'+
'<td>'+status+'</td>'+
'<td><a class="link" onclick="openLineModal(\''+l.id+'\')">餐线设置</a><a class="link" onclick="openLineDetail(\''+l.id+'\')">详情</a><a class="link link-danger" onclick="deleteLine(\''+l.id+'\')">删除</a></td>'+
'</tr>';
}).join('');
}
function toggleLineStatus(id){
var l=lines.find(function(x){return x.id===id;});
if(!l) return;
l.status=l.status==='on'?'off':'on';
renderLines();
}
function deleteLine(id){
var l=lines.find(function(x){return x.id===id;});
if(!l) return;
if(!confirm('确认删除餐线「'+l.name+'」?')) return;
lines=lines.filter(function(x){return x.id!==id;});
renderLines();
}
/* ========== 餐线设置弹窗 ========== */
var editingLineId=null;
var tempCharging=null;
var tempStatus='on';
function openLineModal(id){
editingLineId=id;
var title=document.getElementById('lineModalTitle');
var nameInput=document.getElementById('lineName');
var settle=document.getElementById('lineSettle');
document.getElementById('batchBar').style.display='none';
document.getElementById('batchMode').value='byMeal';
document.getElementById('batchPrice').value='';
onBatchModeChange();
if(id){
var l=lines.find(function(x){return x.id===id;});
if(!l) return;
title.textContent='餐线设置';
nameInput.value=l.name;
settle.value=l.settleType;
tempStatus=l.status;
tempCharging=JSON.parse(JSON.stringify(l.charging));
}else{
title.textContent='新增餐线';
input.value='';
nameInput.value='';
settle.value='auto';
tempStatus='on';
tempCharging=genCharging('free');
}
modal.classList.add('show');
renderStatusSwitch();
renderChargeTable();
document.getElementById('lineModal').classList.add('show');
}
function closeModal(e){
function closeLineModal(e){
if(e&&e.target!==e.currentTarget)return;
document.getElementById('lineModal').classList.remove('show');
}
function renderStatusSwitch(){
var sw=document.getElementById('lineStatusSwitch');
sw.classList.toggle('on',tempStatus==='on');
sw.classList.toggle('off',tempStatus!=='on');
sw.textContent=tempStatus==='on'?'启用':'停用';
}
function toggleLineStatusSwitch(){
tempStatus=tempStatus==='on'?'off':'on';
renderStatusSwitch();
}
/* ========== 收费模式矩阵 ========== */
function renderChargeTable(){
var html='<thead><tr><th>星期</th>';
MEALS.forEach(function(m){html+='<th>'+m+'</th>';});
html+='</tr></thead><tbody>';
for(var w=0;w<7;w++){
html+='<tr><td class="ct-week">'+WEEKDAYS[w]+'</td>';
for(var m=0;m<4;m++){
var cell=tempCharging[w][m];
var opts=MODE_OPTIONS.map(function(o){
return '<option value="'+o.value+'"'+(cell.mode===o.value?' selected':'')+'>'+o.label+'</option>';
}).join('');
var isMeal=cell.mode==='byMeal';
html+='<td>'+
'<select class="cm-mode" data-w="'+w+'" data-m="'+m+'" onchange="onModeChange(this)">'+opts+'</select>'+
'<input class="cm-price" data-w="'+w+'" data-m="'+m+'" value="'+(cell.price||'')+'" placeholder="'+(isMeal?'金额':'-')+'" '+(isMeal?'':'disabled')+' onchange="onPriceChange(this)">'+
'<span class="cm-unit" '+(isMeal?'':'style="display:none"')+'>元</span>'+
'</td>';
}
html+='</tr>';
}
html+='</tbody>';
document.getElementById('chargeTable').innerHTML=html;
}
function onModeChange(sel){
var w=parseInt(sel.dataset.w,10), m=parseInt(sel.dataset.m,10);
var mode=sel.value;
tempCharging[w][m].mode=mode;
if(mode!=='byMeal'){ tempCharging[w][m].price=''; }
/* 刷新该格金额框禁用态、占位符与单位显隐 */
var row=document.querySelectorAll('#chargeTable tbody tr')[w];
var cell=row.querySelectorAll('td')[m+1];
var priceInput=cell.querySelector('.cm-price');
var unit=cell.querySelector('.cm-unit');
var isMeal=(mode==='byMeal');
priceInput.disabled=!isMeal;
priceInput.placeholder=isMeal?'金额':'-';
priceInput.value=tempCharging[w][m].price||'';
unit.style.display=isMeal?'':'none';
}
function onPriceChange(inp){
var w=parseInt(inp.dataset.w,10), m=parseInt(inp.dataset.m,10);
tempCharging[w][m].price=inp.value;
}
/* 批量设置 */
function toggleBatchBar(){
var bar=document.getElementById('batchBar');
bar.style.display=bar.style.display==='none'?'flex':'none';
}
function onBatchModeChange(){
var mode=document.getElementById('batchMode').value;
var show=(mode==='byMeal');
document.getElementById('batchPriceLabel').style.display=show?'':'none';
document.getElementById('batchPrice').style.display=show?'':'none';
if(!show) document.getElementById('batchPrice').value='';
}
function applyBatch(){
var mode=document.getElementById('batchMode').value;
var price=document.getElementById('batchPrice').value.trim();
if(mode==='byMeal' && !price){ alert('按餐计费请填写计费金额'); return; }
for(var w=0;w<7;w++){
for(var m=0;m<4;m++){
tempCharging[w][m].mode=mode;
tempCharging[w][m].price=(mode==='byMeal'?price:'');
}
}
renderChargeTable();
}
/* 确认保存 */
function confirmLine(){
var name=document.getElementById('lineName').value.trim();
var settle=document.getElementById('lineSettle').value;
if(!name){ alert('请输入餐线名称'); return; }
for(var w=0;w<7;w++){
for(var m=0;m<4;m++){
var c=tempCharging[w][m];
if(c.mode==='byMeal' && !String(c.price).trim()){ alert('请为「'+WEEKDAYS[w]+' '+MEALS[m]+'」的按餐计费填写计费金额'); return; }
}
}
if(editingLineId){
var l=lines.find(function(x){return x.id===editingLineId;});
if(l){
l.name=name; l.settleType=settle; l.status=tempStatus;
l.charging=JSON.parse(JSON.stringify(tempCharging));
}
}else{
lines.push({
id:'L'+Date.now(),
name:name, settleType:settle, status:tempStatus,
charging:JSON.parse(JSON.stringify(tempCharging)),
devices:[]
});
}
closeLineModal();
renderLines();
}
/* ========== 设备列表概况弹窗 ========== */
function openDeviceModal(id){
var l=lines.find(function(x){return x.id===id;});
if(!l) return;
document.getElementById('deviceModalTitle').textContent='设备列表 — '+l.name;
var body=document.getElementById('deviceModalBody');
if(l.devices.length===0){
body.innerHTML='<div style="text-align:center;color:var(--text-placeholder);padding:24px 0;">该餐线暂无关联设备</div>';
}else{
var rows=l.devices.map(function(d){
var onlineTag=d.online?'<span class="tag tag-green">在线</span>':'<span class="tag tag-gray">离线</span>';
return '<tr><td>'+d.type+'</td><td>'+d.code+'</td><td>'+d.name+'</td><td>'+d.days+' 天</td><td>'+onlineTag+'</td></tr>';
}).join('');
body.innerHTML='<table class="modal-table">'+
'<thead><tr><th>设备类型</th><th>编号</th><th>设备名称</th><th>运行天数</th><th>在线状态</th></tr></thead>'+
'<tbody>'+rows+'</tbody>'+
'</table>';
}
document.getElementById('deviceModal').classList.add('show');
}
function closeDeviceModal(e){
if(e&&e.target!==e.currentTarget)return;
document.getElementById('deviceModal').classList.remove('show');
}
/* ========== 餐线详情弹窗 ========== */
var detailLineId=null;
var detailPage=1;
var DETAIL_PAGE_SIZE=5;
function openLineDetail(id){
var l=lines.find(function(x){return x.id===id;});
if(!l) return;
detailLineId=id;
detailPage=1;
document.getElementById('detailModalTitle').textContent='餐线详情 — '+l.name;
renderDetail();
document.getElementById('detailModal').classList.add('show');
}
function closeDetailModal(e){
if(e&&e.target!==e.currentTarget)return;
document.getElementById('detailModal').classList.remove('show');
}
function renderDetail(){
var l=lines.find(function(x){return x.id===detailLineId;});
if(!l) return;
var settle=l.settleType==='auto'?'自动结算':'手动结算';
var statusTag=l.status==='on'?'<span class="tag tag-green">启用</span>':'<span class="tag tag-gray">停用</span>';
/* 基础信息 */
var html='<div class="form-section-title">基础信息</div>'+
'<div class="detail-grid">'+
'<div class="detail-field"><span class="detail-field__label">餐线名称</span><span class="detail-field__value">'+l.name+'</span></div>'+
'<div class="detail-field"><span class="detail-field__label">结算方式</span><span class="detail-field__value">'+settle+'</span></div>'+
'<div class="detail-field"><span class="detail-field__label">状态</span><span class="detail-field__value">'+statusTag+'</span></div>'+
'</div>';
/* 收费模式只读回显 */
html+='<div class="form-section-title">收费模式</div>';
html+='<div class="charge-table-wrap"><table class="charge-table charge-read"><thead><tr><th>星期</th>';
MEALS.forEach(function(m){html+='<th>'+m+'</th>';});
html+='</tr></thead><tbody>';
for(var w=0;w<7;w++){
html+='<tr><td class="ct-week">'+WEEKDAYS[w]+'</td>';
for(var m=0;m<4;m++){
var c=l.charging[w][m];
var txt=c.mode==='byMeal'
? '<span class="cr-byMeal">按餐计费 '+c.price+' 元</span>'
: (c.mode==='byWeight'?'<span class="cr-byWeight">称重计费</span>':'<span class="cr-free">免费</span>');
html+='<td>'+txt+'</td>';
}
html+='</tr>';
}
html+='</tbody></table></div>';
/* 设备列表(带分页) */
html+='<div class="form-section-title">关联设备</div>';
var devices=l.devices;
if(devices.length===0){
html+='<div style="text-align:center;color:var(--text-placeholder);padding:16px 0;">暂无关联设备</div>';
}else{
var totalPages=Math.max(1,Math.ceil(devices.length/DETAIL_PAGE_SIZE));
if(detailPage>totalPages) detailPage=totalPages;
var start=(detailPage-1)*DETAIL_PAGE_SIZE;
var pageDevices=devices.slice(start,start+DETAIL_PAGE_SIZE);
var rows=pageDevices.map(function(d){
var onlineTag=d.online?'<span class="tag tag-green">在线</span>':'<span class="tag tag-gray">离线</span>';
return '<tr><td>'+d.type+'</td><td>'+d.code+'</td><td>'+d.name+'</td><td>'+d.days+' 天</td><td>'+onlineTag+'</td></tr>';
}).join('');
html+='<table class="modal-table">'+
'<thead><tr><th>设备类型</th><th>编号</th><th>设备名称</th><th>运行天数</th><th>在线状态</th></tr></thead>'+
'<tbody>'+rows+'</tbody></table>';
var pageBtns='';
for(var i=1;i<=totalPages;i++){
pageBtns+='<button class="page-btn'+(i===detailPage?' active':'')+'" onclick="detailGoPage('+i+')">'+i+'</button>';
}
html+='<div class="modal-pagination"><span>显示 '+pageDevices.length+' 条,共 '+devices.length+' 条</span>'+pageBtns+'</div>';
}
document.getElementById('detailModalBody').innerHTML=html;
}
function detailGoPage(p){
detailPage=p;
renderDetail();
}
/* 初始渲染 */
renderLines();
</script>
</body>
</html>
+54 -32
View File
@@ -192,6 +192,7 @@ tbody tr:hover{background:#F8FBFF;}
.ov-slot__name{font-size:var(--font-sm);font-weight:600;color:var(--text-primary);text-align:center;line-height:1.3;}
.ov-slot__remain{font-size:var(--font-sm);font-weight:700;color:var(--primary);}
.ov-slot__expire{font-size:10px;color:var(--text-placeholder);}
.ov-slot__plate{font-size:10px;color:var(--text-secondary);font-family:monospace;text-align:center;line-height:1.3;}
.ov-cabinet-legend{display:flex;gap:16px;padding:14px 4px 0;flex-wrap:wrap;}
.ov-legend__item{display:flex;align-items:center;gap:4px;font-size:var(--font-xs);color:var(--text-secondary);}
.ov-legend__dot{width:10px;height:10px;border-radius:2px;border:1.5px solid;}
@@ -254,6 +255,11 @@ tbody tr:hover{background:#F8FBFF;}
.member-picker__search{padding:14px 24px 0;flex-shrink:0;}
.member-picker__search input{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);outline:none;}
.member-picker__search input:focus{border-color:var(--primary);}
.plate-detail__field input{height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 10px;font-size:var(--font-sm);outline:none;font-family:monospace;}
.plate-detail__field input:focus{border-color:var(--primary);}
.plate-assign__search{margin-bottom:12px;}
.plate-assign__search input{width:100%;height:36px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 12px;font-size:var(--font-sm);outline:none;}
.plate-assign__search input:focus{border-color:var(--primary);}
.member-picker__body{padding:12px 24px 20px;overflow-y:auto;flex:1;}
.member-picker__item{display:flex;align-items:center;gap:12px;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:8px;cursor:pointer;transition:all .15s;}
.member-picker__item:hover{border-color:var(--primary);background:#F8FBFF;}
@@ -770,16 +776,16 @@ function renderSmartCabinet(){
/* ===== 餐盘柜:格口 + 会员绑定状态 ===== */
/* 格口 Mock:状态 bound 已绑定 / using 使用中(盘子被取出) / free 未绑定 / disabled 异常停用 */
var PLATE_SLOTS=[
{id:'P1-01',status:'bound',member:{name:'张*明',level:'黄金会员',levelCls:'',phone:'138****5621',code:'U20250311',dept:'生产部·一车间'},bindTime:'2026-06-01 09:30',lastUse:'2026-08-18 12:05 · 已归还',useCount:128},
{id:'P1-02',status:'using',member:{name:'李*华',level:'铂金会员',levelCls:'vip',phone:'139****3387',code:'U20240827',dept:'技术部·平台组'},bindTime:'2026-05-12 10:15',lastUse:'2026-08-18 12:20 · 使用中(已取出)',useCount:203},
{id:'P1-03',status:'bound',member:{name:'王*强',level:'普通会员',levelCls:'',phone:'137****8845',code:'U20260702',dept:'后勤部·餐饮科'},bindTime:'2026-07-20 14:00',lastUse:'2026-08-18 11:50 · 已归还',useCount:45},
{id:'P1-04',status:'bound',member:{name:'赵*敏',level:'黄金会员',levelCls:'',phone:'136****2210',code:'U20230915',dept:'财务部·核算组'},bindTime:'2026-04-08 09:00',lastUse:'2026-08-18 12:10 · 已归还',useCount:312},
{id:'P1-05',status:'using',member:{name:'陈*芳',level:'VIP会员',levelCls:'vip',phone:'135****7732',code:'U20250422',dept:'人事部·招聘组'},bindTime:'2026-06-25 11:20',lastUse:'2026-08-18 12:30 · 使用中(已取出)',useCount:97},
{id:'P1-06',status:'bound',member:{name:'刘*洋',level:'普通会员',levelCls:'',phone:'188****4406',code:'U20260801',dept:'生产部·二车间'},bindTime:'2026-08-01 08:40',lastUse:'2026-08-18 11:45 · 已归还',useCount:23},
{id:'P1-07',status:'disabled',member:null,bindTime:'—',lastUse:'2026-08-15 09:12 · 格口门锁故障',useCount:0},
{id:'P1-08',status:'free',member:null,bindTime:'—',lastUse:'—',useCount:0},
{id:'P1-09',status:'free',member:null,bindTime:'—',lastUse:'—',useCount:0},
{id:'P1-10',status:'free',member:null,bindTime:'—',lastUse:'—',useCount:0}
{id:'P1-01',status:'bound',plateCode:'CP-26001',member:{name:'张*明',level:'黄金会员',levelCls:'',phone:'138****5621',code:'U20250311',dept:'生产部·一车间'},bindTime:'2026-06-01 09:30',lastUse:'2026-08-18 12:05 · 已归还',useCount:128},
{id:'P1-02',status:'using',plateCode:'CP-26002',member:{name:'李*华',level:'铂金会员',levelCls:'vip',phone:'139****3387',code:'U20240827',dept:'技术部·平台组'},bindTime:'2026-05-12 10:15',lastUse:'2026-08-18 12:20 · 使用中(已取出)',useCount:203},
{id:'P1-03',status:'bound',plateCode:'CP-26003',member:{name:'王*强',level:'普通会员',levelCls:'',phone:'137****8845',code:'U20260702',dept:'后勤部·餐饮科'},bindTime:'2026-07-20 14:00',lastUse:'2026-08-18 11:50 · 已归还',useCount:45},
{id:'P1-04',status:'bound',plateCode:'CP-26004',member:{name:'赵*敏',level:'黄金会员',levelCls:'',phone:'136****2210',code:'U20230915',dept:'财务部·核算组'},bindTime:'2026-04-08 09:00',lastUse:'2026-08-18 12:10 · 已归还',useCount:312},
{id:'P1-05',status:'using',plateCode:'CP-26005',member:{name:'陈*芳',level:'VIP会员',levelCls:'vip',phone:'135****7732',code:'U20250422',dept:'人事部·招聘组'},bindTime:'2026-06-25 11:20',lastUse:'2026-08-18 12:30 · 使用中(已取出)',useCount:97},
{id:'P1-06',status:'bound',plateCode:'CP-26006',member:{name:'刘*洋',level:'普通会员',levelCls:'',phone:'188****4406',code:'U20260801',dept:'生产部·二车间'},bindTime:'2026-08-01 08:40',lastUse:'2026-08-18 11:45 · 已归还',useCount:23},
{id:'P1-07',status:'disabled',plateCode:'—',member:null,bindTime:'—',lastUse:'2026-08-15 09:12 · 格口门锁故障',useCount:0},
{id:'P1-08',status:'free',plateCode:'—',member:null,bindTime:'—',lastUse:'—',useCount:0},
{id:'P1-09',status:'free',plateCode:'—',member:null,bindTime:'—',lastUse:'—',useCount:0},
{id:'P1-10',status:'free',plateCode:'—',member:null,bindTime:'—',lastUse:'—',useCount:0}
];
/* 可分配会员 Mock(原型演示) */
@@ -809,10 +815,12 @@ function renderPlateCabinet(){
const memberHtml=s.member
?`<div class="ov-slot__name">${s.member.name}</div><div class="ov-slot__level ${s.member.levelCls}">${s.member.level}</div>`
:`<div class="ov-slot__name" style="color:var(--text-placeholder)">停用</div>`;
const plateHtml=s.member?`<div class="ov-slot__plate">餐盘 ${s.plateCode}</div>`:'';
return `<div class="ov-slot ${meta.cls}" onclick="showPlateSlot(${i})">
${meta.badge?`<div class="ov-slot__badge ${meta.badge}"></div>`:''}
<div class="ov-slot__id">${s.id}</div>
${memberHtml}
${plateHtml}
<div class="ov-slot__expire">${meta.text}</div>
</div>`;
}).join('');
@@ -841,6 +849,7 @@ function showPlateSlot(idx){
<div class="plate-detail__field"><span class="plate-detail__label">使用状态</span><span class="plate-detail__value">${statusTag}</span></div>
<div class="plate-detail__field"><span class="plate-detail__label">所属柜</span><span class="plate-detail__value">1号餐盘柜</span></div>
<div class="plate-detail__field"><span class="plate-detail__label">格口</span><span class="plate-detail__value">${s.id}</span></div>
<div class="plate-detail__field"><span class="plate-detail__label">餐盘编码</span><span class="plate-detail__value" style="font-family:monospace">${s.plateCode}</span></div>
<div class="plate-detail__field"><span class="plate-detail__label">绑定时间</span><span class="plate-detail__value">${s.bindTime}</span></div>
</div></div>`;
if(m){
@@ -863,7 +872,7 @@ function showPlateSlot(idx){
/* 操作按钮:未绑定可分配;已绑定/使用中可解绑;异常停用不可操作 */
let footer='';
if(s.status==='free'){
footer=`<button class="btn btn-primary" onclick="openPlateAssign(${idx})">格口分配</button>`;
footer=`<button class="btn btn-primary" onclick="showPlateAssignInSlot(${idx})">格口分配</button>`;
}else if(m){
footer=`<button class="btn btn-danger" onclick="openPlateUnbind(${idx})">用户解绑</button>`;
}else{
@@ -892,34 +901,40 @@ function refreshPlateTab(){
}
}
/* ===== 格口分配:会员选择层 ===== */
/* ===== 格口分配:在格口详情弹窗内原位切换为分配态 ===== */
var PLATE_ASSIGN_IDX=-1;
var PLATE_ASSIGN_SELECTED=null;
function openPlateAssign(idx){
function showPlateAssignInSlot(idx){
PLATE_ASSIGN_IDX=idx;
PLATE_ASSIGN_SELECTED=null;
const mask=document.createElement('div');
mask.className='member-picker-mask';
mask.id='plateAssignMask';
mask.innerHTML=`<div class="member-picker">
<div class="member-picker__header"><div class="member-picker__title">格口分配 — 选择会员(${PLATE_SLOTS[idx].id}</div><button class="member-picker__close" onclick="closePlateAssign()">&times;</button></div>
<div class="member-picker__search"><input type="text" placeholder="搜索会员姓名 / 手机号" oninput="filterPlateMembers(this.value)"></div>
<div class="member-picker__body" id="plateMemberList"></div>
<div class="member-picker__footer">
<button class="btn btn-default" onclick="closePlateAssign()">取消</button>
const mask=document.getElementById('plateSlotMask');
if(!mask)return;
const s=PLATE_SLOTS[idx];
mask.innerHTML=`<div class="slot-his-modal" style="width:640px;height:auto;max-height:78vh">
<div class="slot-his-modal__header"><div class="slot-his-modal__title">格口分配 — ${s.id}</div><button class="slot-his-modal__close" onclick="closePlateSlot()">&times;</button></div>
<div class="slot-his-modal__body">
<div class="plate-detail__section">
<div class="plate-detail__section-title">分配信息</div>
<div class="plate-detail__grid">
<div class="plate-detail__field" style="grid-column:1/-1">
<span class="plate-detail__label">餐盘编码</span>
<input type="text" id="plateAssignCode" placeholder="请输入或扫描餐盘编码" oninput="this.value=this.value.toUpperCase()">
</div>
</div>
</div>
<div class="plate-detail__section">
<div class="plate-detail__section-title">选择会员</div>
<div class="plate-assign__search"><input type="text" placeholder="搜索会员姓名 / 手机号" oninput="filterPlateMembers(this.value)"></div>
<div id="plateMemberList"></div>
</div>
</div>
<div class="slot-his-modal__footer" style="display:flex;justify-content:flex-end;gap:12px;">
<button class="btn btn-default" onclick="showPlateSlot(${idx})">取消</button>
<button class="btn btn-primary" onclick="confirmPlateAssign()">确认分配</button>
</div>
</div>`;
mask.addEventListener('click',e=>{if(e.target===mask)closePlateAssign();});
document.body.appendChild(mask);
renderPlateMemberList(PLATE_MEMBERS);
}
function closePlateAssign(){
const m=document.getElementById('plateAssignMask');
if(!m)return;
m.classList.add('closing');
setTimeout(()=>m.remove(),200);
}
function renderPlateMemberList(list){
const wrap=document.getElementById('plateMemberList');
if(!wrap)return;
@@ -954,16 +969,22 @@ function confirmPlateAssign(){
showOvToast('请先选择要分配的会员');
return;
}
const codeInput=document.getElementById('plateAssignCode');
const plateCode=(codeInput?codeInput.value:'').trim();
if(!plateCode){
showOvToast('请输入餐盘编码');
return;
}
const s=PLATE_SLOTS[PLATE_ASSIGN_IDX];
s.status='bound';
s.member=PLATE_ASSIGN_SELECTED;
s.plateCode=plateCode;
s.bindTime='2026-08-18 16:50';
s.lastUse='— · 分配后待首次使用';
s.useCount=0;
closePlateAssign();
closePlateSlot();
refreshPlateTab();
showOvToast('已将格口 '+s.id+' 分配给会员 '+s.member.name+'(原型演示)');
showOvToast('已将格口 '+s.id+' 分配给会员 '+s.member.name+',绑定餐盘 '+s.plateCode+'(原型演示)');
}
/* ===== 用户解绑:二次确认 ===== */
@@ -994,6 +1015,7 @@ function confirmPlateUnbind(idx){
const s=PLATE_SLOTS[idx];
s.status='free';
s.member=null;
s.plateCode='—';
s.bindTime='—';
s.lastUse='—';
s.useCount=0;
+15 -8
View File
@@ -1065,14 +1065,14 @@ document.querySelectorAll('.tab-panel')[2].querySelectorAll('tbody tr').forEach(
});
/* ========== 会员权益弹窗(Tab3 ========== */
/* 交互说明:点击操作列「会员权益」弹窗展示该会员的权益概要与专属餐盘柜绑定信息。
/* 交互说明:点击操作列「会员权益」弹窗展示该会员的权益概要与专属餐盘柜信息。
会员权益体系尚在规划中,当前已明确权益为「专属餐盘柜」,此处回显柜子/格口/绑定时间/最后使用状态/累计使用次数。 */
// 模拟数据:会员专属餐盘柜绑定信息(未配置的会员显示空态)
// 模拟数据:会员专属餐盘柜信息(未配置的会员显示空态)
var vipTrayLockerMap = {
'张伟': { locker: 'A区-01号柜', cell: '12号格口', bindTime: '2024-01-20 10:32', lastUseTime: '2026-04-28 11:32', lastUseStatus: '已归还', useCount: 1286 },
'李娜': { locker: 'A区-01号柜', cell: '08号格口', bindTime: '2024-05-15 09:18', lastUseTime: '2026-04-28 12:08', lastUseStatus: '使用中', useCount: 862 },
'赵敏': { locker: 'B区-03号柜', cell: '21号格口', bindTime: '2025-08-20 14:05', lastUseTime: '2026-04-28 11:30', lastUseStatus: '已归还', useCount: 316 },
'周明': { locker: 'B区-03号柜', cell: '05号格口', bindTime: '2024-02-18 16:47', lastUseTime: '2026-03-28 12:15', lastUseStatus: '已归还', useCount: 2048 }
'张伟': { locker: 'A区-01号柜', cell: '12号格口', dishCode: 'PT-10236', bindTime: '2024-01-20 10:32', lastUseTime: '2026-04-28 11:32', lastUseStatus: '已归还', useCount: 1286 },
'李娜': { locker: 'A区-01号柜', cell: '08号格口', dishCode: 'PT-10852', bindTime: '2024-05-15 09:18', lastUseTime: '2026-04-28 12:08', lastUseStatus: '使用中', useCount: 862 },
'赵敏': { locker: 'B区-03号柜', cell: '21号格口', dishCode: 'PT-20317', bindTime: '2025-08-20 14:05', lastUseTime: '2026-04-28 11:30', lastUseStatus: '已归还', useCount: 316 },
'周明': { locker: 'B区-03号柜', cell: '05号格口', dishCode: 'PT-20094', bindTime: '2024-02-18 16:47', lastUseTime: '2026-03-28 12:15', lastUseStatus: '已归还', useCount: 2048 }
};
// 等级权益文案映射(与等级配置 Tab 保持一致)
var vipLevelBenefitsMap = {
@@ -1082,21 +1082,28 @@ var vipLevelBenefitsMap = {
'普通': '基础点餐功能、专属餐盘柜'
};
function showVipBenefitsModal(name, tds) {
var levelText = tds[3].querySelector('.tag').textContent;
var cardNo = tds[0].textContent;
renderVipBenefitsModal(name, levelText, cardNo);
}
/* 渲染会员权益弹窗 */
function renderVipBenefitsModal(name, levelText, cardNo) {
var locker = vipTrayLockerMap[name];
// 专属餐盘柜信息区:已绑定展示明细,未绑定展示空态
// 专属餐盘柜信息区:已配置展示明细,未配置展示空态
var lockerHtml;
if (locker) {
var statusTag = locker.lastUseStatus === '使用中' ? 'tag-blue' : 'tag-green';
lockerHtml = ''
+ '<table class="meal-history-table">'
+ ' <thead><tr><th>所属柜子</th><th>格口</th><th>绑定时间</th><th>最后使用时间</th><th>最后使用状态</th><th>累计使用次数</th></tr></thead>'
+ ' <thead><tr><th>所属柜子</th><th>格口</th><th>餐盘编号</th><th>绑定时间</th><th>最后使用时间</th><th>最后使用状态</th><th>累计使用次数</th></tr></thead>'
+ ' <tbody><tr>'
+ ' <td>' + locker.locker + '</td>'
+ ' <td>' + locker.cell + '</td>'
+ ' <td>' + (locker.dishCode || '-') + '</td>'
+ ' <td>' + locker.bindTime + '</td>'
+ ' <td>' + locker.lastUseTime + '</td>'
+ ' <td><span class="tag ' + statusTag + '">' + locker.lastUseStatus + '</span></td>'
+25
View File
@@ -1,5 +1,30 @@
# 营养用餐管理端 CHANGELOG
## [2026-08-19] feat: 餐线管理重构 — 收费模式周设置矩阵 + 设备概况/详情弹窗(`din-canteen-line.html`)
- 餐线列表新增结算方式(自动/手动)·状态(胶囊开关,点击启用/停用切换)两字段;收费模式因每天餐次可能不同,不在列表展示
- 操作列「编辑」改名为「餐线设置」,并新增「详情」入口
- 设备数量列改为纯数字,点击弹出「设备列表概况」弹窗(设备类型/编号/设备名称/运行天数/在线状态)
- 重构新增餐线与餐线设置共用同一大弹窗:基础信息区(名称/结算方式/状态)+ 收费模式独立分区
- 收费模式按周设置:7天×4餐次矩阵(早餐/午餐/晚餐/加餐),每格可独立选择收费模式;按餐计费需填计费金额并带「元」单位,称重计费/免费时金额框显示「-」
- 收费模式支持「⚡ 批量设置」:一键将 7×4 全部餐次统一设为同一模式与金额,计费金额输入框仅在「按餐计费」时显示
- 新增「餐线详情」弹窗:回显基础信息 + 收费模式只读矩阵 + 关联设备列表(带分页,每页 5 条)
- 数据模型改造为 JS 动态渲染,新增/编辑/删除/状态切换/批量设置均联动刷新
## [2026-08-19] feat: 会员权益弹窗支持手动绑定/解绑专属餐盘柜(`din-user-mgmt.html`)
- 已绑定会员:专属餐盘柜信息表格下方新增「解绑绑定」按钮,点击弹出二次确认(提示格口释放后果),确认后清除绑定并回到空态
- 未绑定会员:空态区新增「手动绑定」按钮,弹出绑定弹窗(所属柜子下拉 + 空闲格口下拉,格口随柜子联动刷新)
- 绑定成功写入绑定数据(绑定时间取当前时间、状态默认「已归还」、使用次数 0)并回开会员权益弹窗回显,配套 toast 提示
- 绑定/解绑弹窗取消操作返回会员权益弹窗,不变更数据
- 专属餐盘柜信息表格新增「餐盘编号」列(格口内当前存放餐盘的编号,手动绑定的新格口显示 `-`)
## [2026-08-19] feat: 餐盘柜格口新增餐盘编码 + 格口分配原位改造(`din-terminal.html`)
- 格口数据模型新增 `plateCode`(餐盘编码)字段,格口卡片与格口详情弹窗展示「餐盘 CP-xxxx」,异常/空闲格口显示「—」
- 格口分配由独立会员选择层改为在格口详情弹窗内原位切换为分配态(复用同一弹窗),新增「餐盘编码」输入框(必填,输入自动转大写)
- 确认分配时校验餐盘编码并写入;用户解绑时同步清空餐盘编码为「—」
## [2026-08-18] feat: 订餐终端设备概览大弹窗 + 会员权益体系 + 采购库存抵扣 + 追溯多锅次分支
### 变更一:订餐终端设备概览大弹窗(`din-terminal.html`)