Files
web_template/web/dist/food3/index.html

1784 lines
88 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#2D9A6C" />
<title>绿野鲜送 · 新鲜好味 绿色送达</title>
<style>
/* ============================================================
绿野鲜送 — 清新绿色风外卖平台
主题:健康轻食 / 文艺自然 / 大圆角 / 柔和阴影
============================================================ */
:root {
/* 主色调 */
--green: #2D9A6C;
--green-deep: #1B5E20;
--green-soft: #E8F5E9;
--green-mist: #F1F8F4;
--green-100: #C8E6C9;
--green-200: #A5D6A7;
--green-300: #81C784;
--green-400: #4CAF50;
--green-600: #258059;
--warm-white: #FEFDF9;
/* 文字 */
--ink: #1B5E20;
--ink-2: #355E3B;
--muted: #6B8170;
--line: #E3EFE6;
/* 表面 */
--bg: #F5FBF6;
--card: #FFFFFF;
/* 效果 */
--shadow-sm: 0 4px 16px rgba(45, 154, 108, 0.08);
--shadow: 0 10px 34px rgba(45, 154, 108, 0.12);
--shadow-lg: 0 20px 50px rgba(27, 94, 32, 0.18);
--radius-xl: 28px;
--radius: 20px;
--radius-md: 16px;
--radius-sm: 12px;
--grad-green: linear-gradient(135deg, #2D9A6C 0%, #4CAF50 55%, #66BB6A 100%);
--grad-soft: linear-gradient(160deg, #F1F8F4 0%, #E8F5E9 100%);
--grad-hero: linear-gradient(160deg, #E8F5E9 0%, #F1F8F4 45%, #FEFDF9 100%);
--font: "Nunito", "Quicksand", -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
padding-bottom: 96px;
overflow-x: hidden;
}
img { max-width: 100%; display: block; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
input { font-family: inherit; outline: none; }
a { color: inherit; text-decoration: none; }
::selection { background: var(--green-100); color: var(--green-deep); }
/* 滚动条 */
::-webkit-scrollbar { width: 10px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--green-200); border-radius: 10px; border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--green-300); }
.container { width: 100%; max-width: 1240px; margin: 0 auto; padding: 0 20px; }
/* ============ 顶部导航 ============ */
.navbar {
position: sticky; top: 0; z-index: 100;
background: rgba(232, 245, 233, 0.82);
backdrop-filter: saturate(180%) blur(18px);
-webkit-backdrop-filter: saturate(180%) blur(18px);
border-bottom: 1px solid rgba(165, 214, 167, 0.45);
}
.nav-inner {
display: flex; align-items: center; gap: 20px;
height: 70px;
}
.logo { display: flex; align-items: center; gap: 10px; flex-shrink: 0; cursor: pointer; }
.logo-mark {
width: 42px; height: 42px; border-radius: 14px;
background: var(--grad-green);
display: grid; place-items: center;
box-shadow: 0 6px 16px rgba(45, 154, 108, 0.35);
}
.logo-mark svg { width: 24px; height: 24px; }
.logo-text {
font-size: 21px; font-weight: 800; letter-spacing: 0.5px;
color: var(--green-deep);
}
.logo-text span { color: var(--green); }
.search-wrap { flex: 1; max-width: 520px; position: relative; }
.search-box {
width: 100%; height: 46px;
background: #fff;
border: 1.5px solid var(--line);
border-radius: 24px;
padding: 0 52px 0 22px;
font-size: 15px; color: var(--ink);
transition: all 0.25s;
box-shadow: var(--shadow-sm);
}
.search-box::placeholder { color: #9CB3A3; }
.search-box:focus {
border-color: var(--green);
box-shadow: 0 0 0 4px rgba(45, 154, 108, 0.14);
}
.search-btn {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
width: 36px; height: 36px; border-radius: 50%;
background: var(--grad-green); color: #fff;
display: grid; place-items: center;
box-shadow: 0 4px 12px rgba(45, 154, 108, 0.35);
transition: transform 0.2s;
}
.search-btn:hover { transform: translateY(-50%) scale(1.06); }
.nav-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.cart-btn {
position: relative;
width: 46px; height: 46px; border-radius: 16px;
background: #fff; border: 1.5px solid var(--line);
display: grid; place-items: center;
color: var(--green-deep);
transition: all 0.25s;
}
.cart-btn:hover { border-color: var(--green); color: var(--green); transform: translateY(-2px); }
.cart-badge {
position: absolute; top: -6px; right: -6px;
min-width: 22px; height: 22px; padding: 0 6px;
background: #FF7043; color: #fff;
border-radius: 11px; font-size: 12px; font-weight: 800;
display: grid; place-items: center;
border: 2px solid var(--warm-white);
transform: scale(0); transition: transform 0.25s cubic-bezier(.34,1.56,.64,1);
}
.cart-badge.show { transform: scale(1); }
.nav-cta {
height: 46px; padding: 0 22px; border-radius: 16px;
background: var(--grad-green); color: #fff;
font-weight: 700; font-size: 15px;
display: none; align-items: center; gap: 6px;
box-shadow: 0 8px 20px rgba(45, 154, 108, 0.32);
transition: transform 0.2s;
}
.nav-cta:hover { transform: translateY(-2px); }
@media (min-width: 900px) { .nav-cta { display: inline-flex; } }
/* ============ Hero ============ */
.hero {
position: relative; overflow: hidden;
background: var(--grad-hero);
padding: 48px 0 56px;
}
.hero-leaf { position: absolute; pointer-events: none; opacity: 0.5; }
.hero-leaf svg { width: 100%; height: 100%; }
.leaf-1 { top: -30px; left: -40px; width: 220px; transform: rotate(-25deg); opacity: 0.28; }
.leaf-2 { top: 30px; right: -50px; width: 180px; transform: rotate(40deg); opacity: 0.22; }
.leaf-3 { bottom: -60px; left: 30%; width: 260px; transform: rotate(15deg); opacity: 0.15; }
.hero-card {
position: relative;
background: #fff;
border-radius: var(--radius-xl);
padding: 52px 48px;
box-shadow: var(--shadow);
overflow: hidden;
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 40px;
align-items: center;
}
.hero-card::before {
content: ""; position: absolute; top: -80px; right: -80px;
width: 260px; height: 260px; border-radius: 50%;
background: radial-gradient(circle, rgba(102,187,106,0.18), transparent 70%);
}
.hero-tag {
display: inline-flex; align-items: center; gap: 6px;
background: var(--green-soft); color: var(--green-deep);
padding: 7px 16px; border-radius: 20px;
font-size: 13px; font-weight: 700;
margin-bottom: 20px;
}
.hero-tag .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 1.8s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
.hero-title {
font-size: clamp(30px, 4.4vw, 46px);
font-weight: 900; line-height: 1.15;
color: var(--green-deep); letter-spacing: 0.5px;
margin-bottom: 16px;
}
.hero-title em { font-style: normal; color: var(--green); position: relative; }
.hero-title em::after {
content: ""; position: absolute; left: 0; bottom: 4px; width: 100%; height: 10px;
background: rgba(165, 214, 167, 0.5); border-radius: 10px; z-index: -1;
}
.hero-desc { font-size: 16px; color: var(--muted); margin-bottom: 28px; max-width: 460px; }
.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; }
.btn-primary {
display: inline-flex; align-items: center; gap: 8px;
height: 52px; padding: 0 30px; border-radius: 18px;
background: var(--grad-green); color: #fff;
font-weight: 800; font-size: 16px;
box-shadow: 0 12px 26px rgba(45, 154, 108, 0.35);
transition: transform 0.22s, box-shadow 0.22s;
}
.btn-primary:hover { transform: translateY(-3px); box-shadow: 0 18px 34px rgba(45, 154, 108, 0.42); }
.btn-ghost {
display: inline-flex; align-items: center; gap: 8px;
height: 52px; padding: 0 26px; border-radius: 18px;
background: #fff; color: var(--green-deep);
font-weight: 700; font-size: 16px;
border: 1.5px solid var(--green-100);
transition: all 0.22s;
}
.btn-ghost:hover { border-color: var(--green); background: var(--green-mist); }
.hero-stats { display: flex; gap: 30px; margin-top: 32px; }
.hero-stat .num { font-size: 26px; font-weight: 900; color: var(--green); }
.hero-stat .lbl { font-size: 13px; color: var(--muted); }
.hero-visual { position: relative; height: 320px; display: grid; place-items: center; }
.hero-bowl {
width: 280px; height: 280px; border-radius: 50%;
background: var(--grad-soft);
display: grid; place-items: center;
box-shadow: inset 0 -20px 40px rgba(45,154,108,0.1), 0 20px 50px rgba(45,154,108,0.18);
position: relative;
animation: float 5s ease-in-out infinite;
}
@keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-14px); } }
.hero-bowl svg { width: 170px; height: 170px; }
.hero-float {
position: absolute; background: #fff; border-radius: 16px;
padding: 10px 14px; box-shadow: var(--shadow);
display: flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 700; color: var(--green-deep);
}
.hero-float .ic { width: 28px; height: 28px; border-radius: 9px; background: var(--green-soft); display: grid; place-items: center; color: var(--green); }
.f1 { top: 10px; left: 0; animation: float 4s ease-in-out infinite; }
.f2 { bottom: 30px; right: -10px; animation: float 4.5s ease-in-out 0.5s infinite; }
.f3 { top: 90px; right: 10px; animation: float 5.5s ease-in-out 1s infinite; }
@media (max-width: 880px) {
.hero-card { grid-template-columns: 1fr; padding: 36px 26px; gap: 24px; }
.hero-visual { height: 240px; order: -1; }
.hero-bowl { width: 210px; height: 210px; }
.hero-bowl svg { width: 130px; height: 130px; }
.hero-stats { gap: 22px; }
}
/* ============ 通用区块标题 ============ */
.section { padding: 56px 0; }
.section-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 30px; gap: 16px; flex-wrap: wrap; }
.section-title { display: flex; flex-direction: column; gap: 4px; }
.section-title .kicker { font-size: 13px; font-weight: 700; color: var(--green); letter-spacing: 1.5px; text-transform: uppercase; }
.section-title h2 { font-size: clamp(22px, 3vw, 30px); font-weight: 900; color: var(--green-deep); display: flex; align-items: center; gap: 12px; }
.section-title h2::before { content: ""; width: 6px; height: 26px; border-radius: 4px; background: var(--grad-green); }
.section-title p { font-size: 14px; color: var(--muted); margin-top: 2px; }
.link-more { font-size: 14px; font-weight: 700; color: var(--green); display: inline-flex; align-items: center; gap: 4px; transition: gap 0.2s; }
.link-more:hover { gap: 8px; }
/* ============ 分类网格 ============ */
.cat-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
}
.cat-card {
background: var(--card);
border-radius: var(--radius-md);
padding: 22px 12px 18px;
text-align: center;
cursor: pointer;
border: 1.5px solid transparent;
box-shadow: var(--shadow-sm);
transition: all 0.28s cubic-bezier(.34,1.2,.64,1);
position: relative; overflow: hidden;
}
.cat-card::after {
content: ""; position: absolute; inset: 0;
background: var(--grad-soft); opacity: 0; transition: opacity 0.28s; z-index: 0;
}
.cat-card > * { position: relative; z-index: 1; }
.cat-card:hover { transform: translateY(-8px); box-shadow: var(--shadow); border-color: var(--green-100); }
.cat-card:hover::after { opacity: 1; }
.cat-card.active { background: var(--grad-green); border-color: transparent; box-shadow: 0 14px 30px rgba(45,154,108,0.35); }
.cat-card.active .cat-ic { background: rgba(255,255,255,0.25); color: #fff; }
.cat-card.active .cat-name { color: #fff; }
.cat-card.active .cat-count { color: rgba(255,255,255,0.85); }
.cat-ic {
width: 60px; height: 60px; margin: 0 auto 12px;
border-radius: 50%;
background: var(--green-soft); color: var(--green);
display: grid; place-items: center;
transition: all 0.28s;
}
.cat-ic svg { width: 32px; height: 32px; }
.cat-card:hover .cat-ic { transform: rotate(-8deg) scale(1.06); }
.cat-name { font-size: 15px; font-weight: 800; color: var(--ink); }
.cat-count { font-size: 12px; color: var(--muted); margin-top: 2px; }
@media (max-width: 980px) { .cat-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 520px) { .cat-grid { grid-template-columns: repeat(2, 1fr); } }
/* ============ 推荐餐厅 横滑 ============ */
.featured-scroller {
display: flex; gap: 18px;
overflow-x: auto; padding: 6px 4px 18px;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.featured-scroller::-webkit-scrollbar { height: 6px; }
.feat-card {
flex: 0 0 300px;
scroll-snap-align: start;
background: var(--card);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: all 0.28s;
cursor: pointer;
border: 1.5px solid transparent;
}
.feat-card:hover { transform: translateY(-6px); box-shadow: var(--shadow); border-color: var(--green-100); }
.feat-cover { height: 140px; position: relative; display: grid; place-items: center; }
.feat-cover .grad { position: absolute; inset: 0; }
.feat-cover .ic { position: relative; width: 64px; height: 64px; border-radius: 20px; background: rgba(255,255,255,0.85); display: grid; place-items: center; box-shadow: var(--shadow-sm); }
.feat-cover .ic svg { width: 38px; height: 38px; }
.feat-rank {
position: absolute; top: 12px; left: 12px;
width: 30px; height: 30px; border-radius: 10px;
background: rgba(27,94,32,0.78); color: #fff;
font-weight: 900; font-size: 14px;
display: grid; place-items: center; backdrop-filter: blur(6px);
}
.feat-body { padding: 16px 18px 20px; }
.feat-name { font-size: 17px; font-weight: 800; color: var(--ink); margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.feat-desc { font-size: 13px; color: var(--muted); margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.feat-meta { display: flex; align-items: center; justify-content: space-between; font-size: 13px; }
.feat-rating { display: flex; align-items: center; gap: 4px; font-weight: 800; color: var(--green); }
.feat-sales { color: var(--muted); font-size: 12px; }
.feat-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.feat-tags .t { font-size: 11px; padding: 3px 9px; border-radius: 10px; background: var(--green-mist); color: var(--green-600); font-weight: 700; }
/* ============ 工具栏 筛选/排序 ============ */
.toolbar {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
margin-bottom: 26px;
}
.sort-group {
display: flex; gap: 8px; background: var(--card);
padding: 6px; border-radius: 16px; box-shadow: var(--shadow-sm);
border: 1.5px solid var(--line);
}
.sort-btn {
padding: 9px 18px; border-radius: 11px;
font-size: 14px; font-weight: 700; color: var(--muted);
transition: all 0.22s;
display: inline-flex; align-items: center; gap: 6px;
}
.sort-btn:hover { color: var(--green); }
.sort-btn.active { background: var(--grad-green); color: #fff; box-shadow: 0 6px 14px rgba(45,154,108,0.3); }
.result-info { font-size: 14px; color: var(--muted); margin-left: auto; }
.result-info b { color: var(--green-deep); }
.chip-clear {
display: none; align-items: center; gap: 6px;
padding: 8px 14px; border-radius: 12px;
background: #fff; border: 1.5px solid var(--green-100);
font-size: 13px; font-weight: 700; color: var(--green);
}
.chip-clear.show { display: inline-flex; }
/* ============ 餐厅列表 ============ */
.rest-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
gap: 22px;
}
.rest-card {
background: var(--card);
border-radius: var(--radius);
padding: 18px;
box-shadow: var(--shadow-sm);
border: 1.5px solid transparent;
cursor: pointer;
transition: all 0.28s cubic-bezier(.34,1.2,.64,1);
display: flex; gap: 16px;
position: relative; overflow: hidden;
}
.rest-card::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
background: var(--grad-green); opacity: 0; transition: opacity 0.28s;
}
.rest-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); border-color: var(--green-100); }
.rest-card:hover::before { opacity: 1; }
.rest-logo {
flex-shrink: 0;
width: 96px; height: 96px; border-radius: 20px;
display: grid; place-items: center;
position: relative; overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.5);
}
.rest-logo .grad { position: absolute; inset: 0; }
.rest-logo svg { position: relative; width: 50px; height: 50px; color: #fff; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.12)); }
.rest-info { flex: 1; min-width: 0; }
.rest-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.rest-name { font-size: 18px; font-weight: 800; color: var(--ink); line-height: 1.3; }
.rest-status { flex-shrink: 0; font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 8px; }
.rest-status.open { background: var(--green-soft); color: var(--green-600); }
.rest-status.closed { background: #FBE9E7; color: #C62828; }
.rest-cat { font-size: 12px; color: var(--green); font-weight: 700; margin: 3px 0 8px; }
.rest-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.rest-tags .t { font-size: 11px; padding: 2px 9px; border-radius: 10px; background: var(--green-mist); color: var(--green-600); font-weight: 700; }
.rest-meta { display: flex; align-items: center; gap: 12px; font-size: 13px; flex-wrap: wrap; }
.rest-rating { display: flex; align-items: center; gap: 3px; font-weight: 800; color: var(--green); }
.rest-sales { color: var(--muted); font-size: 12px; }
.rest-delivery { display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--muted); margin-top: 8px; padding-top: 10px; border-top: 1px dashed var(--line); }
.rest-delivery .sep { width: 3px; height: 3px; border-radius: 50%; background: var(--green-200); }
.rest-delivery b { color: var(--ink-2); font-weight: 700; }
/* ============ 骨架屏 ============ */
.skeleton { position: relative; overflow: hidden; background: #EAF3EC; }
.skeleton::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.65), transparent);
transform: translateX(-100%);
animation: shimmer 1.4s infinite;
}
@keyframes shimmer { 100% { transform: translateX(100%); } }
.sk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px,1fr)); gap: 22px; }
.sk-card { background: #fff; border-radius: var(--radius); padding: 18px; display: flex; gap: 16px; box-shadow: var(--shadow-sm); }
.sk-logo { width: 96px; height: 96px; border-radius: 20px; flex-shrink: 0; }
.sk-lines { flex: 1; display: flex; flex-direction: column; gap: 10px; padding-top: 4px; }
.sk-line { height: 14px; border-radius: 7px; }
.sk-line.w70 { width: 70%; } .sk-line.w50 { width: 50%; } .sk-line.w90 { width: 90%; }
.sk-scroller { display: flex; gap: 18px; overflow: hidden; }
.sk-feat { flex: 0 0 300px; height: 260px; border-radius: var(--radius); }
.sk-cat-grid { display: grid; grid-template-columns: repeat(6,1fr); gap: 16px; }
.sk-cat { height: 130px; border-radius: var(--radius-md); }
@media (max-width: 980px) { .sk-cat-grid { grid-template-columns: repeat(3,1fr); } }
@media (max-width: 520px) { .sk-cat-grid { grid-template-columns: repeat(2,1fr); } }
/* ============ 空状态 / 错误 ============ */
.state-box { text-align: center; padding: 70px 20px; }
.state-box .ic { width: 88px; height: 88px; margin: 0 auto 18px; border-radius: 28px; background: var(--green-mist); display: grid; place-items: center; color: var(--green-300); }
.state-box .ic svg { width: 48px; height: 48px; }
.state-box h3 { font-size: 19px; color: var(--ink); font-weight: 800; margin-bottom: 8px; }
.state-box p { color: var(--muted); font-size: 14px; }
.state-box .retry { margin-top: 18px; }
/* ============ 加载更多 ============ */
.load-more-wrap { text-align: center; margin-top: 36px; }
.load-more {
display: inline-flex; align-items: center; gap: 8px;
height: 48px; padding: 0 30px; border-radius: 16px;
background: #fff; border: 1.5px solid var(--green-100);
color: var(--green-deep); font-weight: 700; font-size: 15px;
transition: all 0.22s;
}
.load-more:hover:not(:disabled) { border-color: var(--green); background: var(--green-mist); transform: translateY(-2px); }
.load-more:disabled { opacity: 0.6; cursor: default; }
.spinner { width: 16px; height: 16px; border: 2px solid var(--green-100); border-top-color: var(--green); border-radius: 50%; animation: spin 0.7s linear infinite; }
@keyframes spin { 100% { transform: rotate(360deg); } }
/* ============ 餐厅详情 模态 ============ */
.modal-mask {
position: fixed; inset: 0; z-index: 200;
background: rgba(27, 94, 32, 0.32);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
display: none; align-items: flex-start; justify-content: center;
padding: 30px 16px; overflow-y: auto;
opacity: 0; transition: opacity 0.25s;
}
.modal-mask.show { display: flex; opacity: 1; }
.modal {
width: 100%; max-width: 940px;
background: var(--warm-white);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
overflow: hidden;
transform: translateY(24px) scale(0.98); opacity: 0;
transition: transform 0.32s cubic-bezier(.34,1.2,.64,1), opacity 0.32s;
}
.modal-mask.show .modal { transform: translateY(0) scale(1); opacity: 1; }
.modal-hero {
position: relative; padding: 28px 30px 22px;
background: var(--grad-hero);
border-bottom: 1px solid var(--line);
}
.modal-hero::before {
content: ""; position: absolute; top: -60px; right: -40px; width: 200px; height: 200px;
border-radius: 50%; background: radial-gradient(circle, rgba(102,187,106,0.18), transparent 70%);
}
.modal-close {
position: absolute; top: 18px; right: 18px; z-index: 5;
width: 40px; height: 40px; border-radius: 13px;
background: rgba(255,255,255,0.85); color: var(--green-deep);
display: grid; place-items: center;
box-shadow: var(--shadow-sm);
transition: all 0.2s;
}
.modal-close:hover { background: #fff; transform: rotate(90deg); color: #C62828; }
.modal-rest { display: flex; gap: 18px; align-items: center; }
.modal-logo { width: 84px; height: 84px; border-radius: 22px; display: grid; place-items: center; position: relative; overflow: hidden; flex-shrink: 0; box-shadow: var(--shadow); }
.modal-logo .grad { position: absolute; inset: 0; }
.modal-logo svg { position: relative; width: 46px; height: 46px; color: #fff; }
.modal-rest-info { flex: 1; min-width: 0; }
.modal-rest-name { font-size: 24px; font-weight: 900; color: var(--green-deep); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.modal-rest-cat { font-size: 13px; color: var(--green); font-weight: 700; margin: 4px 0 8px; }
.modal-rest-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.modal-rest-tags .t { font-size: 11px; padding: 3px 10px; border-radius: 11px; background: #fff; color: var(--green-600); font-weight: 700; box-shadow: var(--shadow-sm); }
.modal-rest-stats { display: flex; gap: 22px; flex-wrap: wrap; font-size: 13px; }
.modal-rest-stats .item { display: flex; align-items: center; gap: 5px; color: var(--muted); }
.modal-rest-stats .item b { color: var(--green); font-weight: 800; font-size: 15px; }
.modal-notice {
margin-top: 14px; padding: 10px 14px; border-radius: 12px;
background: rgba(255,255,255,0.7); font-size: 13px; color: var(--ink-2);
display: flex; align-items: flex-start; gap: 8px;
border: 1px solid var(--green-100);
}
.modal-notice svg { width: 16px; height: 16px; color: var(--green); flex-shrink: 0; margin-top: 2px; }
.menu-body { display: flex; max-height: 60vh; min-height: 420px; }
.menu-left {
flex: 0 0 150px;
background: var(--green-mist);
padding: 14px 10px;
overflow-y: auto;
border-right: 1px solid var(--line);
}
.menu-cat {
padding: 12px 14px; border-radius: 12px;
font-size: 14px; font-weight: 700; color: var(--muted);
cursor: pointer; transition: all 0.22s;
margin-bottom: 4px; text-align: center;
display: flex; align-items: center; justify-content: center; gap: 6px;
}
.menu-cat:hover { color: var(--green); }
.menu-cat.active { background: #fff; color: var(--green-deep); box-shadow: var(--shadow-sm); }
.menu-cat .cnt { font-size: 11px; opacity: 0.6; font-weight: 600; }
.menu-right { flex: 1; overflow-y: auto; padding: 8px 18px 24px; scroll-behavior: smooth; }
.menu-section { padding-top: 16px; }
.menu-section-title { font-size: 16px; font-weight: 800; color: var(--green-deep); margin-bottom: 12px; display: flex; align-items: center; gap: 8px; position: sticky; top: 0; background: var(--warm-white); padding: 8px 0; z-index: 2; }
.menu-section-title::before { content: ""; width: 5px; height: 18px; border-radius: 3px; background: var(--grad-green); }
.menu-section-title .cnt { font-size: 12px; color: var(--muted); font-weight: 600; }
.dish-card {
display: flex; gap: 14px; padding: 14px 0;
border-bottom: 1px solid var(--line);
}
.dish-card:last-child { border-bottom: none; }
.dish-img {
flex-shrink: 0; width: 84px; height: 84px; border-radius: 16px;
display: grid; place-items: center; position: relative; overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.4);
}
.dish-img .grad { position: absolute; inset: 0; }
.dish-img svg { position: relative; width: 42px; height: 42px; color: rgba(255,255,255,0.92); }
.dish-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.dish-name { font-size: 15px; font-weight: 800; color: var(--ink); margin-bottom: 4px; }
.dish-desc { font-size: 12px; color: var(--muted); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 6px; }
.dish-tags { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 6px; }
.dish-tags .t { font-size: 10px; padding: 2px 7px; border-radius: 8px; background: var(--green-mist); color: var(--green-600); font-weight: 700; }
.dish-foot { margin-top: auto; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.dish-price { display: flex; align-items: baseline; gap: 6px; }
.dish-price .now { font-size: 18px; font-weight: 900; color: var(--green); }
.dish-price .now small { font-size: 12px; }
.dish-price .old { font-size: 12px; color: #B0B8A8; text-decoration: line-through; }
.dish-rating { font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 3px; }
.add-btn {
display: inline-flex; align-items: center; gap: 5px;
height: 34px; padding: 0 16px; border-radius: 17px;
background: var(--grad-green); color: #fff;
font-weight: 800; font-size: 13px;
box-shadow: 0 6px 14px rgba(45,154,108,0.32);
transition: transform 0.18s;
white-space: nowrap;
}
.add-btn:hover { transform: scale(1.05); }
.add-btn:active { transform: scale(0.96); }
.qty-stepper { display: none; align-items: center; gap: 4px; }
.qty-stepper.show { display: inline-flex; }
.qty-btn {
width: 30px; height: 30px; border-radius: 50%;
display: grid; place-items: center;
font-weight: 900; font-size: 16px;
transition: all 0.18s;
}
.qty-btn.minus { background: #fff; color: var(--green); border: 1.5px solid var(--green-200); }
.qty-btn.minus:hover { border-color: var(--green); }
.qty-btn.plus { background: var(--grad-green); color: #fff; box-shadow: 0 4px 10px rgba(45,154,108,0.3); }
.qty-btn:hover { transform: scale(1.12); }
.qty-num { min-width: 26px; text-align: center; font-weight: 800; color: var(--green-deep); font-size: 15px; }
.menu-loading { padding: 60px 20px; text-align: center; color: var(--muted); }
.menu-loading .spinner { width: 28px; height: 28px; border-width: 3px; margin-bottom: 12px; }
@media (max-width: 720px) {
.modal-hero { padding: 22px 20px 18px; }
.modal-rest { flex-direction: column; align-items: flex-start; gap: 12px; }
.menu-body { flex-direction: column; max-height: 56vh; }
.menu-left { flex: none; display: flex; gap: 8px; overflow-x: auto; overflow-y: hidden; padding: 10px 12px; border-right: none; border-bottom: 1px solid var(--line); }
.menu-cat { flex-shrink: 0; margin: 0; }
.menu-right { padding: 4px 14px 20px; }
}
/* ============ 购物车浮层 ============ */
.cart-bar {
position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%) translateY(120%);
width: calc(100% - 32px); max-width: 480px;
background: #fff; border-radius: 22px;
box-shadow: var(--shadow-lg);
border: 1.5px solid var(--green-100);
padding: 12px 14px 12px 18px;
display: flex; align-items: center; gap: 14px;
z-index: 150;
transition: transform 0.35s cubic-bezier(.34,1.3,.64,1);
}
.cart-bar.show { transform: translateX(-50%) translateY(0); }
.cart-bar-icon {
position: relative; width: 50px; height: 50px; border-radius: 16px;
background: var(--grad-green); color: #fff;
display: grid; place-items: center; flex-shrink: 0;
box-shadow: 0 8px 18px rgba(45,154,108,0.35);
cursor: pointer;
}
.cart-bar-icon .cart-badge { top: -4px; right: -4px; }
.cart-bar-text { flex: 1; min-width: 0; cursor: pointer; }
.cart-bar-total { font-size: 20px; font-weight: 900; color: var(--green-deep); }
.cart-bar-total small { font-size: 13px; color: var(--green); }
.cart-bar-sub { font-size: 12px; color: var(--muted); }
.cart-bar-sub.warn { color: #E67E22; font-weight: 700; }
.cart-checkout {
height: 46px; padding: 0 26px; border-radius: 16px;
background: var(--grad-green); color: #fff;
font-weight: 800; font-size: 15px;
box-shadow: 0 8px 18px rgba(45,154,108,0.35);
transition: all 0.22s;
white-space: nowrap;
}
.cart-checkout:hover:not(:disabled) { transform: translateY(-2px); }
.cart-checkout:disabled { background: #C5D6C8; box-shadow: none; cursor: not-allowed; }
/* ============ 购物车抽屉 ============ */
.cart-drawer-mask {
position: fixed; inset: 0; z-index: 180;
background: rgba(27,94,32,0.3); backdrop-filter: blur(4px);
display: none; opacity: 0; transition: opacity 0.25s;
}
.cart-drawer-mask.show { display: block; opacity: 1; }
.cart-drawer {
position: fixed; left: 50%; bottom: 0; transform: translateX(-50%) translateY(100%);
width: 100%; max-width: 520px;
background: var(--warm-white);
border-radius: 26px 26px 0 0;
box-shadow: 0 -10px 40px rgba(27,94,32,0.2);
z-index: 190;
transition: transform 0.35s cubic-bezier(.34,1.2,.64,1);
max-height: 78vh; display: flex; flex-direction: column;
}
.cart-drawer.show { transform: translateX(-50%) translateY(0); }
.cart-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 20px 22px 14px; border-bottom: 1px solid var(--line); }
.cart-drawer-title { font-size: 18px; font-weight: 900; color: var(--green-deep); display: flex; align-items: center; gap: 8px; }
.cart-drawer-title .cnt { font-size: 13px; color: var(--muted); font-weight: 700; }
.cart-clear { font-size: 13px; color: #E57373; font-weight: 700; display: inline-flex; align-items: center; gap: 4px; }
.cart-drawer-body { overflow-y: auto; padding: 8px 18px 16px; flex: 1; }
.cart-empty { text-align: center; padding: 50px 20px; color: var(--muted); }
.cart-empty .ic { width: 70px; height: 70px; margin: 0 auto 14px; border-radius: 22px; background: var(--green-mist); display: grid; place-items: center; color: var(--green-200); }
.cart-item { display: flex; gap: 12px; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--line); }
.cart-item:last-child { border-bottom: none; }
.cart-item-img { width: 54px; height: 54px; border-radius: 14px; display: grid; place-items: center; position: relative; overflow: hidden; flex-shrink: 0; }
.cart-item-img .grad { position: absolute; inset: 0; }
.cart-item-img svg { position: relative; width: 28px; height: 28px; color: rgba(255,255,255,0.92); }
.cart-item-info { flex: 1; min-width: 0; }
.cart-item-name { font-size: 14px; font-weight: 700; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cart-item-price { font-size: 13px; color: var(--green); font-weight: 800; margin-top: 2px; }
.cart-item-stepper { display: flex; align-items: center; gap: 4px; }
.cart-drawer-foot { padding: 16px 22px 22px; border-top: 1px solid var(--line); background: #fff; border-radius: 0 0 26px 26px; }
.cart-drawer-total { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 14px; }
.cart-drawer-total .lbl { font-size: 14px; color: var(--muted); }
.cart-drawer-total .val { font-size: 24px; font-weight: 900; color: var(--green-deep); }
.cart-drawer-total .val small { font-size: 14px; color: var(--green); }
.cart-drawer-checkout { width: 100%; height: 52px; border-radius: 16px; background: var(--grad-green); color: #fff; font-weight: 800; font-size: 16px; box-shadow: 0 10px 22px rgba(45,154,108,0.35); transition: transform 0.2s; }
.cart-drawer-checkout:hover:not(:disabled) { transform: translateY(-2px); }
.cart-drawer-checkout:disabled { background: #C5D6C8; box-shadow: none; cursor: not-allowed; }
/* ============ 页脚 ============ */
.footer { background: var(--green-soft); padding: 56px 0 30px; margin-top: 40px; position: relative; overflow: hidden; }
.footer::before { content: ""; position: absolute; top: -1px; left: 0; right: 0; height: 40px; background: var(--bg); border-radius: 0 0 50% 50% / 0 0 100% 100%; }
.footer-inner { position: relative; display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: 40px; }
.footer-brand .logo { margin-bottom: 16px; }
.footer-brand p { font-size: 14px; color: var(--muted); max-width: 280px; line-height: 1.7; }
.footer-col h4 { font-size: 15px; font-weight: 800; color: var(--green-deep); margin-bottom: 14px; }
.footer-col a { display: block; font-size: 14px; color: var(--muted); margin-bottom: 10px; transition: color 0.2s; }
.footer-col a:hover { color: var(--green); }
.footer-bottom { position: relative; margin-top: 40px; padding-top: 22px; border-top: 1px solid var(--green-100); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; font-size: 13px; color: var(--muted); }
.footer-social { display: flex; gap: 10px; }
.footer-social a { width: 38px; height: 38px; border-radius: 12px; background: #fff; display: grid; place-items: center; color: var(--green); box-shadow: var(--shadow-sm); transition: all 0.22s; }
.footer-social a:hover { transform: translateY(-3px); background: var(--grad-green); color: #fff; }
@media (max-width: 760px) { .footer-inner { grid-template-columns: 1fr 1fr; gap: 30px; } .footer-brand { grid-column: 1 / -1; } }
/* ============ Toast ============ */
.toast-wrap { position: fixed; top: 86px; left: 50%; transform: translateX(-50%); z-index: 300; display: flex; flex-direction: column; gap: 10px; align-items: center; pointer-events: none; }
.toast {
background: var(--grad-green); color: #fff;
padding: 12px 22px; border-radius: 14px;
font-size: 14px; font-weight: 700;
box-shadow: 0 12px 30px rgba(45,154,108,0.4);
display: flex; align-items: center; gap: 8px;
opacity: 0; transform: translateY(-16px) scale(0.95);
transition: all 0.32s cubic-bezier(.34,1.4,.64,1);
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translateY(0) scale(1); }
.toast.warn { background: linear-gradient(135deg, #FF9800, #FF7043); box-shadow: 0 12px 30px rgba(255,112,67,0.4); }
.toast svg { width: 18px; height: 18px; }
/* 移动端导航 */
@media (max-width: 640px) {
.nav-inner { gap: 10px; height: 62px; }
.logo-text { font-size: 18px; }
.logo-mark { width: 38px; height: 38px; }
.search-wrap { max-width: none; }
.nav-cta { display: none; }
.section { padding: 40px 0; }
}
</style>
</head>
<body>
<!-- ===== 顶部导航 ===== -->
<header class="navbar">
<div class="container nav-inner">
<div class="logo" onclick="scrollToTop()">
<div class="logo-mark" id="logoMark"></div>
<div class="logo-text">绿野<span>鲜送</span></div>
</div>
<div class="search-wrap">
<input id="searchInput" class="search-box" type="text" placeholder="搜索餐厅或美食,如 轻食、寿司、奶茶…" autocomplete="off" />
<button class="search-btn" id="searchBtn" aria-label="搜索"></button>
</div>
<div class="nav-actions">
<button class="nav-cta" onclick="scrollToRestaurants()">
<span id="navCtaIcon"></span> 立即点餐
</button>
<button class="cart-btn" id="cartBtn" aria-label="购物车">
<span id="cartBtnIcon"></span>
<span class="cart-badge" id="cartBadge">0</span>
</button>
</div>
</div>
</header>
<!-- ===== Hero ===== -->
<section class="hero">
<div class="hero-leaf leaf-1" id="leaf1"></div>
<div class="hero-leaf leaf-2" id="leaf2"></div>
<div class="hero-leaf leaf-3" id="leaf3"></div>
<div class="container">
<div class="hero-card">
<div class="hero-text">
<span class="hero-tag"><span class="dot"></span> 当日鲜配 · 0 防腐剂</span>
<h1 class="hero-title">新鲜好味 <em>·</em> 绿色送达</h1>
<p class="hero-desc">甄选本地优质餐厅,从农场到餐桌的轻盈之旅。低卡轻食、匠心料理,用一份自然的绿意,点亮每一餐。</p>
<div class="hero-actions">
<button class="btn-primary" onclick="scrollToRestaurants()">
<span id="heroCtaIcon"></span> 开始点餐
</button>
<button class="btn-ghost" onclick="scrollToCategories()">浏览分类</button>
</div>
<div class="hero-stats">
<div class="hero-stat"><div class="num" id="statRest">12+</div><div class="lbl">精选餐厅</div></div>
<div class="hero-stat"><div class="num">30min</div><div class="lbl">平均送达</div></div>
<div class="hero-stat"><div class="num">4.7★</div><div class="lbl">平均评分</div></div>
</div>
</div>
<div class="hero-visual">
<div class="hero-bowl" id="heroBowl"></div>
<div class="hero-float f1"><span class="ic" id="floatIc1"></span>低卡轻食</div>
<div class="hero-float f2"><span class="ic" id="floatIc2"></span>新鲜直送</div>
<div class="hero-float f3"><span class="ic" id="floatIc3"></span>绿色包装</div>
</div>
</div>
</div>
</section>
<!-- ===== 分类网格 ===== -->
<section class="section" id="categoriesSection">
<div class="container">
<div class="section-head">
<div class="section-title">
<span class="kicker">CATEGORIES</span>
<h2>美食分类</h2>
<p>挑你所爱,一键直达心仪风味</p>
</div>
</div>
<div class="cat-grid" id="catGrid"></div>
</div>
</section>
<!-- ===== 推荐餐厅 ===== -->
<section class="section" id="featuredSection" style="padding-top:0">
<div class="container">
<div class="section-head">
<div class="section-title">
<span class="kicker">FEATURED</span>
<h2>人气推荐</h2>
<p>本月销量领先,大家都在点</p>
</div>
<a class="link-more" href="javascript:scrollToRestaurants()">查看全部 →</a>
</div>
<div class="featured-scroller" id="featuredScroller"></div>
</div>
</section>
<!-- ===== 餐厅列表 ===== -->
<section class="section" id="restaurantsSection" style="padding-top:0">
<div class="container">
<div class="section-head">
<div class="section-title">
<span class="kicker">RESTAURANTS</span>
<h2>全部餐厅</h2>
<p id="listSubtitle">为你找到附近优质好店</p>
</div>
</div>
<div class="toolbar">
<div class="sort-group" id="sortGroup">
<button class="sort-btn active" data-sort="">综合</button>
<button class="sort-btn" data-sort="rating">评分</button>
<button class="sort-btn" data-sort="sales">销量</button>
<button class="sort-btn" data-sort="delivery_time">送达</button>
</div>
<button class="chip-clear" id="chipClear">
<span id="chipClearIcon"></span> <span id="chipClearText">清除筛选</span>
</button>
<div class="result-info">共 <b id="resultCount">0</b> 家餐厅</div>
</div>
<div id="restListWrap">
<div class="rest-grid" id="restGrid"></div>
</div>
<div class="load-more-wrap" id="loadMoreWrap" style="display:none">
<button class="load-more" id="loadMoreBtn">加载更多</button>
</div>
</div>
</section>
<!-- ===== 页脚 ===== -->
<footer class="footer">
<div class="container">
<div class="footer-inner">
<div class="footer-brand">
<div class="logo">
<div class="logo-mark" id="footerLogo"></div>
<div class="logo-text">绿野<span>鲜送</span></div>
</div>
<p>新鲜好味,绿色送达。致力于连接本地优质餐厅与健康食材,让每一餐都自然、轻盈、安心。</p>
<div class="footer-social" style="margin-top:16px">
<a href="javascript:void(0)" aria-label="微信" id="socialWx"></a>
<a href="javascript:void(0)" aria-label="微博" id="socialWb"></a>
<a href="javascript:void(0)" aria-label="邮箱" id="socialMail"></a>
</div>
</div>
<div class="footer-col">
<h4>关于我们</h4>
<a href="javascript:void(0)">品牌故事</a>
<a href="javascript:void(0)">绿色承诺</a>
<a href="javascript:void(0)">加入我们</a>
<a href="javascript:void(0)">联系合作</a>
</div>
<div class="footer-col">
<h4>帮助中心</h4>
<a href="javascript:void(0)">下单指南</a>
<a href="javascript:void(0)">配送说明</a>
<a href="javascript:void(0)">退款规则</a>
<a href="javascript:void(0)">常见问题</a>
</div>
<div class="footer-col">
<h4>商家合作</h4>
<a href="javascript:void(0)">入驻平台</a>
<a href="javascript:void(0)">商家后台</a>
<a href="javascript:void(0)">营销推广</a>
<a href="javascript:void(0)">开放接口</a>
</div>
</div>
<div class="footer-bottom">
<span>© 2026 绿野鲜送 FreshGreen · 用心连接每一份新鲜</span>
<span>新鲜 · 健康 · 自然 · 轻盈</span>
</div>
</div>
</footer>
<!-- ===== 餐厅详情模态 ===== -->
<div class="modal-mask" id="modalMask">
<div class="modal" id="modal">
<div class="modal-hero" id="modalHero"></div>
<div class="menu-body" id="menuBody"></div>
</div>
</div>
<!-- ===== 购物车抽屉 ===== -->
<div class="cart-drawer-mask" id="cartDrawerMask"></div>
<div class="cart-drawer" id="cartDrawer">
<div class="cart-drawer-head">
<div class="cart-drawer-title">
<span id="drawerTitleIcon"></span> 购物车 <span class="cnt" id="drawerCount"></span>
</div>
<button class="cart-clear" id="cartClearBtn">
<span id="clearIcon"></span> 清空
</button>
</div>
<div class="cart-drawer-body" id="cartDrawerBody"></div>
<div class="cart-drawer-foot" id="cartDrawerFoot"></div>
</div>
<!-- ===== 购物车浮层 ===== -->
<div class="cart-bar" id="cartBar">
<div class="cart-bar-icon" id="cartBarIcon">
<span id="cartBarIconSvg"></span>
<span class="cart-badge" id="cartBarBadge">0</span>
</div>
<div class="cart-bar-text" id="cartBarText">
<div class="cart-bar-total"><small>¥</small><span id="cartBarTotal">0</span></div>
<div class="cart-bar-sub" id="cartBarSub">购物车是空的</div>
</div>
<button class="cart-checkout" id="cartCheckoutBtn">去结算</button>
</div>
<!-- ===== Toast ===== -->
<div class="toast-wrap" id="toastWrap"></div>
<script>
/* ============================================================
绿野鲜送 — 前端逻辑
============================================================ */
(function () {
"use strict";
/* ---------- SVG 图标库 ---------- */
const S = (path, vb) => `<svg viewBox="${vb || '0 0 24 24'}" fill="none" xmlns="http://www.w3.org/2000/svg">${path}</svg>`;
const ICONS = {
logo: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 19c0-7 5-13 14-14 0 9-6 15-14 14Z" fill="#fff"/><path d="M5 19c2-4 5-7 9-9" stroke="#2D9A6C" stroke-width="1.6" stroke-linecap="round"/></svg>`,
search: `<svg viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7" stroke="#fff" stroke-width="2.2"/><path d="m20 20-3.2-3.2" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/></svg>`,
cart: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 7h14l-1.2 10.5a2 2 0 0 1-2 1.8H8.2a2 2 0 0 1-2-1.8L5 7Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M9 10V7a3 3 0 0 1 6 0v3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
bag: `<svg viewBox="0 0 24 24" fill="none"><path d="M6 8h12l-1 11.5a1.5 1.5 0 0 1-1.5 1.4h-9A1.5 1.5 0 0 1 5 19.5L6 8Z" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><path d="M9 8V6.5a3 3 0 0 1 6 0V8" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>`,
star: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 17.9 6.8 19.6l1-5.8-4.3-4.1 5.9-.9L12 3.5Z"/></svg>`,
clock: `<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="8.5" stroke="currentColor" stroke-width="1.8"/><path d="M12 7.5V12l3 2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
pin: `<svg viewBox="0 0 24 24" fill="none"><path d="M12 21s7-5.5 7-11a7 7 0 1 0-14 0c0 5.5 7 11 7 11Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><circle cx="12" cy="10" r="2.6" stroke="currentColor" stroke-width="1.8"/></svg>`,
truck: `<svg viewBox="0 0 24 24" fill="none"><path d="M3 6h11v9H3V6Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M14 9h4l3 3v3h-7V9Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><circle cx="7" cy="18" r="2" stroke="currentColor" stroke-width="1.8"/><circle cx="17" cy="18" r="2" stroke="currentColor" stroke-width="1.8"/></svg>`,
fire: `<svg viewBox="0 0 24 24" fill="none"><path d="M12 3c.5 3-2 4-2 6.5 0 1 .8 2 1.5 2 .8 0 1.5-1 1.5-2.2C14.5 11 16 12.5 16 15a4 4 0 0 1-8 0c0-3.5 4-5 4-12Z" fill="currentColor"/></svg>`,
close: `<svg viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>`,
plus: `<svg viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/></svg>`,
minus: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 12h14" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/></svg>`,
trash: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 7h14M9 7V5.5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1V7m-9 0 .8 12a1.5 1.5 0 0 0 1.5 1.4h5.4a1.5 1.5 0 0 0 1.5-1.4L18 7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
bowl: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 11h16a8 8 0 0 1-16 0Z" fill="currentColor"/><path d="M4 11a8 8 0 0 0 16 0H4Z" stroke="currentColor" stroke-width="1.6"/><path d="M2 11h20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 5c0 1.5 1 2 1 3.5M13 4c0 1.5 1 2 1 3.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
megaphone: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 10v4a1 1 0 0 0 1 1h2l9 4V5L7 9H5a1 1 0 0 0-1 1Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M19 9a3 3 0 0 1 0 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`,
leaf: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 19c0-8 6-14 14-14 0 8-6 14-14 14Z" fill="currentColor" opacity="0.85"/><path d="M5 19c3-5 7-8 11-10" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>`,
arrow: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
sparkle: `<svg viewBox="0 0 24 24" fill="none"><path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3Z" fill="currentColor"/></svg>`,
check: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 12.5l4.5 4.5L19 7" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
warn: `<svg viewBox="0 0 24 24" fill="none"><path d="M12 4l9 16H3L12 4Z" stroke="#fff" stroke-width="2" stroke-linejoin="round"/><path d="M12 10v4" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/><circle cx="12" cy="17" r="1.2" fill="#fff"/></svg>`,
empty: `<svg viewBox="0 0 24 24" fill="none"><path d="M6 9h12l-1 10a1.5 1.5 0 0 1-1.5 1.4h-7A1.5 1.5 0 0 1 7 19L6 9Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M9 9V7a3 3 0 0 1 6 0v2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 14h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-dasharray="2 2"/></svg>`,
error: `<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8"/><path d="M12 7v6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="16" r="1.2" fill="currentColor"/></svg>`,
// 社交
wx: `<svg viewBox="0 0 24 24" fill="none"><path d="M9 4C5 4 2 6.8 2 10.2c0 1.9 1 3.6 2.6 4.7L4 17l2.5-1.3c.8.2 1.6.3 2.5.3.3 0 .6 0 .9-.1-.2-.5-.2-1-.2-1.5 0-3 2.9-5.4 6.4-5.4h.6C16 6 12.8 4 9 4Z" fill="currentColor"/><path d="M22 14.5c0-2.5-2.5-4.6-5.5-4.6S11 12 11 14.5s2.5 4.6 5.5 4.6c.6 0 1.3-.1 1.9-.3L20.5 20l-.4-1.6c1.1-.8 1.9-2.1 1.9-3.9Z" fill="currentColor"/></svg>`,
wb: `<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8"/><path d="M7 13c1.5 1.5 4 1.5 5.5 0M9 9.5c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .4 1 1Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
mail: `<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="5" width="18" height="14" rx="2.5" stroke="currentColor" stroke-width="1.8"/><path d="m4 7 8 6 8-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>`
};
// 分类图标映射(按 icon 字段 / 关键词)
const CAT_ICONS = {
chinese: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 10a8 8 0 0 0 16 0H4Z" fill="currentColor"/><path d="M2 10h20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 5c0 1.4.9 1.9.9 3.3M13 4.5c0 1.4.9 1.9.9 3.3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
japanese: `<svg viewBox="0 0 24 24" fill="none"><path d="M3 12c0-3.9 4-7 9-7s9 3.1 9 7-4 7-9 7-9-3.1-9-7Z" stroke="currentColor" stroke-width="1.8"/><ellipse cx="9" cy="12" rx="2" ry="3" fill="currentColor"/><ellipse cx="15" cy="12" rx="2" ry="3" fill="currentColor"/></svg>`,
western: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 11c0-3 1.8-5 8-5s8 2 8 5H4Z" fill="currentColor"/><rect x="4" y="12" width="16" height="2.2" rx="1.1" fill="currentColor"/><rect x="5" y="15.5" width="14" height="2.2" rx="1.1" fill="currentColor"/><path d="M6 18.5h12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`,
hotpot: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 11h16l-1 6a2 2 0 0 1-2 1.8H7A2 2 0 0 1 5 17l-1-6Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M2 11h20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 6c0-1 .5-1.5.5-2.5M12 5c0-1 .5-1.5.5-2.5M15 6c0-1 .5-1.5.5-2.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
dessert: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 10h14l-1.2 8.5a1.5 1.5 0 0 1-1.5 1.3H7.7a1.5 1.5 0 0 1-1.5-1.3L5 10Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M5 10h14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="9" cy="6" r="1.4" fill="currentColor"/><circle cx="12" cy="4.5" r="1.4" fill="currentColor"/><circle cx="15" cy="6" r="1.4" fill="currentColor"/></svg>`,
healthy: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 19c0-8 6-14 14-14 0 8-6 14-14 14Z" fill="currentColor" opacity="0.9"/><path d="M5 19c3-5 7-8 11-10" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/></svg>`,
default: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 11a8 8 0 0 0 16 0H4Z" fill="currentColor"/><path d="M2 11h20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`
};
// 菜品图标(按标签/名称关键词)
const DISH_ICONS = {
salad: `<svg viewBox="0 0 24 24" fill="none"><path d="M3 12a9 9 0 0 0 18 0H3Z" stroke="currentColor" stroke-width="1.7"/><path d="M2 12h20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M8 7c0 1.6 1 2 1 3.5M12 5c0 1.6 1 2 1 3.5M16 7c0 1.6 1 2 1 3.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
bowl: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 11a8 8 0 0 0 16 0H4Z" fill="currentColor"/><path d="M2 11h20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 5c0 1.4.9 1.9.9 3.3M13 4.5c0 1.4.9 1.9.9 3.3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
cup: `<svg viewBox="0 0 24 24" fill="none"><path d="M6 8h12l-1 11a1.5 1.5 0 0 1-1.5 1.4h-7A1.5 1.5 0 0 1 7 19L6 8Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M6 11h12" stroke="currentColor" stroke-width="1.6"/><path d="M10 5c0-1 .8-1.3.8-2M14 5c0-1 .8-1.3.8-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
meat: `<svg viewBox="0 0 24 24" fill="none"><path d="M6 14c-2 0-3-1.5-3-3s1-3 3-3c1 0 2 .5 2.5 1.5C10 7.5 12 7 14 8s3 3 3 5-2 4-4 4c-1.5 0-2.8-.7-3.5-2C8.5 16 7 16 6 14Z" fill="currentColor"/><path d="M16 6c1 0 1.5 1 1.5 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
fish: `<svg viewBox="0 0 24 24" fill="none"><path d="M3 12c3-4 7-5 11-5 3 0 6 2 7 5-1 3-4 5-7 5-4 0-8-1-11-5Z" fill="currentColor"/><path d="M3 12 6 9M3 12l3 3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="16" cy="11" r="1" fill="#fff"/></svg>`,
rice: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 11a7 7 0 0 0 14 0H5Z" fill="currentColor"/><path d="M3 11h18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 6c0 1 .6 1.4.6 2.4M12 5.5c0 1 .6 1.4.6 2.4M15 6c0 1 .6 1.4.6 2.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
cake: `<svg viewBox="0 0 24 24" fill="none"><path d="M5 13c0-2 2-3 7-3s7 1 7 3v6H5v-6Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M5 16c2 0 3-1 3-2M12 16c2 0 3-1 3-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M12 4.5c0 .8-.5 1-.5 1.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="11.5" cy="4" r="1" fill="currentColor"/></svg>`,
default: `<svg viewBox="0 0 24 24" fill="none"><path d="M4 11a8 8 0 0 0 16 0H4Z" fill="currentColor"/><path d="M2 11h20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M9 5c0 1.4.9 1.9.9 3.3M13 4.5c0 1.4.9 1.9.9 3.3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`
};
// 为餐厅/菜品分配渐变色(按 id 取模,保证同一家稳定)
const GRADIENTS = [
{ id: 'g1', css: 'linear-gradient(135deg,#2D9A6C,#66BB6A)' },
{ id: 'g2', css: 'linear-gradient(135deg,#26A69A,#80CBC4)' },
{ id: 'g3', css: 'linear-gradient(135deg,#43A047,#A5D6A7)' },
{ id: 'g4', css: 'linear-gradient(135deg,#558B2F,#AED581)' },
{ id: 'g5', css: 'linear-gradient(135deg,#00897B,#4DB6AC)' },
{ id: 'g6', css: 'linear-gradient(135deg,#388E3C,#81C784)' },
{ id: 'g7', css: 'linear-gradient(135deg,#7CB342,#C5E1A5)' },
{ id: 'g8', css: 'linear-gradient(135deg,#2E7D32,#66BB6A)' }
];
const gradFor = (id) => GRADIENTS[(Number(id) || 0) % GRADIENTS.length];
/* ---------- 工具函数 ---------- */
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
const API = '/api/v1/food';
const parseTags = (str) => { try { const v = JSON.parse(str); return Array.isArray(v) ? v : []; } catch (e) { return []; } };
const money = (n) => { n = Number(n) || 0; return n % 1 === 0 ? String(n) : n.toFixed(2).replace(/0$/, ''); };
const money2 = (n) => (Number(n) || 0).toFixed(1).replace(/\.0$/, '');
const escapeHtml = (s) => String(s || '').replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
function catIcon(icon, name) {
const key = (icon || '').toLowerCase();
if (CAT_ICONS[key]) return CAT_ICONS[key];
const n = (name || '');
if (/中|川|粤|京|餐|菜/.test(n)) return CAT_ICONS.chinese;
if (/日|韩|寿司|刺身|料/.test(n)) return CAT_ICONS.japanese;
if (/西|汉堡|披萨|快餐|美/.test(n)) return CAT_ICONS.western;
if (/火锅|烧烤|串|锅/.test(n)) return CAT_ICONS.hotpot;
if (/奶茶|甜品|甜|饮|茶/.test(n)) return CAT_ICONS.dessert;
if (/轻食|健康|沙拉|低卡|绿/.test(n)) return CAT_ICONS.healthy;
return CAT_ICONS.default;
}
function dishIcon(item) {
const n = (item.name || '') + (item.description || '') + (item.tags || '');
if (/沙拉|轻食|蔬|素|绿|藜麦|牛油果/.test(n)) return DISH_ICONS.salad;
if (/鱼|刺身|三文鱼|金枪|鳗|虾/.test(n)) return DISH_ICONS.fish;
if (/肉|牛|鸡|鸭|排|烤|串|腰|肝/.test(n)) return DISH_ICONS.meat;
if (/茶|奶|饮|汁|酒|可乐/.test(n)) return DISH_ICONS.cup;
if (/蛋糕|提拉米苏|马卡龙|甜|熔岩/.test(n)) return DISH_ICONS.cake;
if (/饭|面|河粉|拌饭|粥|馒头/.test(n)) return DISH_ICONS.rice;
if (/汤|锅|豆腐|毛肚|鸭血/.test(n)) return DISH_ICONS.bowl;
return DISH_ICONS.default;
}
async function fetchJSON(url) {
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
if (!res.ok) throw new Error('网络错误 ' + res.status);
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || '接口返回异常');
return json.data;
}
/* ---------- 状态 ---------- */
const state = {
categories: [],
currentCat: 0, // 0 = 全部
keyword: '',
sort: '',
page: 1,
pageSize: 12,
restaurants: [],
total: 0,
loading: false,
cart: [], // {id, name, price, qty, restaurant_id, restaurant_name, icon}
cartRestId: null,
menu: null, // 当前打开餐厅的菜单
activeMenuCat: null
};
/* ---------- 初始化图标 ---------- */
function initStaticIcons() {
$('#logoMark').innerHTML = ICONS.logo;
$('#footerLogo').innerHTML = ICONS.logo;
$('#searchBtn').innerHTML = ICONS.search;
$('#cartBtnIcon').innerHTML = ICONS.cart;
$('#navCtaIcon').innerHTML = ICONS.truck;
$('#heroCtaIcon').innerHTML = ICONS.sparkle;
$('#cartBarIconSvg').innerHTML = ICONS.bag;
$('#cartBtnIcon').innerHTML = ICONS.cart;
$('#clearIcon').innerHTML = ICONS.trash;
$('#drawerTitleIcon').innerHTML = ICONS.cart;
$('#floatIc1').innerHTML = ICONS.leaf;
$('#floatIc2').innerHTML = ICONS.sparkle;
$('#floatIc3').innerHTML = ICONS.truck;
$('#chipClearIcon').innerHTML = ICONS.close;
$('#heroBowl').innerHTML = `<svg viewBox="0 0 200 200" fill="none">
<ellipse cx="100" cy="120" rx="78" ry="20" fill="#C8E6C9"/>
<path d="M30 115c0 28 31 50 70 50s70-22 70-50" stroke="#2D9A6C" stroke-width="6" stroke-linecap="round"/>
<path d="M22 115h156" stroke="#1B5E20" stroke-width="8" stroke-linecap="round"/>
<circle cx="78" cy="92" r="14" fill="#66BB6A"/>
<circle cx="108" cy="86" r="16" fill="#EF5350"/>
<circle cx="128" cy="100" r="11" fill="#FFB74D"/>
<path d="M78 78c0 8 6 12 6 18M108 70c0 8 6 12 6 18" stroke="#2D9A6C" stroke-width="4" stroke-linecap="round"/>
<path d="M100 60c0 6 4 9 4 14" stroke="#81C784" stroke-width="4" stroke-linecap="round"/>
</svg>`;
// hero 叶子
const leafSvg = (rot) => `<svg viewBox="0 0 100 100" fill="none" style="transform:rotate(${rot}deg)">
<path d="M20 80c0-35 25-60 60-60 0 35-25 60-60 60Z" fill="#81C784"/>
<path d="M20 80c12-22 28-36 46-44" stroke="#2D9A6C" stroke-width="2" stroke-linecap="round"/>
</svg>`;
$('#leaf1').innerHTML = leafSvg(-20);
$('#leaf2').innerHTML = leafSvg(35);
$('#leaf3').innerHTML = leafSvg(10);
$('#socialWx').innerHTML = ICONS.wx;
$('#socialWb').innerHTML = ICONS.wb;
$('#socialMail').innerHTML = ICONS.mail;
}
/* ---------- Toast ---------- */
function toast(msg, type) {
const el = document.createElement('div');
el.className = 'toast' + (type === 'warn' ? ' warn' : '');
el.innerHTML = (type === 'warn' ? ICONS.warn : ICONS.check) + `<span>${escapeHtml(msg)}</span>`;
$('#toastWrap').appendChild(el);
requestAnimationFrame(() => el.classList.add('show'));
setTimeout(() => {
el.classList.remove('show');
setTimeout(() => el.remove(), 350);
}, 2200);
}
/* ---------- 渲染:分类 ---------- */
function renderCategories() {
const wrap = $('#catGrid');
if (!state.categories.length) {
wrap.innerHTML = '<div class="sk-cat-grid">' + Array(6).fill('<div class="sk-cat skeleton"></div>').join('') + '</div>';
return;
}
const all = [{ id: 0, name: '全部', icon: 'all', sort: 0 }];
const cats = all.concat(state.categories);
wrap.innerHTML = cats.map(c => {
const active = state.currentCat === c.id ? ' active' : '';
const ic = c.id === 0
? `<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="7" height="7" rx="2" fill="currentColor"/><rect x="14" y="3" width="7" height="7" rx="2" fill="currentColor" opacity="0.6"/><rect x="3" y="14" width="7" height="7" rx="2" fill="currentColor" opacity="0.6"/><rect x="14" y="14" width="7" height="7" rx="2" fill="currentColor"/></svg>`
: catIcon(c.icon, c.name);
return `<div class="cat-card${active}" data-id="${c.id}" onclick="window._app.selectCat(${c.id})">
<div class="cat-ic">${ic}</div>
<div class="cat-name">${escapeHtml(c.name)}</div>
</div>`;
}).join('');
}
/* ---------- 渲染:推荐 ---------- */
function renderFeatured(list) {
const wrap = $('#featuredScroller');
if (!list || !list.length) {
wrap.innerHTML = '<div class="sk-scroller">' + Array(4).fill('<div class="sk-feat skeleton"></div>').join('') + '</div>';
return;
}
wrap.innerHTML = list.map((r, i) => {
const g = gradFor(r.id);
const tags = parseTags(r.tags).slice(0, 2);
return `<div class="feat-card" onclick="window._app.openRestaurant(${r.id})">
<div class="feat-cover">
<div class="grad" style="background:${g.css}"></div>
${r.cover?`<img src="${escapeHtml(r.cover)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('.ic').style.display=''">`:''}
<div class="ic" style="color:${g.css.match(/#[0-9A-Fa-f]+/)[0]};${r.cover?'display:none':''}">${catIcon(r.category_name ? '' : '', r.category_name)}</div>
<div class="feat-rank">${i + 1}</div>
</div>
<div class="feat-body">
<div class="feat-name">${escapeHtml(r.name)}</div>
<div class="feat-desc">${escapeHtml(r.description || '')}</div>
<div class="feat-meta">
<div class="feat-rating">${ICONS.star} ${money2(r.rating)}</div>
<div class="feat-sales">月售 ${r.sales}</div>
</div>
${tags.length ? `<div class="feat-tags">${tags.map(t => `<span class="t">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
</div>
</div>`;
}).join('');
}
/* ---------- 渲染:餐厅列表 ---------- */
function restaurantCard(r) {
const g = gradFor(r.id);
const tags = parseTags(r.tags).slice(0, 3);
const open = r.status === 1;
return `<div class="rest-card" onclick="window._app.openRestaurant(${r.id})">
<div class="rest-logo">
<div class="grad" style="background:${g.css}"></div>
${r.logo?`<img src="${escapeHtml(r.logo)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('span').style.display=''">`:''}
<span style="${r.logo?'display:none':''}">${catIcon(r.category_name ? '' : '', r.category_name)}</span>
</div>
<div class="rest-info">
<div class="rest-head">
<div class="rest-name">${escapeHtml(r.name)}</div>
<div class="rest-status ${open ? 'open' : 'closed'}">${open ? '营业中' : '休息'}</div>
</div>
<div class="rest-cat">${escapeHtml(r.category_name || '美食')}</div>
${tags.length ? `<div class="rest-tags">${tags.map(t => `<span class="t">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
<div class="rest-meta">
<span class="rest-rating">${ICONS.star} ${money2(r.rating)}</span>
<span class="rest-sales">月售 ${r.sales}</span>
</div>
<div class="rest-delivery">
<span>${ICONS.clock} ${r.delivery_time}分钟</span>
<span class="sep"></span>
<span>${ICONS.pin} ${escapeHtml(r.distance || '-')}</span>
<span class="sep"></span>
<span>起送 <b>¥${money(r.min_order)}</b></span>
<span class="sep"></span>
<span>配送 <b>¥${money(r.delivery_fee)}</b></span>
</div>
</div>
</div>`;
}
function renderRestaurants() {
const wrap = $('#restGrid');
$('#resultCount').textContent = state.total;
if (!state.restaurants.length) {
wrap.innerHTML = `<div class="state-box" style="grid-column:1/-1">
<div class="ic">${ICONS.empty}</div>
<h3>暂无符合条件的餐厅</h3>
<p>试试切换分类或调整搜索关键词</p>
</div>`;
return;
}
wrap.innerHTML = state.restaurants.map(restaurantCard).join('');
}
function skeletonList(n) {
return Array(n).fill(`<div class="sk-card">
<div class="sk-logo skeleton"></div>
<div class="sk-lines">
<div class="sk-line w70"></div>
<div class="sk-line w50"></div>
<div class="sk-line w90"></div>
<div class="sk-line w50"></div>
</div>
</div>`).join('');
}
/* ---------- 数据加载 ---------- */
async function loadHome() {
renderCategories(); // 骨架
renderFeatured(null); // 骨架
try {
const data = await fetchJSON(`${API}/home`);
state.categories = data.categories || [];
renderCategories();
renderFeatured(data.featured || []);
$('#statRest').textContent = (data.featured && data.featured.length ? '' : '') + '12+';
} catch (e) {
console.error(e);
renderFeatured([]);
$('#catGrid').innerHTML = `<div class="state-box" style="grid-column:1/-1">
<div class="ic">${ICONS.error}</div>
<h3>分类加载失败</h3>
<p>${escapeHtml(e.message)}</p>
<div class="retry"><button class="btn-ghost" onclick="window._app.loadHome()">重新加载</button></div>
</div>`;
}
}
async function loadRestaurants(reset) {
if (state.loading) return;
state.loading = true;
if (reset) { state.page = 1; state.restaurants = []; }
$('#loadMoreWrap').style.display = 'none';
if (reset) $('#restGrid').innerHTML = skeletonList(6);
const params = new URLSearchParams();
if (state.currentCat) params.set('category_id', state.currentCat);
if (state.keyword) params.set('keyword', state.keyword);
if (state.sort) params.set('sort', state.sort);
params.set('page', state.page);
params.set('page_size', state.pageSize);
try {
const data = await fetchJSON(`${API}/restaurants?${params.toString()}`);
const list = data.list || [];
state.restaurants = reset ? list : state.restaurants.concat(list);
state.total = data.total || 0;
renderRestaurants();
// 加载更多
if (state.restaurants.length < state.total) {
$('#loadMoreWrap').style.display = 'block';
$('#loadMoreBtn').disabled = false;
$('#loadMoreBtn').innerHTML = '加载更多';
} else {
$('#loadMoreWrap').style.display = 'none';
}
} catch (e) {
console.error(e);
$('#restGrid').innerHTML = `<div class="state-box" style="grid-column:1/-1">
<div class="ic">${ICONS.error}</div>
<h3>餐厅列表加载失败</h3>
<p>${escapeHtml(e.message)}</p>
<div class="retry"><button class="btn-ghost" onclick="window._app.loadRestaurants(true)">重新加载</button></div>
</div>`;
} finally {
state.loading = false;
updateChipClear();
updateSubtitle();
}
}
function updateSubtitle() {
const parts = [];
if (state.currentCat) {
const c = state.categories.find(x => x.id === state.currentCat);
if (c) parts.push('“' + c.name + '”');
}
if (state.keyword) parts.push('“' + state.keyword + '”');
$('#listSubtitle').textContent = parts.length ? `当前筛选:${parts.join(' · ')}` : '为你找到附近优质好店';
}
function updateChipClear() {
const show = state.currentCat || state.keyword;
$('#chipClear').classList.toggle('show', !!show);
const parts = [];
if (state.currentCat) { const c = state.categories.find(x => x.id === state.currentCat); if (c) parts.push(c.name); }
if (state.keyword) parts.push('“' + state.keyword + '”');
$('#chipClearText').textContent = parts.length ? '清除:' + parts.join(' / ') : '清除筛选';
}
/* ---------- 交互:分类/搜索/排序 ---------- */
function selectCat(id) {
if (state.currentCat === id) return;
state.currentCat = id;
renderCategories();
loadRestaurants(true);
scrollToRestaurants();
}
function doSearch() {
const kw = $('#searchInput').value.trim();
if (kw === state.keyword) return;
state.keyword = kw;
loadRestaurants(true);
if (kw) scrollToRestaurants();
}
function setSort(sort) {
if (state.sort === sort) return;
state.sort = sort;
$$('#sortGroup .sort-btn').forEach(b => b.classList.toggle('active', b.dataset.sort === sort));
loadRestaurants(true);
}
function clearFilters() {
state.currentCat = 0;
state.keyword = '';
state.sort = '';
$('#searchInput').value = '';
$$('#sortGroup .sort-btn').forEach(b => b.classList.toggle('active', b.dataset.sort === ''));
renderCategories();
loadRestaurants(true);
}
function loadMore() {
if (state.loading) return;
state.page++;
$('#loadMoreBtn').disabled = true;
$('#loadMoreBtn').innerHTML = '<span class="spinner"></span> 加载中…';
loadRestaurants(false);
}
/* ---------- 餐厅详情 ---------- */
async function openRestaurant(id) {
const mask = $('#modalMask');
mask.classList.add('show');
document.body.style.overflow = 'hidden';
$('#modalHero').innerHTML = `<div style="padding:60px;text-align:center;color:var(--muted)"><div class="spinner" style="width:30px;height:30px;border-width:3px;margin:0 auto 12px"></div>加载中…</div>`;
$('#menuBody').innerHTML = `<div class="menu-loading"><div class="spinner"></div><div>正在准备菜单…</div></div>`;
state.menu = null;
try {
const data = await fetchJSON(`${API}/restaurants/${id}/menu`);
state.menu = data;
renderModal(data);
} catch (e) {
console.error(e);
$('#modalHero').innerHTML = '';
$('#menuBody').innerHTML = `<div class="state-box"><div class="ic">${ICONS.error}</div><h3>菜单加载失败</h3><p>${escapeHtml(e.message)}</p></div>`;
}
}
function renderModal(data) {
const r = data.restaurant || {};
const cats = data.categories || [];
const items = data.menu_items || [];
const g = gradFor(r.id);
const tags = parseTags(r.tags);
$('#modalHero').innerHTML = `
<button class="modal-close" onclick="window._app.closeModal()">${ICONS.close}</button>
<div class="modal-rest">
<div class="modal-logo"><div class="grad" style="background:${g.css}"></div>${r.logo?`<img src="${escapeHtml(r.logo)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('span').style.display=''">`:''}<span style="${r.logo?'display:none':''}">${catIcon('', r.category_name)}</span></div>
<div class="modal-rest-info">
<div class="modal-rest-name">${escapeHtml(r.name)} <span class="rest-status ${r.status === 1 ? 'open' : 'closed'}">${r.status === 1 ? '营业中' : '休息中'}</span></div>
<div class="modal-rest-cat">${escapeHtml(r.category_name || '美食')} · ${escapeHtml(r.distance || '')}</div>
${tags.length ? `<div class="modal-rest-tags">${tags.map(t => `<span class="t">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
<div class="modal-rest-stats">
<div class="item">${ICONS.star} <b>${money2(r.rating)}</b> 评分</div>
<div class="item">月售 <b>${r.sales}</b></div>
<div class="item">${ICONS.clock} <b>${r.delivery_time}</b> 分钟</div>
<div class="item">起送 <b>¥${money(r.min_order)}</b></div>
<div class="item">配送 <b>¥${money(r.delivery_fee)}</b></div>
</div>
</div>
</div>
${r.notice ? `<div class="modal-notice">${ICONS.megaphone}<span>${escapeHtml(r.notice)}</span></div>` : ''}
`;
if (!items.length) {
$('#menuBody').innerHTML = `<div class="state-box"><div class="ic">${ICONS.empty}</div><h3>暂无菜品</h3><p>该餐厅还未上架菜品</p></div>`;
return;
}
// 按菜品分类分组
const grouped = {};
cats.forEach(c => { grouped[c.id] = { cat: c, items: [] }; });
const other = { cat: { id: 0, name: '全部菜品' }, items: [] };
items.forEach(it => {
if (grouped[it.category_id]) grouped[it.category_id].items.push(it);
else other.items.push(it);
});
const sections = Object.values(grouped).filter(s => s.items.length);
if (other.items.length) sections.push(other);
if (!sections.length) sections.push({ cat: { id: 0, name: '全部菜品' }, items });
// 左侧分类
const leftHtml = sections.map((s, i) => `
<div class="menu-cat${i === 0 ? ' active' : ''}" data-cat="${s.cat.id}" onclick="window._app.scrollMenuTo(${s.cat.id})">
${escapeHtml(s.cat.name)} <span class="cnt">${s.items.length}</span>
</div>`).join('');
// 右侧菜品
const rightHtml = sections.map(s => `
<div class="menu-section" id="sec-${s.cat.id}" data-cat="${s.cat.id}">
<div class="menu-section-title">${escapeHtml(s.cat.name)} <span class="cnt">${s.items.length} 道</span></div>
${s.items.map(it => dishCardHtml(it)).join('')}
</div>`).join('');
$('#menuBody').innerHTML = `
<div class="menu-left" id="menuLeft">${leftHtml}</div>
<div class="menu-right" id="menuRight">${rightHtml}</div>
`;
state.activeMenuCat = sections[0].cat.id;
// 渲染各菜品的购物车数量
refreshDishQties();
// 滚动联动
setupMenuScroll();
}
function dishCardHtml(it) {
const g = gradFor(it.id || it.restaurant_id);
const tags = parseTags(it.tags).slice(0, 2);
const hasOld = it.original_price && Number(it.original_price) > Number(it.price);
return `<div class="dish-card" data-dish="${it.id}">
<div class="dish-img"><div class="grad" style="background:${g.css}"></div>${it.image?`<img src="${escapeHtml(it.image)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('span').style.display=''">`:''}<span style="${it.image?'display:none':''}">${dishIcon(it)}</span></div>
<div class="dish-info">
<div class="dish-name">${escapeHtml(it.name)}</div>
${it.description ? `<div class="dish-desc">${escapeHtml(it.description)}</div>` : ''}
${tags.length ? `<div class="dish-tags">${tags.map(t => `<span class="t">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
<div class="dish-foot">
<div class="dish-price">
<span class="now"><small>¥</small>${money(it.price)}</span>
${hasOld ? `<span class="old">¥${money(it.original_price)}</span>` : ''}
</div>
<div style="display:flex;align-items:center;gap:6px">
<span class="dish-rating">${ICONS.star} ${money2(it.rating)}</span>
<button class="add-btn" data-add="${it.id}" onclick="window._app.addToCart(${it.id}, event)">
${ICONS.plus} 加入
</button>
<div class="qty-stepper" data-stepper="${it.id}">
<button class="qty-btn minus" onclick="window._app.changeQty(${it.id}, -1)">${ICONS.minus}</button>
<span class="qty-num" data-num="${it.id}">0</span>
<button class="qty-btn plus" onclick="window._app.changeQty(${it.id}, 1)">${ICONS.plus}</button>
</div>
</div>
</div>
</div>
</div>`;
}
function refreshDishQties() {
if (!state.menu) return;
const items = state.menu.menu_items || [];
items.forEach(it => {
const inCart = state.cart.find(c => c.id === it.id);
const addBtn = document.querySelector(`[data-add="${it.id}"]`);
const stepper = document.querySelector(`[data-stepper="${it.id}"]`);
const num = document.querySelector(`[data-num="${it.id}"]`);
if (addBtn) addBtn.style.display = inCart ? 'none' : 'inline-flex';
if (stepper) stepper.classList.toggle('show', !!inCart);
if (num && inCart) num.textContent = inCart.qty;
});
}
function setupMenuScroll() {
const right = $('#menuRight');
if (!right) return;
const cats = $$('#menuLeft .menu-cat');
let ticking = false;
right.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const scrollTop = right.scrollTop;
const sections = $$('#menuRight .menu-section');
let current = sections[0] ? sections[0].dataset.cat : null;
for (const sec of sections) {
if (sec.offsetTop - 40 <= scrollTop) current = sec.dataset.cat;
}
if (current && current !== String(state.activeMenuCat)) {
state.activeMenuCat = current;
cats.forEach(c => c.classList.toggle('active', c.dataset.cat === current));
// 让激活的分类在左侧可见
const activeEl = $('#menuLeft .menu-cat.active');
if (activeEl && activeEl.scrollIntoView) {
const left = $('#menuLeft');
const isHorizontal = window.matchMedia('(max-width:720px)').matches;
if (isHorizontal) {
activeEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
} else {
left.scrollTop = activeEl.offsetTop - left.clientHeight / 2 + activeEl.clientHeight / 2;
}
}
}
ticking = false;
});
});
}
function scrollMenuTo(catId) {
const right = $('#menuRight');
const sec = document.querySelector(`#menuRight .menu-section[data-cat="${catId}"]`);
if (!right || !sec) return;
right.scrollTo({ top: sec.offsetTop - 8, behavior: 'smooth' });
state.activeMenuCat = catId;
$$('#menuLeft .menu-cat').forEach(c => c.classList.toggle('active', c.dataset.cat === String(catId)));
}
function closeModal() {
$('#modalMask').classList.remove('show');
document.body.style.overflow = '';
}
/* ---------- 购物车 ---------- */
function findDish(id) {
if (!state.menu) return null;
return (state.menu.menu_items || []).find(i => i.id === id);
}
function addToCart(id, ev) {
if (ev) ev.stopPropagation();
const dish = findDish(id);
if (!dish) return;
const rest = state.menu.restaurant || {};
// 切换餐厅时清空
if (state.cartRestId && state.cartRestId !== rest.id && state.cart.length) {
state.cart = [];
state.cartRestId = rest.id;
toast('已切换餐厅,购物车已更新', 'warn');
} else {
state.cartRestId = rest.id;
}
const exist = state.cart.find(c => c.id === id);
if (exist) exist.qty++;
else state.cart.push({ id, name: dish.name, price: Number(dish.price), qty: 1, restaurant_id: rest.id, restaurant_name: rest.name });
refreshDishQties();
renderCart();
// 飞入动画
flyToCart(ev);
toast(`已加入「${dish.name}」`);
}
function changeQty(id, delta) {
const item = state.cart.find(c => c.id === id);
if (!item) return;
item.qty += delta;
if (item.qty <= 0) {
state.cart = state.cart.filter(c => c.id !== id);
if (!state.cart.length) state.cartRestId = null;
}
refreshDishQties();
renderCart();
}
function clearCart() {
if (!state.cart.length) return;
state.cart = [];
state.cartRestId = null;
refreshDishQties();
renderCart();
toast('购物车已清空');
}
function cartCount() { return state.cart.reduce((s, c) => s + c.qty, 0); }
function cartTotal() { return state.cart.reduce((s, c) => s + c.price * c.qty, 0); }
function renderCart() {
const count = cartCount();
const total = cartTotal();
// 角标
$('#cartBadge').textContent = count;
$('#cartBadge').classList.toggle('show', count > 0);
$('#cartBarBadge').textContent = count;
$('#cartBarBadge').classList.toggle('show', count > 0);
// 浮层
const bar = $('#cartBar');
if (count > 0) {
bar.classList.add('show');
$('#cartBarTotal').textContent = money(total);
const rest = state.cart[0] ? state.cart[0].restaurant_name : '';
// 起送价提示
const minOrder = state.menu && state.menu.restaurant ? Number(state.menu.restaurant.min_order) || 0 : 0;
const useMin = state.cartRestId === (state.menu && state.menu.restaurant && state.menu.restaurant.id) ? minOrder : 0;
if (useMin && total < useMin) {
$('#cartBarSub').textContent = `还差 ¥${money(useMin - total)} 起送`;
$('#cartBarSub').classList.add('warn');
$('#cartCheckoutBtn').disabled = true;
} else {
$('#cartBarSub').textContent = rest ? `来自「${rest}」· 另需配送费` : '已满起送价';
$('#cartBarSub').classList.remove('warn');
$('#cartCheckoutBtn').disabled = false;
}
} else {
bar.classList.remove('show');
$('#cartBarSub').textContent = '购物车是空的';
$('#cartBarSub').classList.remove('warn');
$('#cartCheckoutBtn').disabled = true;
}
// 抽屉
renderCartDrawer(total, count);
}
function renderCartDrawer(total, count) {
$('#drawerCount').textContent = count ? `· ${count} 件` : '';
const body = $('#cartDrawerBody');
if (!state.cart.length) {
body.innerHTML = `<div class="cart-empty"><div class="ic">${ICONS.empty}</div><p>购物车空空如也<br>快去挑选心仪美食吧</p></div>`;
$('#cartDrawerFoot').innerHTML = `<button class="cart-drawer-checkout" disabled>去结算</button>`;
return;
}
body.innerHTML = state.cart.map(c => {
const g = gradFor(c.id || c.restaurant_id);
const dish = findDish(c.id);
const dishImg = dish ? dish.image : '';
return `<div class="cart-item">
<div class="cart-item-img">
<div class="grad" style="background:${g.css}"></div>
${dishImg?`<img src="${escapeHtml(dishImg)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('span').style.display=''">`:''}
<span style="${dishImg?'display:none':''}">${DISH_ICONS.default}</span>
</div>
<div class="cart-item-info">
<div class="cart-item-name">${escapeHtml(c.name)}</div>
<div class="cart-item-price">¥${money(c.price)} / 份</div>
</div>
<div class="cart-item-stepper">
<button class="qty-btn minus" onclick="window._app.changeQty(${c.id}, -1)">${ICONS.minus}</button>
<span class="qty-num">${c.qty}</span>
<button class="qty-btn plus" onclick="window._app.changeQty(${c.id}, 1)">${ICONS.plus}</button>
</div>
</div>`;
}).join('');
const minOrder = state.menu && state.menu.restaurant && state.cartRestId === state.menu.restaurant.id ? Number(state.menu.restaurant.min_order) || 0 : 0;
const deliveryFee = state.menu && state.menu.restaurant && state.cartRestId === state.menu.restaurant.id ? Number(state.menu.restaurant.delivery_fee) || 0 : 0;
const meetMin = total >= minOrder;
const footNote = minOrder && !meetMin ? `<div style="font-size:12px;color:#E67E22;margin-bottom:10px;text-align:center">还差 ¥${money(minOrder - total)} 满起送价</div>` : '';
$('#cartDrawerFoot').innerHTML = `
${footNote}
<div class="cart-drawer-total">
<span class="lbl">合计 (含配送费 ¥${money(deliveryFee)})</span>
<span class="val"><small>¥</small>${money(total + deliveryFee)}</span>
</div>
<button class="cart-drawer-checkout" ${meetMin ? '' : 'disabled'} onclick="window._app.checkout()">去结算</button>
`;
}
function checkout() {
if (!state.cart.length) return;
const total = cartTotal();
toast(`下单成功!共 ¥${money(total)},预计 30 分钟送达`);
state.cart = [];
state.cartRestId = null;
refreshDishQties();
renderCart();
closeCartDrawer();
}
/* 飞入购物车动画 */
function flyToCart(ev) {
if (!ev) return;
const from = ev.currentTarget || ev.target;
if (!from.getBoundingClientRect) return;
const cartIcon = $('#cartBtn');
const r1 = from.getBoundingClientRect();
const r2 = cartIcon.getBoundingClientRect();
const dot = document.createElement('div');
dot.style.cssText = `position:fixed;z-index:250;width:18px;height:18px;border-radius:50%;background:${gradFor(0).css};left:${r1.left + r1.width/2}px;top:${r1.top}px;pointer-events:none;transition:all .6s cubic-bezier(.5,-0.3,.7,1);box-shadow:0 4px 10px rgba(45,154,108,0.4)`;
document.body.appendChild(dot);
requestAnimationFrame(() => {
dot.style.left = (r2.left + r2.width/2) + 'px';
dot.style.top = (r2.top + r2.height/2) + 'px';
dot.style.transform = 'scale(0.3)';
dot.style.opacity = '0.2';
});
setTimeout(() => dot.remove(), 620);
}
/* ---------- 购物车抽屉开关 ---------- */
function openCartDrawer() {
if (!state.cart.length) { toast('购物车还是空的', 'warn'); return; }
$('#cartDrawerMask').classList.add('show');
$('#cartDrawer').classList.add('show');
}
function closeCartDrawer() {
$('#cartDrawerMask').classList.remove('show');
$('#cartDrawer').classList.remove('show');
}
/* ---------- 滚动定位 ---------- */
function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); }
function scrollToCategories() { $('#categoriesSection').scrollIntoView({ behavior: 'smooth' }); }
function scrollToRestaurants() {
const el = $('#restaurantsSection');
const y = el.getBoundingClientRect().top + window.scrollY - 70;
window.scrollTo({ top: y, behavior: 'smooth' });
}
/* ---------- 事件绑定 ---------- */
function bindEvents() {
$('#searchBtn').addEventListener('click', doSearch);
$('#searchInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); });
// 搜索输入实时清空时自动刷新
let searchTimer = null;
$('#searchInput').addEventListener('input', (e) => {
clearTimeout(searchTimer);
const v = e.target.value.trim();
searchTimer = setTimeout(() => {
if (v === '' && state.keyword !== '') { state.keyword = ''; loadRestaurants(true); }
}, 400);
});
$$('#sortGroup .sort-btn').forEach(b => b.addEventListener('click', () => setSort(b.dataset.sort)));
$('#chipClear').addEventListener('click', clearFilters);
$('#loadMoreBtn').addEventListener('click', loadMore);
// 模态
$('#modalMask').addEventListener('click', (e) => { if (e.target === $('#modalMask')) closeModal(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(); closeCartDrawer(); } });
// 购物车
$('#cartBtn').addEventListener('click', openCartDrawer);
$('#cartBarIcon').addEventListener('click', openCartDrawer);
$('#cartBarText').addEventListener('click', openCartDrawer);
$('#cartCheckoutBtn').addEventListener('click', checkout);
$('#cartClearBtn').addEventListener('click', clearCart);
$('#cartDrawerMask').addEventListener('click', closeCartDrawer);
}
/* ---------- 暴露 ---------- */
window._app = {
selectCat, loadHome, loadRestaurants, openRestaurant, closeModal,
scrollMenuTo, addToCart, changeQty, checkout
};
window.scrollToTop = scrollToTop;
window.scrollToCategories = scrollToCategories;
window.scrollToRestaurants = scrollToRestaurants;
/* ---------- 启动 ---------- */
function init() {
initStaticIcons();
bindEvents();
renderCart();
loadHome();
loadRestaurants(true);
}
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', init)
: init();
})();
</script>
</body>
</html>