Files
TakeoutSaaS.Prototypes/Cend-MiniProgram-Prototype/index.html

1512 lines
109 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TakeoutSaaS C端小程序原型</title>
<style>
:root {
--bg: #f5f7fb;
--surface: #ffffff;
--surface-2: #f8fafc;
--text: #111827;
--muted: #6b7280;
--line: #e5e7eb;
--primary: #ff6b35;
--primary-soft: rgba(255, 107, 53, 0.12);
--success: #10b981;
--success-soft: rgba(16, 185, 129, 0.12);
--warning: #f59e0b;
--warning-soft: rgba(245, 158, 11, 0.12);
--danger: #ef4444;
--danger-soft: rgba(239, 68, 68, 0.12);
--shadow: 0 20px 60px rgba(15, 23, 42, 0.08);
--radius-xl: 32px;
--radius-lg: 24px;
--radius-md: 18px;
--radius-sm: 14px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(180deg, #fff7f3 0%, var(--bg) 18%, var(--bg) 100%);
color: var(--text);
}
button,
input,
textarea {
font: inherit;
}
button {
border: 0;
cursor: pointer;
}
.workspace {
min-height: 100vh;
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 24px;
padding: 24px;
}
.sidebar {
background: rgba(255, 255, 255, 0.86);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: 32px;
padding: 24px;
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
position: sticky;
top: 24px;
height: calc(100vh - 48px);
overflow: auto;
}
.brand {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.brand-title {
font-size: 26px;
font-weight: 800;
}
.brand-subtitle {
margin-top: 6px;
color: var(--muted);
font-size: 14px;
}
.chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 0 14px;
border-radius: 999px;
background: var(--primary-soft);
color: var(--primary);
font-size: 12px;
font-weight: 700;
}
.sidebar-card {
margin-top: 18px;
padding: 18px;
border-radius: 24px;
background: var(--surface);
border: 1px solid #f1f5f9;
box-shadow: 0 14px 40px rgba(15, 23, 42, 0.05);
}
.section-title {
font-size: 16px;
font-weight: 800;
margin-bottom: 10px;
}
.section-subtitle {
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.phase-list,
.route-list,
.summary-list,
.control-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.phase-item {
display: grid;
grid-template-columns: 36px 1fr;
gap: 10px;
padding: 12px 14px;
border-radius: 18px;
background: #fff7f3;
}
.phase-index {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #ff8a5b, #ff5a1f);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 800;
}
.phase-name {
font-weight: 700;
font-size: 14px;
}
.phase-desc {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.route-button,
.control-button {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-radius: 16px;
background: #f8fafc;
color: var(--text);
text-align: left;
transition: 0.2s ease;
}
.route-button:hover,
.control-button:hover {
background: #fff0e8;
}
.route-button.active {
background: linear-gradient(135deg, #fff1eb, #ffe4d5);
color: var(--primary);
box-shadow: inset 0 0 0 1px rgba(255, 107, 53, 0.14);
}
.route-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.route-title {
font-size: 14px;
font-weight: 700;
}
.route-path {
color: var(--muted);
font-size: 12px;
}
.stage {
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-radius: 24px;
background: rgba(255, 255, 255, 0.86);
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
}
.toolbar-title {
font-size: 18px;
font-weight: 800;
}
.toolbar-subtitle {
margin-top: 6px;
color: var(--muted);
font-size: 13px;
}
.toolbar-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.pill-button {
min-height: 40px;
padding: 0 16px;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--line);
color: var(--text);
font-weight: 700;
}
.pill-button.active {
background: var(--primary-soft);
color: var(--primary);
border-color: transparent;
}
.phone-shell {
width: 420px;
max-width: 100%;
margin: 0 auto;
padding: 18px;
border-radius: 42px;
background: linear-gradient(180deg, #1f2937, #0f172a);
box-shadow: 0 40px 100px rgba(15, 23, 42, 0.24);
}
.phone-notch {
width: 140px;
height: 28px;
margin: 0 auto 14px;
border-radius: 0 0 20px 20px;
background: #0b1220;
}
.phone-screen {
min-height: 860px;
border-radius: 32px;
background: var(--bg);
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.page {
position: relative;
min-height: 860px;
padding: 22px 18px 96px;
}
.page.with-safe-bottom {
padding-bottom: 140px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.topbar-title {
font-size: 22px;
font-weight: 800;
}
.topbar-subtitle {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
}
.icon-circle,
.back-button {
width: 42px;
height: 42px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--surface);
border: 1px solid #eef2f7;
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.06);
font-weight: 800;
color: var(--text);
}
.card {
margin-top: 14px;
padding: 18px;
border-radius: 24px;
background: var(--surface);
box-shadow: 0 14px 40px rgba(15, 23, 42, 0.05);
}
.card:first-child {
margin-top: 0;
}
.store-strip {
display: grid;
gap: 10px;
cursor: pointer;
}
.store-title {
font-size: 18px;
font-weight: 800;
}
.store-meta,
.muted {
color: var(--muted);
font-size: 13px;
}
.scene-switch,
.filter-row,
.segmented {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 14px;
}
.scene-item,
.filter-chip {
padding: 12px 10px;
border-radius: 18px;
background: #f3f4f6;
border: 1px solid transparent;
text-align: center;
cursor: pointer;
font-size: 13px;
color: var(--muted);
}
.scene-item strong,
.filter-chip strong {
display: block;
color: var(--text);
font-size: 14px;
margin-bottom: 4px;
}
.scene-item.active,
.filter-chip.active {
background: linear-gradient(135deg, #fff1eb, #ffe4d5);
border-color: rgba(255, 107, 53, 0.12);
color: var(--primary);
}
.banner-list,
.quick-grid,
.asset-grid,
.topic-grid,
.coupon-grid,
.record-list,
.plan-grid {
display: grid;
gap: 12px;
}
.quick-grid,
.asset-grid,
.topic-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.banner-card {
padding: 18px;
border-radius: 22px;
color: #fff;
min-height: 116px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
}
.banner-title {
font-size: 20px;
font-weight: 800;
}
.banner-subtitle {
font-size: 13px;
opacity: 0.92;
}
.quick-card,
.asset-item,
.topic-card {
padding: 14px;
border-radius: 18px;
background: #fff7f3;
cursor: pointer;
}
.quick-title,
.asset-title,
.topic-title {
font-size: 14px;
font-weight: 800;
}
.quick-subtitle,
.asset-subtitle,
.topic-subtitle {
margin-top: 6px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.section-header h3 {
margin: 0;
font-size: 16px;
font-weight: 800;
}
.section-header span {
color: var(--muted);
font-size: 12px;
}
.product-list,
.order-list,
.address-list,
.message-list,
.timeline,
.faq-list,
.fee-list,
.record-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.product-card,
.order-card,
.address-card,
.message-card,
.faq-card,
.summary-card,
.record-card,
.activity-card,
.benefit-card,
.pass-card,
.plan-card {
padding: 14px;
border-radius: 20px;
background: #fff;
border: 1px solid #f1f5f9;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
}
.product-card {
display: grid;
grid-template-columns: 94px minmax(0, 1fr);
gap: 12px;
cursor: pointer;
}
.product-image {
height: 94px;
border-radius: 18px;
position: relative;
overflow: hidden;
}
.product-badge {
position: absolute;
top: 10px;
left: 10px;
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 800;
background: rgba(17, 24, 39, 0.18);
color: #fff;
}
.product-name {
font-size: 16px;
font-weight: 800;
margin-bottom: 6px;
}
.product-desc,
.tiny {
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.tag-row,
.row,
.space-between,
.toolbar-grid {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.space-between {
justify-content: space-between;
}
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 26px;
padding: 0 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #f3f4f6;
color: var(--muted);
}
.tag.primary { background: var(--primary-soft); color: var(--primary); }
.tag.success { background: var(--success-soft); color: var(--success); }
.tag.warning { background: var(--warning-soft); color: #b45309; }
.tag.danger { background: var(--danger-soft); color: var(--danger); }
.price-row {
display: flex;
align-items: baseline;
gap: 8px;
margin-top: 10px;
}
.price {
color: var(--primary);
font-size: 18px;
font-weight: 900;
}
.origin-price {
color: #9ca3af;
font-size: 12px;
text-decoration: line-through;
}
.btn,
.mini-btn {
min-height: 42px;
padding: 0 16px;
border-radius: 999px;
font-weight: 700;
transition: 0.2s ease;
}
.mini-btn {
min-height: 34px;
padding: 0 12px;
font-size: 12px;
}
.btn-primary,
.mini-btn.primary {
background: linear-gradient(135deg, #ff7a45, #ff5a1f);
color: #fff;
}
.btn-secondary,
.mini-btn.secondary {
background: var(--primary-soft);
color: var(--primary);
}
.btn-ghost,
.mini-btn.ghost {
background: #fff;
border: 1px solid var(--line);
color: var(--text);
}
.btn-danger,
.mini-btn.danger {
background: var(--danger-soft);
color: var(--danger);
}
.btn[disabled],
.mini-btn[disabled] {
background: #e5e7eb;
color: #9ca3af;
cursor: not-allowed;
}
.bottom-tabs,
.cart-bar,
.fixed-actions {
position: absolute;
left: 14px;
right: 14px;
bottom: 14px;
background: rgba(17, 24, 39, 0.96);
color: #fff;
border-radius: 22px;
padding: 12px 14px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.2);
}
.bottom-tabs {
background: rgba(255, 255, 255, 0.96);
color: var(--text);
border: 1px solid rgba(229, 231, 235, 0.8);
gap: 8px;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--muted);
cursor: pointer;
padding: 8px 0;
border-radius: 16px;
}
.tab-item.active {
background: #fff1eb;
color: var(--primary);
font-weight: 700;
}
</style>
</head>
<body>
<div class="workspace">
<aside class="sidebar">
<div class="brand">
<div>
<div class="brand-title">TakeoutSaaS</div>
<div class="brand-subtitle">C 端小程序可点击原型 · 按 `plan.md` 顺序实现</div>
</div>
<div class="chip">V1 Demo</div>
</div>
<div class="sidebar-card">
<div class="section-title">阶段执行总览</div>
<div class="phase-list" id="phaseList"></div>
</div>
<div class="sidebar-card">
<div class="section-title">页面导航</div>
<div class="section-subtitle">路由命名与文档保持一致,支持直接切页演示。</div>
<div class="route-list" id="routeList"></div>
</div>
<div class="sidebar-card">
<div class="section-title">全局控制</div>
<div class="control-list" id="controlList"></div>
</div>
<div class="sidebar-card">
<div class="section-title">当前状态</div>
<div class="summary-list" id="stateSummary"></div>
</div>
</aside>
<main class="stage">
<div class="toolbar">
<div>
<div class="toolbar-title" id="toolbarTitle"></div>
<div class="toolbar-subtitle" id="toolbarSubtitle"></div>
</div>
<div class="toolbar-actions" id="toolbarActions"></div>
</div>
<div class="phone-shell">
<div class="phone-notch"></div>
<div class="phone-screen" id="phoneScreen"></div>
</div>
</main>
</div>
<script>
const SCENES = [
{ key: 'delivery', label: '外卖', tip: '30 分钟送达' },
{ key: 'pickup', label: '自提', tip: '到店自取更省' },
{ key: 'dine_in', label: '堂食', tip: '扫码点餐更快' }
];
const STORES = {
store_guomao: {
id: 'store_guomao',
name: 'Haz Coffee 国贸店',
shortName: '国贸店',
distance: '1.2km',
status: '营业中',
hours: '07:30-22:00',
address: '朝阳区建国门外大街 88 号 SOHO 2 座 1 层',
phone: '010-8888-6666',
minOrder: 38,
deliveryFee: 5,
freeDelivery: 68,
supports: ['delivery', 'pickup', 'dine_in'],
pickupRule: '支持 30 分钟内预约取餐',
dineRule: '支持堂食加菜和合单提醒'
},
store_sanlitun: {
id: 'store_sanlitun',
name: 'Haz Coffee 三里屯店',
shortName: '三里屯店',
distance: '3.4km',
status: '营业中',
hours: '08:00-23:00',
address: '朝阳区工体北路 81 号三里屯太古里南区',
phone: '010-8888-6677',
minOrder: 42,
deliveryFee: 6,
freeDelivery: 78,
supports: ['delivery', 'pickup', 'dine_in'],
pickupRule: '夜间自提立减 ¥2',
dineRule: '支持桌边加菜与呼叫服务'
},
store_wangjing: {
id: 'store_wangjing',
name: 'Haz Coffee 望京店',
shortName: '望京店',
distance: '6.1km',
status: '休息中',
hours: '09:00-21:00',
address: '朝阳区望京街 10 号望京 SOHO 塔 1 B1',
phone: '010-8888-6688',
minOrder: 45,
deliveryFee: 7,
freeDelivery: 88,
supports: ['delivery', 'pickup'],
pickupRule: '工作日早餐 8 折',
dineRule: '当前门店暂不支持堂食'
}
};
const PRODUCTS = [
{ id: 'latte_oat', category: 'coffee', name: '燕麦拿铁', desc: '双份浓缩配燕麦奶,口感更柔和顺滑', image: '#ffb48a', badge: 'TOP1', tags: ['招牌', '热销'], price: 21, origin: 26, variants: [{ id: 'm', label: '中杯 / 热饮', price: 21 }, { id: 'l', label: '大杯 / 少冰', price: 25 }], addons: [{ id: 'shot', label: '加浓', price: 3 }, { id: 'foam', label: '奶盖', price: 4 }], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: false },
{ id: 'orange_americano', category: 'coffee', name: '橙香美式', desc: '鲜橙与美式融合,清爽提神', image: '#ff9460', badge: '新品', tags: ['清爽', '当季'], price: 18, origin: 24, variants: [{ id: 'm', label: '中杯 / 冰', price: 18 }, { id: 'l', label: '大杯 / 冰', price: 22 }], addons: [{ id: 'syrup', label: '橙香糖浆', price: 2 }], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: false },
{ id: 'dirty_coconut', category: 'coffee', name: '生椰 Dirty', desc: '生椰乳搭配浓缩,层次更分明', image: '#ffd39a', badge: '爆款', tags: ['招牌', '会员日'], price: 24, origin: 29, variants: [{ id: 'std', label: '大杯 / 冰', price: 24 }], addons: [{ id: 'coconut', label: '加生椰乳', price: 4 }, { id: 'extra', label: '加浓', price: 3 }], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: false },
{ id: 'grape_jasmine', category: 'tea', name: '葡萄茉莉轻饮', desc: '葡萄果肉搭配茉莉茶底,轻甜微香', image: '#b38bff', badge: '推荐', tags: ['低负担'], price: 19, origin: 25, variants: [{ id: 'm', label: '中杯 / 少冰', price: 19 }, { id: 'l', label: '大杯 / 少冰', price: 23 }], addons: [{ id: 'nata', label: '椰果', price: 2 }, { id: 'grape', label: '加葡萄果肉', price: 3 }], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: false },
{ id: 'berry_yogurt', category: 'tea', name: '莓果酸奶杯', desc: '草莓蓝莓双拼,口感清新绵密', image: '#ff7ca8', badge: '限定', tags: ['轻食搭配'], price: 22, origin: 28, variants: [{ id: 'std', label: '标准杯', price: 22 }], addons: [{ id: 'granola', label: '加麦片', price: 3 }], scenes: ['delivery', 'pickup'], soldOut: false },
{ id: 'chicken_wrap', category: 'lightmeal', name: '香煎鸡肉卷', desc: '低脂鸡胸搭配羽衣甘蓝,轻盈饱腹', image: '#ffc87a', badge: '午餐热卖', tags: ['轻食', '午餐'], price: 26, origin: 32, variants: [{ id: 'std', label: '标准份', price: 26 }], addons: [{ id: 'egg', label: '加溏心蛋', price: 4 }, { id: 'avocado', label: '加牛油果', price: 5 }], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: false },
{ id: 'mushroom_panini', category: 'lightmeal', name: '蘑菇帕尼尼', desc: '双重芝士加蘑菇,热压口感酥香', image: '#d8a16f', badge: '现烤', tags: ['堂食推荐'], price: 27, origin: 34, variants: [{ id: 'std', label: '标准份', price: 27 }], addons: [{ id: 'cheese', label: '加芝士', price: 4 }], scenes: ['pickup', 'dine_in'], soldOut: false },
{ id: 'tiramisu_box', category: 'dessert', name: '提拉米苏盒子', desc: '马斯卡彭与咖啡酒香层层叠加', image: '#b38c73', badge: '人气', tags: ['甜品', '下午茶'], price: 22, origin: 29, variants: [{ id: 'std', label: '单份', price: 22 }], addons: [{ id: 'cocoa', label: '加可可粉', price: 1 }], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: false },
{ id: 'durian_crepe', category: 'dessert', name: '榴莲千层', desc: '限量供应,卖完即止', image: '#f6d365', badge: '售罄', tags: ['限量'], price: 29, origin: 36, variants: [{ id: 'std', label: '单份', price: 29 }], addons: [], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: true },
{ id: 'breakfast_combo', category: 'combo', name: '早餐能量套餐', desc: '燕麦拿铁 + 鸡肉卷组合更划算', image: '#ffaf87', badge: '超值', tags: ['套餐', '省 ¥6'], price: 47, origin: 53, variants: [{ id: 'std', label: '套餐价', price: 47 }], addons: [{ id: 'fruit', label: '加水果杯', price: 6 }], scenes: ['delivery', 'pickup'], soldOut: false },
{ id: 'brunch_combo', category: 'combo', name: '午后双人轻享', desc: '两杯饮品搭配甜品,双人分享刚刚好', image: '#fca5a5', badge: '限时折扣', tags: ['双人', '79 折'], price: 62, origin: 78, variants: [{ id: 'std', label: '套餐价', price: 62 }], addons: [], scenes: ['pickup', 'dine_in'], soldOut: false },
{ id: 'rose_coldbrew', category: 'seasonal', name: '玫瑰冷萃', desc: '春季限定,花香与冷萃咖啡平衡柔和', image: '#f9a8d4', badge: '限时', tags: ['春季限定'], price: 25, origin: 31, variants: [{ id: 'std', label: '中杯 / 冰', price: 25 }], addons: [{ id: 'rose', label: '加玫瑰冻', price: 3 }], scenes: ['delivery', 'pickup', 'dine_in'], soldOut: false }
];
const CATEGORIES = [
{ id: 'coffee', name: '招牌咖啡' },
{ id: 'tea', name: '果茶轻饮' },
{ id: 'lightmeal', name: '轻食主食' },
{ id: 'dessert', name: '甜品烘焙' },
{ id: 'combo', name: '套餐超值' },
{ id: 'seasonal', name: '限定新品' }
];
const HOME_BANNERS = [
{ id: 'b1', title: '新客立领 ¥18 券包', subtitle: '首单优惠 + 免配送费券', color: 'linear-gradient(135deg, #ff8a5b, #ff5a1f)', route: 'pages/coupon/center/index' },
{ id: 'b2', title: '17:00 秒杀场进行中', subtitle: '燕麦拿铁低至 ¥9.9', color: 'linear-gradient(135deg, #fb7185, #ef4444)', route: 'pages/activity/seckill/index' },
{ id: 'b3', title: '会员日福利上线', subtitle: '成长值翻倍,积分兑饮品', color: 'linear-gradient(135deg, #f59e0b, #f97316)', route: 'pages/member/center/index' }
];
const HOME_QUICK = [
{ title: '新客有礼', subtitle: '首单券包', route: 'pages/coupon/center/index' },
{ title: '领券中心', subtitle: '满减折扣', route: 'pages/coupon/center/index' },
{ title: '满减活动', subtitle: '凑单立减', route: 'pages/tab/menu/index' },
{ title: '秒杀专区', subtitle: '限量抢购', route: 'pages/activity/seckill/index' },
{ title: '限时折扣', subtitle: '专场直降', route: 'pages/activity/flash-sale/index' },
{ title: '储值充值', subtitle: '充 100 送 15', route: 'pages/prepaid/index' },
{ title: '次卡专区', subtitle: '爆款次卡', route: 'pages/pass-card/index' },
{ title: '会员中心', subtitle: '等级权益', route: 'pages/member/center/index' }
];
const ROUTES = [
{ code: 'T01', title: '首页', path: 'pages/tab/home/index', phase: '阶段 2 · 交易主链路', type: 'tab' },
{ code: 'T02', title: '点餐页', path: 'pages/tab/menu/index', phase: '阶段 2 · 交易主链路', type: 'tab' },
{ code: 'T03', title: '订单页', path: 'pages/tab/orders/index', phase: '阶段 3 · 履约售后', type: 'tab' },
{ code: 'T04', title: '我的页', path: 'pages/tab/me/index', phase: '阶段 4 · 会员资产', type: 'tab' },
{ code: 'P01', title: '门店选择页', path: 'pages/store/select/index', phase: '阶段 5 · 辅助页', type: 'page' },
{ code: 'P02', title: '地址管理页', path: 'pages/address/list/index', phase: '阶段 5 · 辅助页', type: 'page' },
{ code: 'P03', title: '结算确认页', path: 'pages/checkout/index', phase: '阶段 2 · 交易主链路', type: 'page' },
{ code: 'P04', title: '支付成功页', path: 'pages/payment/success/index', phase: '阶段 2 · 交易主链路', type: 'page' },
{ code: 'P05', title: '订单详情页', path: 'pages/order/detail/index', phase: '阶段 2 · 交易主链路', type: 'page' },
{ code: 'P06', title: '退款申请页', path: 'pages/order/refund/apply/index', phase: '阶段 3 · 履约售后', type: 'page' },
{ code: 'P07', title: '退款详情页', path: 'pages/order/refund/detail/index', phase: '阶段 3 · 履约售后', type: 'page' },
{ code: 'P08', title: '评价页', path: 'pages/review/create/index', phase: '阶段 3 · 履约售后', type: 'page' },
{ code: 'P09', title: '领券中心页', path: 'pages/coupon/center/index', phase: '阶段 4 · 会员资产', type: 'page' },
{ code: 'P10', title: '秒杀活动页', path: 'pages/activity/seckill/index', phase: '阶段 5 · 活动页', type: 'page' },
{ code: 'P11', title: '限时折扣活动页', path: 'pages/activity/flash-sale/index', phase: '阶段 5 · 活动页', type: 'page' },
{ code: 'P12', title: '会员中心页', path: 'pages/member/center/index', phase: '阶段 4 · 会员资产', type: 'page' },
{ code: 'P13', title: '积分商城页', path: 'pages/points/mall/index', phase: '阶段 4 · 会员资产', type: 'page' },
{ code: 'P14', title: '储值充值页', path: 'pages/prepaid/index', phase: '阶段 4 · 会员资产', type: 'page' },
{ code: 'P15', title: '次卡页', path: 'pages/pass-card/index', phase: '阶段 4 · 会员资产', type: 'page' },
{ code: 'P16', title: '消息中心页', path: 'pages/message/center/index', phase: '阶段 5 · 辅助页', type: 'page' },
{ code: 'P17', title: '帮助中心页', path: 'pages/help/center/index', phase: '阶段 5 · 辅助页', type: 'page' },
{ code: 'P18', title: '堂食扫码确认页', path: 'pages/dine-in/confirm/index', phase: '阶段 5 · 场景辅助页', type: 'page' }
];
const PHASES = [
{ title: '阶段 1原型基础骨架', desc: '壳层、导航、TabBar、抽屉容器、通用卡片与状态标签' },
{ title: '阶段 2交易主链路', desc: '首页 → 点餐 → 抽屉 → 结算 → 支付成功 → 订单详情' },
{ title: '阶段 3履约与售后', desc: '订单状态、退款申请、退款详情、评价闭环' },
{ title: '阶段 4我的与资产', desc: '会员、积分、储值、次卡、领券入口与承接' },
{ title: '阶段 5活动与辅助页', desc: '秒杀、限时折扣、门店、地址、消息、帮助、堂食确认' },
{ title: '阶段 6统一联调', desc: '主 CTA 全通、三场景可演示、命名与状态统一' }
];
const ORDERS = [
{
id: 'order_1001', number: 'TO202603060001', storeId: 'store_guomao', scene: 'delivery', status: 'pending_pay',
createdAt: '2026-03-06 14:27', paidAt: '', payMethod: '微信支付', remark: '咖啡去冰,放前台',
items: [
{ key: 'o1_lat', productId: 'latte_oat', name: '燕麦拿铁', variantLabel: '中杯 / 热饮', addons: [], unitPrice: 21, qty: 1 },
{ key: 'o1_wrap', productId: 'chicken_wrap', name: '香煎鸡肉卷', variantLabel: '标准份', addons: [], unitPrice: 26, qty: 1 }
],
amount: { subtotal: 47, packing: 2, utensil: 1, delivery: 5, discount: 0, payable: 55 },
address: '朝阳区建国门外大街 88 号 SOHO 2 座 12 层前台',
timeline: ['提交订单 14:27', '待支付 14:27', '支付成功', '商家接单', '配送中']
},
{
id: 'order_1002', number: 'TO202603050318', storeId: 'store_guomao', scene: 'delivery', status: 'preparing',
createdAt: '2026-03-05 12:18', paidAt: '2026-03-05 12:19', payMethod: '微信支付', remark: '少糖',
items: [
{ key: 'o2_dirty', productId: 'dirty_coconut', name: '生椰 Dirty', variantLabel: '大杯 / 冰', addons: ['加浓'], unitPrice: 27, qty: 1 },
{ key: 'o2_tira', productId: 'tiramisu_box', name: '提拉米苏盒子', variantLabel: '单份', addons: [], unitPrice: 22, qty: 1 }
],
amount: { subtotal: 49, packing: 2, utensil: 0, delivery: 5, discount: 8, payable: 48 },
address: '朝阳区建国门外大街 88 号 SOHO 2 座 12 层前台',
timeline: ['支付成功 12:19', '商家接单 12:21', '制作中 12:28', '配送中', '已完成']
},
{
id: 'order_1003', number: 'TO202603041021', storeId: 'store_sanlitun', scene: 'pickup', status: 'finished',
createdAt: '2026-03-04 08:18', paidAt: '2026-03-04 08:19', payMethod: '储值余额 + 微信支付', remark: '尽量热一点', pickupCode: 'A603', pickupSlot: '08:30-08:40',
items: [{ key: 'o3_combo', productId: 'breakfast_combo', name: '早餐能量套餐', variantLabel: '套餐价', addons: [], unitPrice: 47, qty: 1 }],
amount: { subtotal: 47, packing: 1, utensil: 0, delivery: 0, discount: 5, payable: 43 },
timeline: ['支付成功 08:19', '商家接单 08:20', '待自提 08:31', '已取餐 08:36', '订单完成']
},
{
id: 'order_1004', number: 'TO202603031889', storeId: 'store_guomao', scene: 'dine_in', status: 'refunding',
createdAt: '2026-03-03 18:05', paidAt: '2026-03-03 18:06', payMethod: '微信支付', remark: '少辣', tableNo: 'A12',
items: [
{ key: 'o4_panini', productId: 'mushroom_panini', name: '蘑菇帕尼尼', variantLabel: '标准份', addons: ['加芝士'], unitPrice: 31, qty: 1 },
{ key: 'o4_grape', productId: 'grape_jasmine', name: '葡萄茉莉轻饮', variantLabel: '中杯 / 少冰', addons: [], unitPrice: 19, qty: 1 }
],
amount: { subtotal: 50, packing: 0, utensil: 0, delivery: 0, discount: 6, payable: 44 },
refund: { status: 'processing', amount: 18, reason: '商品与描述不符', remark: '热压三明治实际未加蘑菇', rejectReason: '', timeline: ['提交退款申请 18:35', '商家处理中 18:42', '退款完成'] },
timeline: ['支付成功 18:06', '商家接单 18:08', '堂食进行中 18:20', '提交退款 18:35', '退款处理中 18:42']
}
];
const COUPONS = [
{ id: 'coupon_new_guest', title: '新客满减券', type: 'cash', value: '¥12', threshold: '满 ¥58 可用', validUntil: '2026-03-31', scope: '国贸店 / 外卖自提通用', claimed: true },
{ id: 'coupon_shipping', title: '免配送费券', type: 'shipping', value: '免配送费', threshold: '满 ¥38 可用', validUntil: '2026-03-20', scope: '全门店 / 仅外卖可用', claimed: false },
{ id: 'coupon_discount', title: '会员 88 折券', type: 'discount', value: '8.8 折', threshold: '无门槛', validUntil: '2026-03-15', scope: '全门店 / 自提堂食可用', claimed: true },
{ id: 'coupon_expired', title: '晚高峰满减券', type: 'cash', value: '¥10', threshold: '满 ¥49 可用', validUntil: '2026-03-01', scope: '国贸店 / 外卖通用', claimed: true, expired: true }
];
const ADDRESSES = [
{ id: 'addr_company', name: '李晓', phone: '13800138000', tag: '公司', detail: '朝阳区建国门外大街 88 号 SOHO 2 座 12 层前台', default: true, outOfRange: false, tip: '配送预计 30 分钟' },
{ id: 'addr_home', name: '李晓', phone: '13800138000', tag: '家', detail: '朝阳区百子湾路 32 号苹果社区北区 3 号楼 802', default: false, outOfRange: false, tip: '配送预计 42 分钟' },
{ id: 'addr_out', name: '张敏', phone: '13911112222', tag: '超区', detail: '顺义区后沙峪安泰大街 10 号', default: false, outOfRange: true, tip: '当前门店超出配送范围' }
];
const MESSAGES = [
{ id: 'msg1', type: 'order', title: '订单已接单', summary: 'Haz Coffee 国贸店已接单,正在制作中', time: '5 分钟前', unread: true, target: { route: 'pages/order/detail/index', params: { orderId: 'order_1002' } } },
{ id: 'msg2', type: 'marketing', title: '17:00 秒杀已开始', summary: '燕麦拿铁低至 ¥9.9,限量抢购中', time: '22 分钟前', unread: true, target: { route: 'pages/activity/seckill/index' } },
{ id: 'msg3', type: 'system', title: '自提规则更新', summary: '到店自提支持 30 分钟内预约取餐', time: '昨天 18:20', unread: false, target: { route: 'pages/help/center/index' } },
{ id: 'msg4', type: 'order', title: '退款申请处理中', summary: '订单 TO202603031889 退款申请正在处理', time: '昨天 18:45', unread: false, target: { route: 'pages/order/refund/detail/index', params: { orderId: 'order_1004' } } }
];
const POINTS_GOODS = [
{ id: 'p1', title: '品牌随行杯', points: 999, stock: '剩余 24 件', tag: '热兑' },
{ id: 'p2', title: '¥10 饮品兑换券', points: 600, stock: '今日可兑 50 份', tag: '高频' },
{ id: 'p3', title: '甜品兑换券', points: 480, stock: '库存充足', tag: '新品' }
];
const PREPAID_PLANS = [
{ id: 'pl1', amount: 100, gift: 15, arrival: 115, tag: '推荐' },
{ id: 'pl2', amount: 200, gift: 35, arrival: 235, tag: '热门' },
{ id: 'pl3', amount: 500, gift: 100, arrival: 600, tag: '超值' }
];
const PASS_DATA = {
available: [
{ id: 'pc1', title: '拿铁 5 次卡', scope: '燕麦拿铁 / 生椰 Dirty 适用', valid: '购买后 30 天', remain: 5, total: 5, cta: '立即购买' },
{ id: 'pc2', title: '午餐轻食 3 次卡', scope: '鸡肉卷 / 帕尼尼适用', valid: '购买后 15 天', remain: 3, total: 3, cta: '立即购买' }
],
mine: [
{ id: 'pc3', title: '拿铁 5 次卡', scope: '燕麦拿铁 / 生椰 Dirty 适用', valid: '2026-03-20', remain: 2, total: 5, cta: '查看详情' }
],
records: [
{ id: 'pr1', title: '使用 拿铁次卡', desc: '已核销 1 次', time: '2026-03-02 08:18' },
{ id: 'pr2', title: '购买 午餐轻食 3 次卡', desc: '支付成功', time: '2026-02-28 13:26' }
]
};
const state = {
route: 'pages/tab/home/index',
params: {},
history: [],
currentScene: 'delivery',
currentStoreId: 'store_guomao',
activeCategory: 'coffee',
menuSearch: '',
orderFilter: 'all',
orderScene: 'all',
couponFilter: 'all',
messageType: 'all',
pointsTab: 'goods',
passTab: 'available',
seckillTab: 'now',
flashTab: 'current',
memberLoggedIn: true,
debugState: 'default',
showProductDrawer: false,
showCartDrawer: false,
activeProductId: null,
productVariant: null,
productAddons: [],
productQty: 1,
selectedOrderId: 'order_1002',
lastOrderId: 'order_1004',
reviewDraft: { rating: 5, tags: ['味道不错', '包装精致'], comment: '', anonymous: false },
refundDraft: { reason: '配送超时', remark: '' },
openFaqs: ['faq1'],
checkout: {
addressId: 'addr_company',
couponId: 'coupon_new_guest',
usePoints: true,
useBalance: false,
usePassCard: false,
remark: '少冰少糖,送达前请电话联系',
utensils: '无需餐具',
pickupSlot: '今日 18:20-18:30',
pickupName: '李晓',
pickupPhone: '13800138000'
},
cart: [
{ key: 'cart1', productId: 'latte_oat', name: '燕麦拿铁', variantLabel: '大杯 / 少冰', addons: ['加浓'], unitPrice: 28, qty: 1 },
{ key: 'cart2', productId: 'chicken_wrap', name: '香煎鸡肉卷', variantLabel: '标准份', addons: ['加牛油果'], unitPrice: 31, qty: 1 }
],
orders: JSON.parse(JSON.stringify(ORDERS)),
coupons: JSON.parse(JSON.stringify(COUPONS)),
addresses: JSON.parse(JSON.stringify(ADDRESSES)),
messages: JSON.parse(JSON.stringify(MESSAGES))
};
const routeMap = Object.fromEntries(ROUTES.map((item) => [item.path, item]));
function currentStore() { return STORES[state.currentStoreId] || STORES.store_guomao; }
function currentSceneInfo() { return SCENES.find((item) => item.key === state.currentScene) || SCENES[0]; }
function productById(id) { return PRODUCTS.find((item) => item.id === id); }
function routeMeta(path = state.route) { return routeMap[path] || ROUTES[0]; }
function isTabRoute(path = state.route) { return (routeMap[path] || {}).type === 'tab'; }
function currency(value) { return `¥${Number(value).toFixed(2)}`; }
function statusMeta(status) {
return {
pending_pay: { text: '待支付', type: 'warning', desc: '订单已提交,请尽快完成支付' },
paid_wait_accept: { text: '待接单', type: 'primary', desc: '支付成功,等待商家接单' },
preparing: { text: '制作中', type: 'primary', desc: '商家正在制作,请稍候' },
delivering: { text: '配送中', type: 'success', desc: '骑手正在配送,注意接听电话' },
wait_pickup: { text: '待自提', type: 'primary', desc: '请按时到店出示取餐码' },
dine_in_serving: { text: '堂食进行中', type: 'primary', desc: '堂食订单进行中,可继续加菜' },
finished: { text: '已完成', type: 'success', desc: '订单已完成,欢迎再次下单' },
refunding: { text: '退款中', type: 'warning', desc: '退款申请处理中' },
refunded: { text: '已退款', type: 'secondary', desc: '退款已完成' },
closed: { text: '已关闭', type: 'secondary', desc: '订单已关闭' }
}[status] || { text: '未知状态', type: 'secondary', desc: '' };
}
function sceneLabel(key) { return (SCENES.find((item) => item.key === key) || {}).label || ''; }
function clone(data) { return JSON.parse(JSON.stringify(data)); }
function makeLineKey() { return `line_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`; }
function filteredProducts() {
return PRODUCTS.filter((item) => item.category === state.activeCategory)
.filter((item) => !state.menuSearch || `${item.name}${item.desc}${item.tags.join('')}`.includes(state.menuSearch));
}
function cartSummary() {
const subtotal = state.cart.reduce((sum, item) => sum + item.unitPrice * item.qty, 0);
const packing = state.currentScene === 'dine_in' ? 0 : state.cart.reduce((sum, item) => sum + item.qty, 0);
const utensil = state.currentScene === 'delivery' && state.cart.length ? 1 : 0;
const delivery = state.currentScene === 'delivery' && state.cart.length ? (subtotal >= currentStore().freeDelivery ? 0 : currentStore().deliveryFee) : 0;
const total = subtotal + packing + utensil + delivery;
return {
subtotal,
packing,
utensil,
delivery,
total,
count: state.cart.reduce((sum, item) => sum + item.qty, 0),
gapToMinOrder: state.currentScene === 'delivery' && subtotal < currentStore().minOrder ? currentStore().minOrder - subtotal : 0,
gapToFreeDelivery: state.currentScene === 'delivery' && subtotal < currentStore().freeDelivery ? currentStore().freeDelivery - subtotal : 0
};
}
function checkoutData() {
const summary = cartSummary();
const address = state.addresses.find((item) => item.id === state.checkout.addressId);
const coupon = state.coupons.find((item) => item.id === state.checkout.couponId);
const couponDiscount = coupon && coupon.claimed && !coupon.expired ? (coupon.type === 'cash' ? 12 : coupon.type === 'shipping' ? summary.delivery : state.currentScene !== 'delivery' ? Math.round(summary.subtotal * 0.12) : 0) : 0;
const pointsDiscount = state.checkout.usePoints ? Math.min(5, summary.total - couponDiscount) : 0;
const balanceDiscount = state.checkout.useBalance ? Math.min(12, summary.total - couponDiscount - pointsDiscount) : 0;
const passCardDiscount = state.checkout.usePassCard ? 21 : 0;
const payable = Math.max(0, summary.total - couponDiscount - pointsDiscount - balanceDiscount - passCardDiscount);
const issues = [];
if (state.currentScene === 'delivery') {
if (!address) issues.push('请选择配送地址');
if (address && address.outOfRange) issues.push('当前地址超出配送范围');
if (summary.gapToMinOrder > 0) issues.push(`还差 ${currency(summary.gapToMinOrder)} 起送`);
}
if (state.currentScene === 'pickup' && (!state.checkout.pickupName || !state.checkout.pickupPhone)) issues.push('请完善取餐人信息');
if (currentStore().status !== '营业中') issues.push('门店当前休息中');
return { summary, address, coupon, couponDiscount, pointsDiscount, balanceDiscount, passCardDiscount, payable, issues };
}
function filteredOrders() {
let list = clone(state.orders);
if (state.orderFilter === 'processing') list = list.filter((item) => ['paid_wait_accept', 'preparing', 'delivering', 'wait_pickup', 'dine_in_serving'].includes(item.status));
else if (state.orderFilter === 'finished') list = list.filter((item) => item.status === 'finished');
else if (state.orderFilter === 'refund') list = list.filter((item) => ['refunding', 'refunded'].includes(item.status));
else if (state.orderFilter !== 'all') list = list.filter((item) => item.status === state.orderFilter);
if (state.orderScene !== 'all') list = list.filter((item) => item.scene === state.orderScene);
if (state.debugState === 'empty') return [];
return list;
}
function applicableCoupons() {
return state.coupons.filter((item) => {
if (state.couponFilter === 'all') return true;
return item.type === state.couponFilter;
}).filter((item) => state.debugState === 'empty' ? false : true);
}
function navigate(route, params = {}) {
if (state.route !== route) state.history.push({ route: state.route, params: clone(state.params) });
state.route = route;
state.params = params;
renderApp();
}
function switchTab(route) {
state.route = route;
state.params = {};
state.history = [];
state.showProductDrawer = false;
state.showCartDrawer = false;
renderApp();
}
function goBack() {
const last = state.history.pop();
if (last) {
state.route = last.route;
state.params = last.params || {};
} else {
state.route = 'pages/tab/home/index';
state.params = {};
}
state.showProductDrawer = false;
state.showCartDrawer = false;
renderApp();
}
function openProduct(productId, forceRoute = true) {
if (forceRoute) state.route = 'pages/tab/menu/index';
state.activeProductId = productId;
state.productVariant = productById(productId).variants[0].id;
state.productAddons = [];
state.productQty = 1;
state.showProductDrawer = true;
state.showCartDrawer = false;
renderApp();
}
function selectedProduct() {
const product = productById(state.activeProductId);
if (!product) return null;
const variant = product.variants.find((item) => item.id === state.productVariant) || product.variants[0];
const addonList = product.addons.filter((item) => state.productAddons.includes(item.id));
const addonTotal = addonList.reduce((sum, item) => sum + item.price, 0);
return { product, variant, addonList, total: (variant.price + addonTotal) * state.productQty };
}
function addCurrentSelectionToCart() {
const selected = selectedProduct();
if (!selected) return;
const newLine = {
key: makeLineKey(),
productId: selected.product.id,
name: selected.product.name,
variantLabel: selected.variant.label,
addons: selected.addonList.map((item) => item.label),
unitPrice: selected.variant.price + selected.addonList.reduce((sum, item) => sum + item.price, 0),
qty: state.productQty
};
state.cart.unshift(newLine);
state.showProductDrawer = false;
renderApp();
}
function addQuickCart(productId) {
const product = productById(productId);
const variant = product.variants[0];
state.cart.unshift({ key: makeLineKey(), productId, name: product.name, variantLabel: variant.label, addons: [], unitPrice: variant.price, qty: 1 });
renderApp();
}
function statusTag(text, type) { return `<span class="tag ${type}">${text}</span>`; }
function infoRow(label, value) { return `<div class="space-between"><span class="muted">${label}</span><strong>${value}</strong></div>`; }
function emptyState(title, desc, actionLabel, action) { return `<div class="card"><div class="section-title">${title}</div><div class="section-subtitle">${desc}</div>${actionLabel ? `<button class="btn btn-secondary" data-action="${action}">${actionLabel}</button>` : ''}</div>`; }
function topBar(title, subtitle = '', showBack = false) { return `<div class="topbar">${showBack ? `<button class="back-button" data-action="back"></button>` : `<div class="icon-circle">☰</div>`}<div><div class="topbar-title">${title}</div>${subtitle ? `<div class="topbar-subtitle">${subtitle}</div>` : ''}</div><div class="icon-circle" data-action="goto-messages">✉</div></div>`; }
function storeStrip() { const store = currentStore(); return `<div class="card store-strip" data-action="open-store-select"><div class="space-between"><div class="store-title">${store.name}</div>${statusTag(store.status, store.status === '营业中' ? 'success' : 'warning')}</div><div class="muted">${store.address}</div><div class="space-between"><span class="muted">${store.distance} · ${store.hours}</span><span class="chip">切换门店</span></div></div>`; }
function sceneSwitch() { return `<div class="scene-switch">${SCENES.map((item) => `<div class="scene-item ${state.currentScene === item.key ? 'active' : ''}" data-action="set-scene" data-scene="${item.key}"><strong>${item.label}</strong><span>${item.tip}</span></div>`).join('')}</div>`; }
function renderTabbar() { return `<div class="bottom-tabs">${ROUTES.filter((item) => item.type === 'tab').map((item) => `<div class="tab-item ${state.route === item.path ? 'active' : ''}" data-action="switch-tab" data-route="${item.path}"><span>${item.code}</span><strong>${item.title}</strong></div>`).join('')}</div>`; }
function renderProductCard(item, mode = 'menu') {
const disabled = item.soldOut || !item.scenes.includes(state.currentScene);
return `
<div class="product-card" data-action="open-product" data-product-id="${item.id}">
<div class="product-image" style="background: linear-gradient(135deg, ${item.image}, #fff1eb)"><span class="product-badge">${item.badge}</span></div>
<div>
<div class="space-between"><div class="product-name">${item.name}</div>${disabled ? statusTag(item.soldOut ? '已售罄' : '当前场景不可售', 'warning') : ''}</div>
<div class="product-desc">${item.desc}</div>
<div class="tag-row">${item.tags.map((tag) => `<span class="tag">${tag}</span>`).join('')}</div>
<div class="space-between" style="margin-top: 10px;">
<div class="price-row"><span class="price">¥${item.price}</span><span class="origin-price">¥${item.origin}</span></div>
<button class="mini-btn ${mode === 'home' ? 'secondary' : 'primary'}" data-action="${mode === 'home' ? 'open-product' : 'quick-add'}" data-product-id="${item.id}" ${disabled ? 'disabled' : ''}>${mode === 'home' ? '查看' : '加购'}</button>
</div>
</div>
</div>`;
}
function renderOrderCard(order) {
const meta = statusMeta(order.status);
const actions = order.status === 'pending_pay'
? [{ key: 'cancel-order', label: '取消订单', style: 'ghost' }, { key: 'continue-pay', label: '继续支付', style: 'primary' }]
: order.status === 'finished'
? [{ key: 'reorder', label: '再来一单', style: 'ghost' }, { key: 'open-review', label: '去评价', style: 'primary' }]
: ['refunding', 'refunded'].includes(order.status)
? [{ key: 'open-refund-detail', label: '退款详情', style: 'primary' }]
: [{ key: 'open-order-detail', label: '查看详情', style: 'ghost' }, { key: 'urge-order', label: '催单', style: 'primary' }];
return `
<div class="order-card" data-action="open-order-detail" data-order-id="${order.id}">
<div class="space-between"><strong>${STORES[order.storeId].name}</strong>${statusTag(meta.text, meta.type)}</div>
<div class="tag-row" style="margin-top: 10px;">${statusTag(sceneLabel(order.scene), 'secondary')}<span class="muted">${order.createdAt}</span></div>
<div style="margin-top: 10px; font-size: 14px; color: #374151;">${order.items.map((item) => item.name).join('、')}</div>
<div class="space-between" style="margin-top: 14px;">
<div><div class="muted">共 ${order.items.length} 件商品</div><div class="price">${currency(order.amount.payable)}</div></div>
<div class="row">${actions.map((item) => `<button class="mini-btn ${item.style}" data-action="${item.key}" data-order-id="${order.id}">${item.label}</button>`).join('')}</div>
</div>
</div>`;
}
function renderHome() {
const service = currentStore();
const sections = [
{ title: '热销推荐', desc: '本店今日高频加购', items: ['latte_oat', 'orange_americano', 'tiramisu_box'] },
{ title: '套餐推荐', desc: '早餐午后都适配', items: ['breakfast_combo', 'brunch_combo'] },
{ title: '复购推荐', desc: '根据最近订单推荐', items: ['dirty_coconut', 'chicken_wrap'] },
{ title: '猜你喜欢', desc: '偏好口味为你匹配', items: ['grape_jasmine', 'berry_yogurt', 'mushroom_panini'] }
];
return `
<div class="page with-safe-bottom">
${topBar(`当前位置 · ${service.shortName}`, `${service.distance} · ${currentSceneInfo().label}`)}
${state.debugState === 'error' ? `<div class="card"><strong>定位失败</strong><div class="section-subtitle">已切换到手动选店模式,请点击门店条重新选择。</div></div>` : ''}
${storeStrip()}
${sceneSwitch()}
<div class="card banner-list">${HOME_BANNERS.map((item) => `<div class="banner-card" style="background:${item.color}" data-action="navigate" data-route="${item.route}"><div class="banner-title">${item.title}</div><div class="banner-subtitle">${item.subtitle}</div></div>`).join('')}</div>
<div class="card"><div class="section-header"><h3>快捷入口</h3><span>活动与资产导流</span></div><div class="quick-grid">${HOME_QUICK.map((item) => `<div class="quick-card" data-action="navigate" data-route="${item.route}"><div class="quick-title">${item.title}</div><div class="quick-subtitle">${item.subtitle}</div></div>`).join('')}</div></div>
${sections.map((section) => `<div class="card"><div class="section-header"><h3>${section.title}</h3><span>${section.desc}</span></div><div class="product-list">${section.items.map((id) => renderProductCard(productById(id), 'home')).join('')}</div></div>`).join('')}
<div class="card"><div class="section-header"><h3>门店服务信息</h3><span>G12 + G13</span></div>${infoRow('营业时间', service.hours)}${infoRow('配送说明', `起送 ¥${service.minOrder} / 配送费 ¥${service.deliveryFee} / 满 ¥${service.freeDelivery} 免配送`)}${infoRow('自提规则', service.pickupRule)}${infoRow('堂食规则', service.dineRule)}${infoRow('联系门店', service.phone)}${infoRow('到店导航', `${service.distance} · 步行约 8 分钟`)}</div>
${renderTabbar()}
</div>`;
}
function renderMenu() {
const products = filteredProducts();
const summary = cartSummary();
return `
<div class="page with-safe-bottom">
${topBar('点餐页', `${currentStore().shortName} · ${currentSceneInfo().label}`)}
${storeStrip()}
${sceneSwitch()}
<div class="card"><div class="section-header"><h3>搜索与活动提示</h3><span>${state.currentScene === 'delivery' ? '突出配送门槛' : state.currentScene === 'pickup' ? '突出自提优惠' : '突出桌号与加菜'}</span></div><input class="text-input" data-model="menuSearch" value="${state.menuSearch}" placeholder="搜索商品名称" /><div class="quick-subtitle" style="margin-top: 12px;">满 ¥58 减 ¥817:00 秒杀场进行中,储值支付再享 95 折</div></div>
<div class="card"><div class="section-header"><h3>分类导航</h3><span>G20</span></div><div class="segmented">${CATEGORIES.map((item) => `<div class="filter-chip ${state.activeCategory === item.id ? 'active' : ''}" data-action="set-category" data-category="${item.id}"><strong>${item.name}</strong><span>${PRODUCTS.filter((p) => p.category === item.id).length} 款</span></div>`).join('')}</div></div>
<div class="card"><div class="section-header"><h3>商品列表</h3><span>${products.length ? `${products.length} 款商品` : '当前分类暂无商品'}</span></div>${products.length ? `<div class="product-list">${products.map((item) => renderProductCard(item)).join('')}</div>` : emptyState('暂无商品', '请切换分类或清空搜索条件', '重置筛选', 'reset-search')}</div>
<div class="cart-bar"><div><div style="font-weight: 800;">已选 ${summary.count} 件 · ${currency(summary.total)}</div><div class="muted" style="color: rgba(255,255,255,0.72);">${summary.gapToMinOrder > 0 ? `还差 ${currency(summary.gapToMinOrder)} 起送` : summary.gapToFreeDelivery > 0 ? `再买 ${currency(summary.gapToFreeDelivery)} 免配送` : '已满足起送,可去结算'}</div></div><div class="row"><button class="mini-btn ghost" data-action="open-cart">购物车</button><button class="mini-btn primary" data-action="go-checkout" ${summary.count === 0 ? 'disabled' : ''}>去结算</button></div></div>
${renderTabbar()}
${renderProductDrawer()}
${renderCartDrawer()}
</div>`;
}
function renderOrders() {
const list = filteredOrders();
return `
<div class="page with-safe-bottom">
${topBar('订单页', '按状态与场景快速筛选')}
<div class="card"><div class="section-header"><h3>状态筛选</h3><span>T03</span></div><div class="filter-row">${[
['all', '全部'], ['pending_pay', '待支付'], ['processing', '进行中'], ['finished', '已完成'], ['refund', '退款售后']
].map(([key, label]) => `<div class="filter-chip ${state.orderFilter === key ? 'active' : ''}" data-action="set-order-filter" data-value="${key}"><strong>${label}</strong><span>筛选</span></div>`).join('')}</div></div>
<div class="card"><div class="section-header"><h3>场景筛选</h3><span>外卖 / 自提 / 堂食</span></div><div class="filter-row">${[['all', '全部'], ['delivery', '外卖'], ['pickup', '自提'], ['dine_in', '堂食']].map(([key, label]) => `<div class="filter-chip ${state.orderScene === key ? 'active' : ''}" data-action="set-order-scene" data-value="${key}"><strong>${label}</strong><span>切换</span></div>`).join('')}</div></div>
${list.length ? `<div class="order-list">${list.map((item) => renderOrderCard(item)).join('')}</div>` : emptyState('暂无订单', '当前筛选条件下没有订单记录,可返回点餐页开始下单。', '去点餐', 'go-menu')}
${renderTabbar()}
</div>`;
}
function renderMe() {
const couponCount = state.coupons.filter((item) => item.claimed).length;
return `
<div class="page with-safe-bottom">
${topBar('我的', state.memberLoggedIn ? '会员、资产、服务与复购入口' : '未登录预览态')}
<div class="card" style="background: linear-gradient(135deg, #fff1eb, #ffe6d9);"><div class="space-between"><div><div class="product-name">${state.memberLoggedIn ? 'Hazel · 金卡会员' : '未登录用户'}</div><div class="section-subtitle">${state.memberLoggedIn ? '成长值 860 / 距黑金还差 340' : '登录后查看会员、优惠券、积分和储值资产'}</div></div><button class="mini-btn secondary" data-action="toggle-login">${state.memberLoggedIn ? '切换未登录' : '模拟登录'}</button></div></div>
<div class="card"><div class="section-header"><h3>资产总览</h3><span>G41</span></div><div class="asset-grid">${[
['优惠券', `${couponCount}`, 'pages/coupon/center/index'],
['积分余额', '1280', 'pages/points/mall/index'],
['储值余额', '¥246.50', 'pages/prepaid/index'],
['次卡剩余', '2 次', 'pages/pass-card/index']
].map(([title, value, route]) => `<div class="asset-item" data-action="navigate" data-route="${route}"><div class="asset-title">${title}</div><div class="price-row"><span class="price">${value}</span></div></div>`).join('')}</div></div>
<div class="card"><div class="section-header"><h3>订单快捷入口</h3><span>T03 联动</span></div><div class="quick-grid">${[
['待支付', 'pending_pay'], ['进行中', 'processing'], ['已完成', 'finished'], ['退款售后', 'refund']
].map(([title, filter]) => `<div class="quick-card" data-action="jump-orders" data-filter="${filter}"><div class="quick-title">${title}</div><div class="quick-subtitle">点击跳转订单筛选</div></div>`).join('')}</div></div>
<div class="card"><div class="section-header"><h3>会员与增长入口</h3><span>G42</span></div><div class="quick-grid">${[
['会员中心', 'pages/member/center/index'], ['领券中心', 'pages/coupon/center/index'], ['积分商城', 'pages/points/mall/index'], ['储值充值', 'pages/prepaid/index'], ['次卡页', 'pages/pass-card/index']
].map(([title, route]) => `<div class="quick-card" data-action="navigate" data-route="${route}"><div class="quick-title">${title}</div><div class="quick-subtitle">进入二级页查看详情</div></div>`).join('')}</div></div>
<div class="card"><div class="section-header"><h3>服务入口区</h3><span>消息、帮助、地址</span></div><div class="quick-grid">${[
['地址管理', 'pages/address/list/index'], ['消息中心', 'pages/message/center/index'], ['帮助中心', 'pages/help/center/index'], ['联系客服', 'pages/help/center/index']
].map(([title, route]) => `<div class="quick-card" data-action="navigate" data-route="${route}"><div class="quick-title">${title}</div><div class="quick-subtitle">可点击演示</div></div>`).join('')}</div></div>
<div class="card"><div class="section-header"><h3>复购推荐区</h3><span>最近订单与常点商品</span></div><div class="product-list">${['dirty_coconut', 'chicken_wrap'].map((id) => renderProductCard(productById(id), 'home')).join('')}</div></div>
${renderTabbar()}
</div>`;
}
function renderProductDrawer() {
if (!state.showProductDrawer) return '';
const selected = selectedProduct();
if (!selected) return '';
return `<div class="drawer-mask" data-action="close-overlays"></div><div class="drawer"><div class="section-header"><h3>C01 商品详情抽屉</h3><button class="mini-btn ghost" data-action="close-overlays">关闭</button></div><div class="product-card" style="grid-template-columns: 94px minmax(0,1fr); margin-top: 0;"><div class="product-image" style="background: linear-gradient(135deg, ${selected.product.image}, #fff1eb)"><span class="product-badge">${selected.product.badge}</span></div><div><div class="product-name">${selected.product.name}</div><div class="product-desc">${selected.product.desc}</div><div class="price-row"><span class="price">${currency(selected.variant.price)}</span><span class="origin-price">¥${selected.product.origin}</span></div></div></div><div class="card" style="margin-top: 14px;"><div class="section-header"><h3>规格做法</h3><span>G23</span></div><div class="filter-row">${selected.product.variants.map((item) => `<div class="filter-chip ${state.productVariant === item.id ? 'active' : ''}" data-action="set-product-variant" data-variant="${item.id}"><strong>${item.label}</strong><span>${currency(item.price)}</span></div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>加料区</h3><span>G24</span></div><div class="filter-row">${selected.product.addons.length ? selected.product.addons.map((item) => `<div class="filter-chip ${state.productAddons.includes(item.id) ? 'active' : ''}" data-action="toggle-addon" data-addon="${item.id}"><strong>${item.label}</strong><span>+ ${currency(item.price)}</span></div>`).join('') : '<div class="section-subtitle">当前商品无附加项</div>'}</div></div><div class="card"><div class="section-header"><h3>数量与金额</h3><span>G22</span></div><div class="space-between"><div class="row"><button class="mini-btn ghost" data-action="change-product-qty" data-step="-1">-</button><strong>${state.productQty}</strong><button class="mini-btn ghost" data-action="change-product-qty" data-step="1">+</button></div><div class="price">${currency(selected.total)}</div></div></div><div class="fixed-actions"><div><div style="font-weight: 800;">已选组合金额</div><div class="muted" style="color: rgba(255,255,255,0.72);">规格未选全时按钮禁用 · 当前已满足</div></div><button class="btn btn-primary" data-action="confirm-add-cart">加入购物车</button></div></div>`;
}
function renderCartDrawer() {
if (!state.showCartDrawer) return '';
const summary = cartSummary();
return `<div class="drawer-mask" data-action="close-overlays"></div><div class="drawer"><div class="section-header"><h3>C02 购物车抽屉</h3><div class="row"><button class="mini-btn danger" data-action="clear-cart">清空</button><button class="mini-btn ghost" data-action="close-overlays">关闭</button></div></div>${state.cart.length ? `<div class="product-list">${state.cart.map((item) => `<div class="summary-card"><div class="space-between"><strong>${item.name}</strong><div class="row"><button class="mini-btn ghost" data-action="change-cart-qty" data-key="${item.key}" data-step="-1">-</button><strong>${item.qty}</strong><button class="mini-btn ghost" data-action="change-cart-qty" data-key="${item.key}" data-step="1">+</button></div></div><div class="section-subtitle">${item.variantLabel}${item.addons.length ? ` · ${item.addons.join(' / ')}` : ''}</div><div class="price-row"><span class="price">${currency(item.unitPrice * item.qty)}</span></div></div>`).join('')}</div><div class="card"><div class="section-header"><h3>优惠与凑单提示</h3><span>G26</span></div>${infoRow('满减差额', summary.total >= 58 ? '已命中满减' : `再买 ${currency(58 - summary.total)}`)}${infoRow('起送差额', summary.gapToMinOrder > 0 ? currency(summary.gapToMinOrder) : '已满足')}${infoRow('免配送费差额', summary.gapToFreeDelivery > 0 ? currency(summary.gapToFreeDelivery) : '已满足')}<div class="section-subtitle" style="margin-top: 10px;">已命中活动:满 ¥58 减 ¥8 / 17:00 秒杀活动</div></div>` : emptyState('购物车为空', '请先在点餐页选择商品。', '', '')}<div class="fixed-actions"><div><div style="font-weight: 800;">购物车总额 ${currency(summary.total)}</div><div class="muted" style="color: rgba(255,255,255,0.72);">统一去结算主路径</div></div><button class="btn btn-primary" data-action="go-checkout" ${!state.cart.length ? 'disabled' : ''}>去结算</button></div></div>`;
}
function currentOrder() {
const orderId = state.params.orderId || state.selectedOrderId || state.lastOrderId;
return state.orders.find((item) => item.id === orderId) || state.orders[0];
}
function secondaryPage(title, subtitle, inner, actions = '') {
return `<div class="page with-safe-bottom">${topBar(title, subtitle, true)}${inner}${actions}</div>`;
}
function renderStoreSelect() {
const list = Object.values(STORES).filter((item) => item.supports.includes(state.currentScene)).filter((item) => state.debugState === 'empty' ? false : true);
return secondaryPage('门店选择页', '搜索、筛选、切店返回来源页', `<div class="card"><input class="text-input" placeholder="按门店名称或商圈搜索(原型占位)" /></div>${list.length ? `<div class="address-list">${list.map((item) => `<div class="address-card"><div class="space-between"><strong>${item.name}</strong>${statusTag(item.status, item.status === '营业中' ? 'success' : 'warning')}</div><div class="section-subtitle" style="margin-top: 8px;">${item.address}</div><div class="tag-row" style="margin-top: 10px;">${item.supports.map((scene) => statusTag(sceneLabel(scene), 'secondary')).join('')}</div><div class="space-between" style="margin-top: 12px;"><span class="muted">${item.distance} · 起送 ¥${item.minOrder} / 配送费 ¥${item.deliveryFee}</span><button class="mini-btn primary" data-action="choose-store" data-store-id="${item.id}">切换到此门店</button></div></div>`).join('')}</div>` : emptyState('暂无可选门店', '当前区域或场景下没有可服务门店。', '返回首页', 'back')}`);
}
function renderAddressList() {
const list = state.debugState === 'empty' ? [] : state.addresses;
return secondaryPage('地址管理页', '设为默认、编辑、删除、结算选择', `${list.length ? `<div class="address-list">${list.map((item) => `<div class="address-card"><div class="space-between"><strong>${item.name} ${item.phone}</strong>${item.default ? statusTag('默认地址', 'primary') : ''}</div><div class="section-subtitle" style="margin-top: 8px;">${item.detail}</div><div class="section-subtitle">${item.tip}</div><div class="row" style="margin-top: 12px;">${!item.default ? `<button class="mini-btn secondary" data-action="set-default-address" data-address-id="${item.id}">设为默认</button>` : ''}<button class="mini-btn ghost" data-action="select-address" data-address-id="${item.id}">用于本次结算</button><button class="mini-btn danger" data-action="delete-address" data-address-id="${item.id}">删除</button></div></div>`).join('')}</div>` : emptyState('暂无地址', '请先新增一个可用于外卖配送的地址。', '新增地址', 'add-address')}`, `<div class="fixed-actions"><div><div style="font-weight:800;">P02 地址管理</div><div class="muted" style="color:rgba(255,255,255,.72);">当前文档以一个管理页为主</div></div><button class="btn btn-primary" data-action="add-address">新增地址</button></div>`);
}
function renderCheckout() {
const check = checkoutData();
const scene = state.currentScene;
const scenarioCard = scene === 'delivery'
? `<div class="summary-card">${infoRow('配送地址', check.address ? check.address.detail : '请选择地址')}${infoRow('预计送达', check.address ? check.address.tip : '待选择地址')}</div>`
: scene === 'pickup'
? `<div class="summary-card">${infoRow('自提门店', currentStore().name)}${infoRow('自提时段', state.checkout.pickupSlot)}${infoRow('取餐人', `${state.checkout.pickupName} ${state.checkout.pickupPhone}`)}</div>`
: `<div class="summary-card">${infoRow('堂食门店', currentStore().name)}${infoRow('桌号', 'A12')}${infoRow('堂食说明', '支持继续加菜,不展示配送信息')}</div>`;
return secondaryPage('结算确认页', `${currentSceneInfo().label} · 信息确认 + 金额确认`, `<div class="card"><div class="section-header"><h3>场景信息卡</h3><span>P03</span></div>${scenarioCard}${check.issues.length ? `<div class="section-subtitle" style="margin-top: 12px; color: var(--danger);">${check.issues.join('')}</div>` : ''}</div><div class="card"><div class="section-header"><h3>商品清单区</h3><span>G31</span></div><div class="record-list">${state.cart.map((item) => `<div class="summary-card">${infoRow(item.name, `${item.variantLabel}${item.addons.length ? ` / ${item.addons.join(' / ')}` : ''}`)}${infoRow('数量', `${item.qty}`)}${infoRow('小计', currency(item.unitPrice * item.qty))}</div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>优惠资产区</h3><span>G32</span></div><div class="record-list"><div class="summary-card space-between"><div><strong>优惠券</strong><div class="section-subtitle">${check.coupon ? check.coupon.title : '未选择优惠券'}</div></div><button class="mini-btn secondary" data-action="cycle-coupon">切换</button></div><div class="summary-card space-between"><div><strong>积分抵扣</strong><div class="section-subtitle">当前 ${state.checkout.usePoints ? '已使用,抵扣 ¥5' : '未使用'}</div></div><button class="mini-btn ${state.checkout.usePoints ? 'primary' : 'ghost'}" data-action="toggle-checkout-flag" data-flag="usePoints">切换</button></div><div class="summary-card space-between"><div><strong>储值余额支付</strong><div class="section-subtitle">${state.checkout.useBalance ? '已启用,模拟抵扣 ¥12' : '未启用'}</div></div><button class="mini-btn ${state.checkout.useBalance ? 'primary' : 'ghost'}" data-action="toggle-checkout-flag" data-flag="useBalance">切换</button></div><div class="summary-card space-between"><div><strong>次卡核销</strong><div class="section-subtitle">${state.checkout.usePassCard ? '已使用拿铁次卡' : '当前不使用次卡'}</div></div><button class="mini-btn ${state.checkout.usePassCard ? 'primary' : 'ghost'}" data-action="toggle-checkout-flag" data-flag="usePassCard">切换</button></div></div></div><div class="card"><div class="section-header"><h3>备注与附加信息</h3><span>订单备注 / 餐具</span></div><textarea class="text-area" data-model="checkoutRemark" placeholder="填写口味、忌口、特殊要求">${state.checkout.remark}</textarea><div class="row" style="margin-top: 12px;"><button class="mini-btn ${state.checkout.utensils === '无需餐具' ? 'primary' : 'ghost'}" data-action="set-utensils" data-value="无需餐具">无需餐具</button><button class="mini-btn ${state.checkout.utensils === '1 份餐具' ? 'primary' : 'ghost'}" data-action="set-utensils" data-value="1 份餐具">1 份餐具</button><button class="mini-btn ${state.checkout.utensils === '2 份餐具' ? 'primary' : 'ghost'}" data-action="set-utensils" data-value="2 份餐具">2 份餐具</button></div></div><div class="card"><div class="section-header"><h3>费用明细区</h3><span>G31</span></div>${infoRow('商品金额', currency(check.summary.subtotal))}${infoRow('打包费', currency(check.summary.packing))}${infoRow('餐具费', currency(check.summary.utensil))}${infoRow('配送费', currency(check.summary.delivery))}${infoRow('优惠减免', `- ${currency(check.couponDiscount + check.pointsDiscount + check.balanceDiscount + check.passCardDiscount)}`)}${infoRow('实付金额', currency(check.payable))}</div>`, `<div class="fixed-actions"><div><div style="font-weight:800;">实付金额 ${currency(check.payable)}</div><div class="muted" style="color:rgba(255,255,255,.72);">${check.issues.length ? check.issues[0] : '所有影响实付金额项目均可见'}</div></div><button class="btn btn-primary" data-action="submit-order" ${check.issues.length || !state.cart.length ? 'disabled' : ''}>提交订单并支付</button></div>`);
}
function renderPaymentSuccess() {
const order = state.orders.find((item) => item.id === state.lastOrderId) || currentOrder();
return secondaryPage('支付成功页', '明确告诉用户支付已完成', `<div class="card" style="text-align:center;"><div class="product-name">支付成功</div><div class="section-subtitle">订单已提交,等待商家处理</div></div><div class="card"><div class="section-header"><h3>订单摘要</h3><span>P04</span></div>${infoRow('订单号', order.number)}${infoRow('门店', STORES[order.storeId].name)}${infoRow('支付金额', currency(order.amount.payable))}</div><div class="card"><div class="section-header"><h3>场景关键信息</h3><span>${sceneLabel(order.scene)}</span></div>${order.scene === 'delivery' ? `${infoRow('预计送达', '约 30 分钟后')} ${infoRow('收货地址', order.address || '朝阳区建国门外大街 88 号')}` : order.scene === 'pickup' ? `${infoRow('取餐时间', order.pickupSlot || '今日 18:20-18:30')}${infoRow('取餐码', order.pickupCode || 'A928')}` : `${infoRow('桌号', order.tableNo || 'A12')}${infoRow('用餐状态', '堂食进行中,可继续加菜')}`}</div>`, `<div class="fixed-actions"><div><div style="font-weight:800;">支付成功</div><div class="muted" style="color:rgba(255,255,255,.72);">建议继续查看订单详情</div></div><div class="row"><button class="btn btn-ghost" data-action="go-menu">继续点餐</button><button class="btn btn-primary" data-action="navigate" data-route="pages/order/detail/index" data-order-id="${order.id}">查看订单详情</button></div></div>`);
}
function renderOrderDetail() {
const order = currentOrder();
const meta = statusMeta(order.status);
return secondaryPage('订单详情页', meta.desc, `<div class="card"><div class="section-header"><h3>顶部状态头图</h3><span>${routeMeta().code}</span></div><div class="space-between"><div><div class="product-name">${meta.text}</div><div class="section-subtitle">${meta.desc}</div></div>${statusTag(meta.text, meta.type)}</div></div><div class="card"><div class="section-header"><h3>场景信息区</h3><span>${sceneLabel(order.scene)}</span></div>${order.scene === 'delivery' ? `${infoRow('收货地址', order.address)}${infoRow('预计送达', '约 30 分钟')} ` : order.scene === 'pickup' ? `${infoRow('取餐码', order.pickupCode || 'A603')}${infoRow('取餐时间', order.pickupSlot || '08:30-08:40')}` : `${infoRow('桌号', order.tableNo || 'A12')}${infoRow('堂食状态', '可继续加菜')}`}</div><div class="card"><div class="section-header"><h3>履约时间轴</h3><span>G34</span></div><div class="timeline">${order.timeline.map((item, index) => `<div class="summary-card">${index + 1}. ${item}</div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>商品清单区</h3><span>${order.items.length} 件</span></div><div class="record-list">${order.items.map((item) => `<div class="summary-card">${infoRow(item.name, item.variantLabel)}${item.addons.length ? infoRow('加料', item.addons.join(' / ')) : ''}${infoRow('数量', `${item.qty}`)}${infoRow('小计', currency(item.unitPrice * item.qty))}</div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>费用明细区</h3><span>G31</span></div>${infoRow('商品金额', currency(order.amount.subtotal))}${infoRow('打包费', currency(order.amount.packing))}${infoRow('餐具费', currency(order.amount.utensil))}${infoRow('配送费', currency(order.amount.delivery))}${infoRow('优惠减免', `- ${currency(order.amount.discount)}`)}${infoRow('实付金额', currency(order.amount.payable))}</div><div class="card"><div class="section-header"><h3>订单基础信息</h3><span>基础字段</span></div>${infoRow('订单号', order.number)}${infoRow('下单时间', order.createdAt)}${infoRow('支付方式', order.payMethod)}${infoRow('支付时间', order.paidAt || '待支付')} </div>`, `<div class="fixed-actions"><div><div style="font-weight:800;">${meta.text}</div><div class="muted" style="color:rgba(255,255,255,.72);">状态强表达页面</div></div><div class="row">${order.status === 'pending_pay' ? `<button class="btn btn-ghost" data-action="cancel-order" data-order-id="${order.id}">取消订单</button><button class="btn btn-primary" data-action="continue-pay" data-order-id="${order.id}">继续支付</button>` : order.status === 'finished' ? `<button class="btn btn-ghost" data-action="reorder" data-order-id="${order.id}">再来一单</button><button class="btn btn-primary" data-action="open-review" data-order-id="${order.id}">去评价</button>` : ['refunding', 'refunded'].includes(order.status) ? `<button class="btn btn-primary" data-action="open-refund-detail" data-order-id="${order.id}">退款详情</button>` : `<button class="btn btn-ghost" data-action="open-refund-apply" data-order-id="${order.id}">申请退款</button><button class="btn btn-primary" data-action="urge-order" data-order-id="${order.id}">催单</button>`}</div></div>`);
}
function renderRefundApply() {
const order = currentOrder();
return secondaryPage('退款申请页', '低阻力提交退款原因与说明', `<div class="card"><div class="section-header"><h3>可退款订单摘要</h3><span>P06</span></div>${infoRow('门店', STORES[order.storeId].name)}${infoRow('订单号', order.number)}${infoRow('商品摘要', order.items.map((item) => item.name).join('、'))}${infoRow('当前可退金额', currency(order.amount.payable / 2))}</div><div class="card"><div class="section-header"><h3>退款原因区</h3><span>预设原因</span></div><div class="filter-row">${['商品与描述不符', '配送超时', '少送漏送', '不想要了', '其他原因'].map((item) => `<div class="filter-chip ${state.refundDraft.reason === item ? 'active' : ''}" data-action="set-refund-reason" data-value="${item}"><strong>${item}</strong><span>点击选择</span></div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>补充说明区</h3><span>可选上传凭证图片</span></div><textarea class="text-area" data-model="refundRemark" placeholder="输入补充说明">${state.refundDraft.remark}</textarea></div>`, `<div class="fixed-actions"><div><div style="font-weight:800;">提交退款申请</div><div class="muted" style="color:rgba(255,255,255,.72);">原因未选时不可提交</div></div><button class="btn btn-primary" data-action="submit-refund" ${!state.refundDraft.reason ? 'disabled' : ''}>提交退款申请</button></div>`);
}
function renderRefundDetail() {
const order = currentOrder();
const refund = order.refund || { status: 'processing', amount: 18, reason: '配送超时', remark: state.refundDraft.remark, rejectReason: '', timeline: ['提交退款申请', '商家处理中', '退款完成'] };
const type = refund.status === 'rejected' ? 'danger' : refund.status === 'refunded' ? 'success' : 'warning';
const text = refund.status === 'rejected' ? '已拒绝' : refund.status === 'refunded' ? '已退款' : '处理中';
return secondaryPage('退款详情页', '展示退款状态、金额、原因与时间轴', `<div class="card"><div class="space-between"><div><div class="product-name">${text}</div><div class="section-subtitle">${refund.status === 'processing' ? '预计 1-2 小时内完成' : refund.status === 'refunded' ? '退款金额已原路返回' : refund.rejectReason || '请联系门店客服了解详情'}</div></div>${statusTag(text, type)}</div></div><div class="card">${infoRow('退款金额', currency(refund.amount))}${infoRow('退款原因', refund.reason)}${infoRow('补充说明', refund.remark || '无')}</div><div class="card"><div class="section-header"><h3>退款时间轴</h3><span>G34</span></div><div class="timeline">${refund.timeline.map((item, index) => `<div class="summary-card">${index + 1}. ${item}</div>`).join('')}</div></div><div class="card">${infoRow('订单号', order.number)}${infoRow('关联门店', STORES[order.storeId].name)}${infoRow('商品摘要', order.items.map((item) => item.name).join('、'))}</div>`, `<div class="fixed-actions"><div><div style="font-weight:800;">退款状态:${text}</div><div class="muted" style="color:rgba(255,255,255,.72);">拒绝状态需明确展示驳回原因</div></div><button class="btn btn-primary" data-action="navigate" data-route="pages/order/detail/index" data-order-id="${order.id}">返回订单详情</button></div>`);
}
function renderReview() {
const order = currentOrder();
return secondaryPage('评价页', '星级、标签、文本、匿名与奖励闭环', `<div class="card">${infoRow('门店', STORES[order.storeId].name)}${infoRow('商品摘要', order.items.map((item) => item.name).join('、'))}</div><div class="card"><div class="section-header"><h3>星级评分区</h3><span>1-5 星</span></div><div class="row">${[1,2,3,4,5].map((num) => `<button class="mini-btn ${state.reviewDraft.rating >= num ? 'primary' : 'ghost'}" data-action="set-rating" data-value="${num}">★</button>`).join('')}</div></div><div class="card"><div class="section-header"><h3>快捷评价标签</h3><span>随评分变化</span></div><div class="filter-row">${['味道不错', '包装精致', '配送很快', '分量足', '还会再买'].map((item) => `<div class="filter-chip ${state.reviewDraft.tags.includes(item) ? 'active' : ''}" data-action="toggle-review-tag" data-value="${item}"><strong>${item}</strong><span>点击切换</span></div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>文本评价区</h3><span>支持晒单说明</span></div><textarea class="text-area" data-model="reviewComment" placeholder="输入你的评价内容">${state.reviewDraft.comment}</textarea><div class="section-subtitle" style="margin-top: 12px;">图片上传区:原型中用占位表达,不接真实上传。</div></div><div class="card"><div class="space-between"><div><strong>匿名评价</strong><div class="section-subtitle">保护顾客隐私</div></div><button class="mini-btn ${state.reviewDraft.anonymous ? 'primary' : 'ghost'}" data-action="toggle-anonymous">${state.reviewDraft.anonymous ? '已匿名' : '未匿名'}</button></div></div>`, `<div class="fixed-actions"><div><div style="font-weight:800;">提交评价</div><div class="muted" style="color:rgba(255,255,255,.72);">评价成功可获得积分奖励</div></div><button class="btn btn-primary" data-action="submit-review" ${!state.reviewDraft.rating ? 'disabled' : ''}>提交评价</button></div>`);
}
function renderCouponCenter() {
const list = applicableCoupons();
return secondaryPage('领券中心页', '券分类筛选 + 去使用回流点餐页', `<div class="card"><div class="filter-row">${[['all','全部'],['cash','满减券'],['discount','折扣券'],['shipping','免配送费券']].map(([key,label]) => `<div class="filter-chip ${state.couponFilter === key ? 'active' : ''}" data-action="set-coupon-filter" data-value="${key}"><strong>${label}</strong><span>筛选</span></div>`).join('')}</div></div>${list.length ? `<div class="record-list">${list.map((item) => `<div class="activity-card"><div class="space-between"><strong>${item.title}</strong>${statusTag(item.claimed ? (item.expired ? '已过期' : '已领取') : '可领取', item.claimed ? (item.expired ? 'warning' : 'success') : 'primary')}</div><div class="price-row"><span class="price">${item.value}</span><span class="muted">${item.threshold}</span></div><div class="section-subtitle">${item.scope} · 有效期至 ${item.validUntil}</div><div class="row" style="margin-top: 12px;"><button class="mini-btn ${item.claimed ? 'ghost' : 'primary'}" data-action="claim-coupon" data-coupon-id="${item.id}" ${item.claimed ? 'disabled' : ''}>${item.claimed ? '已领取' : '立即领取'}</button><button class="mini-btn secondary" data-action="go-menu">去点餐</button></div></div>`).join('')}</div>` : emptyState('暂无可领取优惠券', '可切换筛选或稍后再来查看。', '返回首页', 'go-home')}`, `<div class="fixed-actions"><div><div style="font-weight:800;">P09 领券中心</div><div class="muted" style="color:rgba(255,255,255,.72);">领取动作轻量,领取后不强制跳转</div></div><button class="btn btn-primary" data-action="go-menu">去点餐</button></div>`);
}
function renderSeckill() { return secondaryPage('秒杀活动页', '突出时间紧迫感和库存稀缺感', `<div class="card" style="background: linear-gradient(135deg, #fb7185, #ef4444); color: #fff;"><div class="product-name">17:00 秒杀场进行中</div><div class="section-subtitle" style="color: rgba(255,255,255,.85);">倒计时 00:26:18 · 限量抢购</div></div><div class="card"><div class="filter-row">${[['now','当前场次'],['next','即将开始'],['ended','已结束']].map(([key,label]) => `<div class="filter-chip ${state.seckillTab === key ? 'active' : ''}" data-action="set-seckill-tab" data-value="${key}"><strong>${label}</strong><span>${key === 'now' ? '进行中' : key === 'next' ? '20:00 开抢' : '已结束'}</span></div>`).join('')}</div></div><div class="product-list">${['latte_oat','tiramisu_box','rose_coldbrew'].map((id) => renderProductCard(productById(id))).join('')}</div>`); }
function renderFlashSale() { return secondaryPage('限时折扣活动页', '弱化“抢”,强化“省”', `<div class="card" style="background: linear-gradient(135deg, #f59e0b, #f97316); color: #fff;"><div class="product-name">限时折扣专场</div><div class="section-subtitle" style="color: rgba(255,255,255,.85);">当前进行中 · 全场精选直降</div></div><div class="card"><div class="filter-row">${[['current','进行中'],['upcoming','即将开始'],['ended','已结束']].map(([key,label]) => `<div class="filter-chip ${state.flashTab === key ? 'active' : ''}" data-action="set-flash-tab" data-value="${key}"><strong>${label}</strong><span>${key === 'current' ? '可购买' : key === 'upcoming' ? '查看开抢时间' : '置灰展示'}</span></div>`).join('')}</div></div><div class="product-list">${['brunch_combo','orange_americano','berry_yogurt'].map((id) => renderProductCard(productById(id))).join('')}</div>`); }
function renderMemberCenter() { return secondaryPage('会员中心页', '当前能享受什么 + 如何升级', `<div class="card" style="background: linear-gradient(135deg, #fff1eb, #ffe4d5);"><div class="product-name">Hazel · 金卡会员</div><div class="section-subtitle">成长值 860 / 距离黑金还差 340</div></div><div class="card"><div class="section-header"><h3>权益总览区</h3><span>当前可享权益</span></div><div class="record-list">${['会员折扣:饮品 95 折','积分倍率:消费 1 元得 1.2 积分','生日权益:生日月赠饮品兑换券','会员日权益:每月 18 日双倍积分'].map((item) => `<div class="benefit-card">${item}</div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>等级说明区</h3><span>升级路径</span></div><div class="record-list">${['银卡会员:成长值 0-399','金卡会员:成长值 400-1199','黑金会员:成长值 1200+'].map((item) => `<div class="summary-card">${item}</div>`).join('')}</div></div>`); }
function renderPointsMall() { return secondaryPage('积分商城页', '展示积分余额、可兑换商品和兑换记录', `<div class="card"><div class="space-between"><div><div class="product-name">当前积分 1280</div><div class="section-subtitle">积分能兑换饮品券、周边与甜品</div></div><button class="mini-btn secondary">积分说明</button></div></div><div class="card"><div class="filter-row">${[['goods','兑换商品'],['records','兑换记录']].map(([key,label]) => `<div class="filter-chip ${state.pointsTab === key ? 'active' : ''}" data-action="set-points-tab" data-value="${key}"><strong>${label}</strong><span>切换</span></div>`).join('')}</div></div>${state.pointsTab === 'goods' ? `<div class="record-list">${POINTS_GOODS.map((item) => `<div class="activity-card"><div class="space-between"><strong>${item.title}</strong>${statusTag(item.tag, 'primary')}</div><div class="price-row"><span class="price">${item.points} 积分</span><span class="muted">${item.stock}</span></div><button class="mini-btn primary">立即兑换</button></div>`).join('')}</div>` : `<div class="record-list">${[{title:'兑换 ¥10 饮品券',desc:'已到账',time:'2026-03-04 15:08'},{title:'订单评价奖励',desc:'已发放',time:'2026-03-03 18:55'}].map((item) => `<div class="record-card"><strong>${item.title}</strong><div class="section-subtitle">${item.desc} · ${item.time}</div></div>`).join('')}</div>`}`); }
function renderPrepaid() { return secondaryPage('储值充值页', '余额、充值方案和充值记录', `<div class="card"><div class="product-name">储值余额 ¥246.50</div><div class="section-subtitle">含赠送余额 ¥38.00</div></div><div class="card"><div class="section-header"><h3>充值方案区</h3><span>G44</span></div><div class="record-list">${PREPAID_PLANS.map((item) => `<div class="plan-card"><div class="space-between"><strong>充 ${item.amount}</strong>${statusTag(item.tag, 'primary')}</div><div class="price-row"><span class="price">到账 ¥${item.arrival}</span><span class="muted">赠送 ¥${item.gift}</span></div></div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>充值记录区</h3><span>最近记录</span></div><div class="record-list">${[{time:'2026-03-01 11:18',pay:'微信支付',arrival:235},{time:'2026-02-15 18:07',pay:'微信支付',arrival:115}].map((item) => `<div class="record-card">${infoRow(item.time, `${item.pay} / 到账 ¥${item.arrival}`)}</div>`).join('')}</div></div>`, `<div class="fixed-actions"><div><div style="font-weight:800;"></div><div class="muted" style="color:rgba(255,255,255,.72);"></div></div><button class="btn btn-primary"></button></div>`); }
function renderPassCard() { const list = PASS_DATA[state.passTab]; return secondaryPage('次卡页', '展示可购买、已购次卡和使用记录', `<div class="card"><div class="filter-row">${[['available','可购买次卡'],['mine','我的次卡'],['records','使用记录']].map(([key,label]) => `<div class="filter-chip ${state.passTab === key ? 'active' : ''}" data-action="set-pass-tab" data-value="${key}"><strong>${label}</strong><span>切换</span></div>`).join('')}</div></div><div class="record-list">${list.map((item) => `<div class="pass-card"><div class="space-between"><strong>${item.title}</strong>${item.cta ? statusTag(item.cta, 'primary') : ''}</div><div class="section-subtitle">${item.scope || item.desc}</div><div class="space-between" style="margin-top: 12px;"><span class="muted">${item.valid || item.time || ''}</span><span class="price">${item.remain !== undefined ? `${item.remain}/${item.total}` : ''}</span></div></div>`).join('')}</div>`); }
function renderMessageCenter() { const list = state.messages.filter((item) => state.messageType === 'all' ? true : item.type === state.messageType).filter((item) => state.debugState === 'empty' ? false : true); return secondaryPage('消息中心页', '订单消息、营销消息、系统通知', `<div class="card"><div class="filter-row">${[['all','全部'],['order','订单消息'],['marketing','营销消息'],['system','系统通知']].map(([key,label]) => `<div class="filter-chip ${state.messageType === key ? 'active' : ''}" data-action="set-message-type" data-value="${key}"><strong>${label}</strong><span>切换</span></div>`).join('')}</div></div>${list.length ? `<div class="message-list">${list.map((item) => `<div class="message-card" data-action="open-message" data-message-id="${item.id}"><div class="space-between"><strong>${item.title}</strong>${item.unread ? statusTag('未读', 'danger') : ''}</div><div class="section-subtitle" style="margin-top: 8px;">${item.summary}</div><div class="muted" style="margin-top: 10px;">${item.time}</div></div>`).join('')}</div>` : emptyState('暂无消息', '当前分类下没有消息通知。', '返回我的', 'go-me')}`); }
function renderHelpCenter() { return secondaryPage('帮助中心页', 'FAQ、专题帮助与联系客服', `<div class="faq-list">${[{id:'faq1',q:'如何下单?',a:'选择门店与场景后,在点餐页加购商品并进入结算即可。'},{id:'faq2',q:'如何使用优惠券?',a:'在结算页点击优惠券入口,系统会展示可用券与不可用原因。'},{id:'faq3',q:'如何申请退款?',a:'进入订单详情页,找到申请退款入口,选择原因并提交。'},{id:'faq4',q:'如何查看取餐码?',a:'自提订单支付成功后,可在支付成功页和订单详情页查看取餐码。'}].map((item) => `<div class="faq-card"><div class="space-between"><strong>${item.q}</strong><button class="mini-btn ghost" data-action="toggle-faq" data-faq-id="${item.id}">${state.openFaqs.includes(item.id) ? '' : ''}</button></div>${state.openFaqs.includes(item.id) ? `<div class="section-subtitle" style="margin-top: 10px;">${item.a}</div>` : ''}</div>`).join('')}</div><div class="card"><div class="section-header"><h3></h3><span> / / / </span></div><div class="topic-grid">${['','','','',''].map((item) => `<div class="topic-card"><div class="topic-title">${item}</div><div class="topic-subtitle"></div></div>`).join('')}</div></div><div class="card"><div class="space-between"><div><strong></strong><div class="section-subtitle">线 / </div></div><div class="row"><button class="mini-btn secondary">线</button><button class="mini-btn ghost">400-800-8899</button></div></div></div>`); }
function renderDineIn() { return secondaryPage('堂食扫码确认页', '扫码后确认门店与桌号,进入堂食点餐', `<div class="card"><div class="product-name">Haz Coffee 国贸店 · A12 桌</div><div class="section-subtitle">桌号有效,可继续点餐</div></div><div class="card"><div class="section-header"><h3>堂食规则说明区</h3><span>P18</span></div><div class="record-list">${['先付款后上餐','支持继续加菜','支持与同桌订单合单提醒','结账后可在订单页查看堂食状态'].map((item) => `<div class="summary-card">${item}</div>`).join('')}</div></div><div class="card"><div class="section-header"><h3>当前桌台状态区</h3><span>可选</span></div><div class="section-subtitle">当前桌已有 1 笔进行中订单,可继续加菜。</div></div>`, `<div class="fixed-actions"><div><div style="font-weight:800;">进入堂食点餐</div><div class="muted" style="color:rgba(255,255,255,.72);">扫码确认后默认切换到堂食场景</div></div><div class="row"><button class="btn btn-ghost" data-action="back">重新扫码</button><button class="btn btn-primary" data-action="enter-dine-in">进入堂食点餐</button></div></div>`); }
function renderRoute() {
switch (state.route) {
case 'pages/tab/home/index': return renderHome();
case 'pages/tab/menu/index': return renderMenu();
case 'pages/tab/orders/index': return renderOrders();
case 'pages/tab/me/index': return renderMe();
case 'pages/store/select/index': return renderStoreSelect();
case 'pages/address/list/index': return renderAddressList();
case 'pages/checkout/index': return renderCheckout();
case 'pages/payment/success/index': return renderPaymentSuccess();
case 'pages/order/detail/index': return renderOrderDetail();
case 'pages/order/refund/apply/index': return renderRefundApply();
case 'pages/order/refund/detail/index': return renderRefundDetail();
case 'pages/review/create/index': return renderReview();
case 'pages/coupon/center/index': return renderCouponCenter();
case 'pages/activity/seckill/index': return renderSeckill();
case 'pages/activity/flash-sale/index': return renderFlashSale();
case 'pages/member/center/index': return renderMemberCenter();
case 'pages/points/mall/index': return renderPointsMall();
case 'pages/prepaid/index': return renderPrepaid();
case 'pages/pass-card/index': return renderPassCard();
case 'pages/message/center/index': return renderMessageCenter();
case 'pages/help/center/index': return renderHelpCenter();
case 'pages/dine-in/confirm/index': return renderDineIn();
default: return renderHome();
}
}
function renderSidebar() {
document.getElementById('phaseList').innerHTML = PHASES.map((item, index) => `<div class="phase-item"><div class="phase-index">${index + 1}</div><div><div class="phase-name">${item.title}</div><div class="phase-desc">${item.desc}</div></div></div>`).join('');
document.getElementById('routeList').innerHTML = ROUTES.map((item) => `<button class="route-button ${state.route === item.path ? 'active' : ''}" data-action="navigate" data-route="${item.path}"><div class="route-meta"><div class="route-title">${item.code} · ${item.title}</div><div class="route-path">${item.path}</div></div><span class="chip">${item.phase}</span></button>`).join('');
document.getElementById('controlList').innerHTML = `${SCENES.map((item) => `<button class="control-button" data-action="set-scene" data-scene="${item.key}"><span>${item.label}</span><strong>${state.currentScene === item.key ? '当前' : item.tip}</strong></button>`).join('')}<button class="control-button" data-action="toggle-login"><span>登录态</span><strong>${state.memberLoggedIn ? '已登录' : '未登录'}</strong></button><button class="control-button" data-action="set-debug" data-value="default"><span>页面状态</span><strong>${state.debugState === 'default' ? '默认态' : '切换默认'}</strong></button><button class="control-button" data-action="set-debug" data-value="empty"><span>页面状态</span><strong>${state.debugState === 'empty' ? '空态' : '切换空态'}</strong></button><button class="control-button" data-action="set-debug" data-value="error"><span>页面状态</span><strong>${state.debugState === 'error' ? '异常态' : '切换异常态'}</strong></button>`;
document.getElementById('stateSummary').innerHTML = `<div class="summary-card">${infoRow('当前页面', `${routeMeta().code} · ${routeMeta().title}`)}${infoRow('当前场景', currentSceneInfo().label)}${infoRow('当前门店', currentStore().shortName)}${infoRow('购物车件数', `${cartSummary().count}`)}${infoRow('实付预估', currency(checkoutData().payable))}</div>`;
}
function renderToolbar() {
document.getElementById('toolbarTitle').textContent = `${routeMeta().code} · ${routeMeta().title}`;
document.getElementById('toolbarSubtitle').textContent = `${routeMeta().phase} 当前场景:${currentSceneInfo().label} 当前门店:${currentStore().name}`;
document.getElementById('toolbarActions').innerHTML = `<button class="pill-button ${state.debugState === 'default' ? 'active' : ''}" data-action="set-debug" data-value="default">默认态</button><button class="pill-button ${state.debugState === 'empty' ? 'active' : ''}" data-action="set-debug" data-value="empty">空态</button><button class="pill-button ${state.debugState === 'error' ? 'active' : ''}" data-action="set-debug" data-value="error">异常态</button>`;
}
function renderApp() {
renderSidebar();
renderToolbar();
document.getElementById('phoneScreen').innerHTML = renderRoute();
const extraStyle = document.getElementById('extraStyle') || document.createElement('style');
extraStyle.id = 'extraStyle';
extraStyle.textContent = `.drawer-mask{position:absolute;inset:0;background:rgba(15,23,42,.42);z-index:40}.drawer{position:absolute;left:0;right:0;bottom:0;max-height:82%;overflow:auto;background:#fff;border-radius:28px 28px 0 0;padding:16px 16px 88px;z-index:50;box-shadow:0 -20px 60px rgba(15,23,42,.16)}.text-input,.text-area{width:100%;border:1px solid #e5e7eb;background:#f8fafc;border-radius:16px;padding:0 14px;color:#111827}.text-input{height:46px}.text-area{min-height:110px;padding-top:12px;padding-bottom:12px}.fixed-actions{display:flex;align-items:center;justify-content:space-between;gap:12px}.summary-card strong{font-size:14px}.record-card,.summary-card,.benefit-card{line-height:1.6}`;
document.head.appendChild(extraStyle);
location.hash = `${state.route}${state.params.orderId ? `?orderId=${state.params.orderId}` : ''}`;
}
document.addEventListener('click', (event) => {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
if (action === 'navigate') navigate(target.dataset.route, target.dataset.orderId ? { orderId: target.dataset.orderId } : {});
if (action === 'switch-tab') switchTab(target.dataset.route);
if (action === 'back') goBack();
if (action === 'goto-messages') navigate('pages/message/center/index');
if (action === 'set-scene') { state.currentScene = target.dataset.scene; renderApp(); }
if (action === 'set-category') { state.activeCategory = target.dataset.category; renderApp(); }
if (action === 'quick-add') addQuickCart(target.dataset.productId);
if (action === 'open-product') openProduct(target.dataset.productId, !isTabRoute(state.route));
if (action === 'close-overlays') { state.showProductDrawer = false; state.showCartDrawer = false; renderApp(); }
if (action === 'open-cart') { state.showCartDrawer = true; state.showProductDrawer = false; renderApp(); }
if (action === 'change-cart-qty') { const line = state.cart.find((item) => item.key === target.dataset.key); line.qty += Number(target.dataset.step); state.cart = state.cart.filter((item) => item.qty > 0); renderApp(); }
if (action === 'clear-cart') { state.cart = []; renderApp(); }
if (action === 'set-product-variant') { state.productVariant = target.dataset.variant; renderApp(); }
if (action === 'toggle-addon') { const addon = target.dataset.addon; state.productAddons = state.productAddons.includes(addon) ? state.productAddons.filter((item) => item !== addon) : state.productAddons.concat(addon); renderApp(); }
if (action === 'change-product-qty') { state.productQty = Math.max(1, state.productQty + Number(target.dataset.step)); renderApp(); }
if (action === 'confirm-add-cart') addCurrentSelectionToCart();
if (action === 'go-checkout') navigate('pages/checkout/index');
if (action === 'cycle-coupon') { const ids = [null].concat(state.coupons.filter((item) => item.claimed && !item.expired).map((item) => item.id)); const currentIndex = Math.max(ids.indexOf(state.checkout.couponId), 0); state.checkout.couponId = ids[(currentIndex + 1) % ids.length]; renderApp(); }
if (action === 'toggle-checkout-flag') { state.checkout[target.dataset.flag] = !state.checkout[target.dataset.flag]; renderApp(); }
if (action === 'set-utensils') { state.checkout.utensils = target.dataset.value; renderApp(); }
if (action === 'submit-order') { const check = checkoutData(); if (check.issues.length || !state.cart.length) return; const newOrder = { id: `order_${Date.now()}`, number: `TO${Date.now()}`, storeId: state.currentStoreId, scene: state.currentScene, status: 'paid_wait_accept', createdAt: '2026-03-06 18:08', paidAt: '2026-03-06 18:08', payMethod: state.checkout.useBalance ? '储值余额 + 微信支付' : '微信支付', items: clone(state.cart), amount: { subtotal: check.summary.subtotal, packing: check.summary.packing, utensil: check.summary.utensil, delivery: check.summary.delivery, discount: check.couponDiscount + check.pointsDiscount + check.balanceDiscount + check.passCardDiscount, payable: check.payable }, address: check.address ? check.address.detail : '', pickupCode: 'A928', pickupSlot: state.checkout.pickupSlot, tableNo: 'A12', refund: null, timeline: ['支付成功 18:08','商家接单','履约中','订单完成'] }; state.orders.unshift(newOrder); state.lastOrderId = newOrder.id; state.selectedOrderId = newOrder.id; state.cart = []; navigate('pages/payment/success/index'); }
if (action === 'set-order-filter') { state.orderFilter = target.dataset.value; renderApp(); }
if (action === 'set-order-scene') { state.orderScene = target.dataset.value; renderApp(); }
if (action === 'open-order-detail') { state.selectedOrderId = target.dataset.orderId; navigate('pages/order/detail/index', { orderId: target.dataset.orderId }); }
if (action === 'continue-pay') { const order = state.orders.find((item) => item.id === target.dataset.orderId); order.status = 'paid_wait_accept'; state.lastOrderId = order.id; navigate('pages/payment/success/index'); }
if (action === 'cancel-order') { const order = state.orders.find((item) => item.id === target.dataset.orderId); order.status = 'closed'; renderApp(); }
if (action === 'urge-order') alert('已模拟催单提醒');
if (action === 'reorder') { const order = state.orders.find((item) => item.id === target.dataset.orderId); state.cart = order.items.map((item) => ({ ...item, key: makeLineKey() })); state.currentScene = order.scene; state.currentStoreId = order.storeId; switchTab('pages/tab/menu/index'); }
if (action === 'open-refund-apply') { state.selectedOrderId = target.dataset.orderId; navigate('pages/order/refund/apply/index', { orderId: target.dataset.orderId }); }
if (action === 'set-refund-reason') { state.refundDraft.reason = target.dataset.value; renderApp(); }
if (action === 'submit-refund') { const order = currentOrder(); order.status = 'refunding'; order.refund = { status: 'processing', amount: order.amount.payable / 2, reason: state.refundDraft.reason, remark: state.refundDraft.remark, rejectReason: '', timeline: ['提交退款申请 18:35','商家处理中','退款完成'] }; navigate('pages/order/refund/detail/index', { orderId: order.id }); }
if (action === 'open-refund-detail') { state.selectedOrderId = target.dataset.orderId; navigate('pages/order/refund/detail/index', { orderId: target.dataset.orderId }); }
if (action === 'open-review') { state.selectedOrderId = target.dataset.orderId; navigate('pages/review/create/index', { orderId: target.dataset.orderId }); }
if (action === 'set-rating') { state.reviewDraft.rating = Number(target.dataset.value); renderApp(); }
if (action === 'toggle-review-tag') { const value = target.dataset.value; state.reviewDraft.tags = state.reviewDraft.tags.includes(value) ? state.reviewDraft.tags.filter((item) => item !== value) : state.reviewDraft.tags.concat(value); renderApp(); }
if (action === 'toggle-anonymous') { state.reviewDraft.anonymous = !state.reviewDraft.anonymous; renderApp(); }
if (action === 'submit-review') { const order = currentOrder(); order.status = 'finished'; navigate('pages/order/detail/index', { orderId: order.id }); }
if (action === 'set-coupon-filter') { state.couponFilter = target.dataset.value; renderApp(); }
if (action === 'claim-coupon') { const coupon = state.coupons.find((item) => item.id === target.dataset.couponId); coupon.claimed = true; renderApp(); }
if (action === 'set-seckill-tab') { state.seckillTab = target.dataset.value; renderApp(); }
if (action === 'set-flash-tab') { state.flashTab = target.dataset.value; renderApp(); }
if (action === 'set-points-tab') { state.pointsTab = target.dataset.value; renderApp(); }
if (action === 'set-pass-tab') { state.passTab = target.dataset.value; renderApp(); }
if (action === 'set-message-type') { state.messageType = target.dataset.value; renderApp(); }
if (action === 'open-message') { const message = state.messages.find((item) => item.id === target.dataset.messageId); message.unread = false; navigate(message.target.route, message.target.params || {}); }
if (action === 'toggle-faq') { const id = target.dataset.faqId; state.openFaqs = state.openFaqs.includes(id) ? state.openFaqs.filter((item) => item !== id) : state.openFaqs.concat(id); renderApp(); }
if (action === 'open-store-select') navigate('pages/store/select/index');
if (action === 'choose-store') { state.currentStoreId = target.dataset.storeId; goBack(); }
if (action === 'set-default-address') { state.addresses.forEach((item) => item.default = item.id === target.dataset.addressId); renderApp(); }
if (action === 'select-address') { state.checkout.addressId = target.dataset.addressId; goBack(); }
if (action === 'delete-address') { state.addresses = state.addresses.filter((item) => item.id !== target.dataset.addressId); renderApp(); }
if (action === 'add-address') { state.addresses.unshift({ id: makeLineKey(), name: '新联系人', phone: '13600000000', tag: '新地址', detail: '朝阳区新建示例地址 100 号', default: false, outOfRange: false, tip: '配送预计 35 分钟' }); renderApp(); }
if (action === 'jump-orders') { state.orderFilter = target.dataset.filter; switchTab('pages/tab/orders/index'); }
if (action === 'go-home') switchTab('pages/tab/home/index');
if (action === 'go-me') switchTab('pages/tab/me/index');
if (action === 'go-menu') switchTab('pages/tab/menu/index');
if (action === 'toggle-login') { state.memberLoggedIn = !state.memberLoggedIn; renderApp(); }
if (action === 'set-debug') { state.debugState = target.dataset.value; renderApp(); }
if (action === 'enter-dine-in') { state.currentScene = 'dine_in'; switchTab('pages/tab/menu/index'); }
});
document.addEventListener('input', (event) => {
if (event.target.dataset.model === 'menuSearch') { state.menuSearch = event.target.value; renderApp(); }
if (event.target.dataset.model === 'checkoutRemark') state.checkout.remark = event.target.value;
if (event.target.dataset.model === 'refundRemark') state.refundDraft.remark = event.target.value;
if (event.target.dataset.model === 'reviewComment') state.reviewDraft.comment = event.target.value;
});
window.addEventListener('hashchange', () => {
const raw = location.hash.replace(/^#/, '');
if (!raw) return;
const [route, query] = raw.split('?');
if (routeMap[route]) {
state.route = route;
state.params = {};
if (query) new URLSearchParams(query).forEach((value, key) => state.params[key] = value);
renderApp();
}
});
renderApp();
</script>
</body>
</html>