1512 lines
109 KiB
HTML
1512 lines
109 KiB
HTML
<!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 减 ¥8,17: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>
|
||
|