1784 lines
88 KiB
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 => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[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>
|