1143 lines
58 KiB
HTML
1143 lines
58 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Gourmet Delivery · 精致美食外送</title>
|
||
<style>
|
||
:root{
|
||
--bg-darkest:#0F0F12;
|
||
--bg-dark:#1A1A1E;
|
||
--bg-card:#252530;
|
||
--bg-card-2:#2A2A38;
|
||
--bg-elev:#2F2F3E;
|
||
--gold:#D4AF37;
|
||
--gold-light:#E8C766;
|
||
--gold-bright:#F0D878;
|
||
--gold-dim:#8B7423;
|
||
--text:#F5F2EA;
|
||
--text-2:#B8B5AD;
|
||
--text-3:#7A7770;
|
||
--line:rgba(212,175,55,.14);
|
||
--line-strong:rgba(212,175,55,.42);
|
||
--glass:rgba(26,26,30,.72);
|
||
--glass-2:rgba(37,37,48,.78);
|
||
--radius:16px;
|
||
--shadow:0 18px 50px -20px rgba(0,0,0,.7);
|
||
--serif:Georgia,'Times New Roman','Songti SC',serif;
|
||
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||
}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
html{scroll-behavior:smooth}
|
||
body{
|
||
font-family:var(--sans);
|
||
background:var(--bg-darkest);
|
||
color:var(--text);
|
||
line-height:1.6;
|
||
-webkit-font-smoothing:antialiased;
|
||
overflow-x:hidden;
|
||
}
|
||
/* ambient gold glow background */
|
||
body::before{
|
||
content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
|
||
background:
|
||
radial-gradient(900px 600px at 78% -8%, rgba(212,175,55,.12), transparent 60%),
|
||
radial-gradient(700px 500px at 8% 18%, rgba(212,175,55,.06), transparent 55%),
|
||
radial-gradient(800px 700px at 50% 120%, rgba(212,175,55,.05), transparent 60%);
|
||
}
|
||
body::after{
|
||
content:"";position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.4;
|
||
background-image:linear-gradient(rgba(212,175,55,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(212,175,55,.025) 1px,transparent 1px);
|
||
background-size:60px 60px;
|
||
mask-image:radial-gradient(ellipse at 50% 0%,#000,transparent 70%);
|
||
}
|
||
a{color:inherit;text-decoration:none}
|
||
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
|
||
::selection{background:rgba(212,175,55,.3);color:var(--gold-bright)}
|
||
::-webkit-scrollbar{width:9px;height:9px}
|
||
::-webkit-scrollbar-track{background:var(--bg-dark)}
|
||
::-webkit-scrollbar-thumb{background:var(--gold-dim);border-radius:10px}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--gold)}
|
||
.wrap{max-width:1240px;margin:0 auto;padding:0 24px;position:relative;z-index:1}
|
||
|
||
/* ===== NAVBAR ===== */
|
||
.nav{
|
||
position:fixed;top:0;left:0;right:0;z-index:80;
|
||
background:var(--glass);
|
||
backdrop-filter:blur(18px) saturate(140%);
|
||
-webkit-backdrop-filter:blur(18px) saturate(140%);
|
||
border-bottom:1px solid var(--line);
|
||
}
|
||
.nav-inner{display:flex;align-items:center;gap:24px;height:68px}
|
||
.brand{display:flex;align-items:baseline;gap:8px;flex-shrink:0}
|
||
.brand-main{
|
||
font-family:var(--serif);font-weight:700;font-size:23px;letter-spacing:.06em;
|
||
color:var(--gold);
|
||
background:linear-gradient(135deg,var(--gold-bright),var(--gold),var(--gold-dim));
|
||
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
|
||
}
|
||
.brand-sub{font-size:10px;letter-spacing:.32em;color:var(--text-3);text-transform:uppercase;font-weight:600}
|
||
.nav-search{flex:1;max-width:520px;position:relative}
|
||
.nav-search input{
|
||
width:100%;height:42px;padding:0 16px 0 44px;
|
||
background:rgba(15,15,18,.6);
|
||
border:1px solid var(--line);
|
||
border-radius:30px;color:var(--text);font-size:14px;outline:none;
|
||
transition:.25s;
|
||
}
|
||
.nav-search input::placeholder{color:var(--text-3)}
|
||
.nav-search input:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(212,175,55,.12);background:rgba(15,15,18,.85)}
|
||
.nav-search svg{position:absolute;left:15px;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--gold-dim)}
|
||
.nav-actions{display:flex;align-items:center;gap:8px;margin-left:auto}
|
||
.icon-btn{
|
||
position:relative;width:42px;height:42px;border-radius:50%;
|
||
display:flex;align-items:center;justify-content:center;
|
||
border:1px solid var(--line);background:rgba(15,15,18,.5);
|
||
transition:.25s;color:var(--text-2);
|
||
}
|
||
.icon-btn:hover{border-color:var(--gold);color:var(--gold);transform:translateY(-1px)}
|
||
.icon-btn svg{width:20px;height:20px}
|
||
.cart-badge{
|
||
position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;padding:0 5px;
|
||
background:linear-gradient(135deg,var(--gold-bright),var(--gold-dim));
|
||
color:#1A1A1E;font-size:11px;font-weight:800;border-radius:10px;
|
||
display:flex;align-items:center;justify-content:center;
|
||
box-shadow:0 0 0 2px var(--bg-dark);
|
||
transform:scale(0);transition:.25s cubic-bezier(.34,1.56,.64,1);
|
||
}
|
||
.cart-badge.show{transform:scale(1)}
|
||
|
||
/* ===== HERO ===== */
|
||
.hero{position:relative;padding:128px 0 56px;overflow:hidden}
|
||
.hero-deco{position:absolute;inset:0;z-index:0;pointer-events:none}
|
||
.hero-glow{position:absolute;width:560px;height:560px;border-radius:50%;top:-180px;right:-120px;
|
||
background:radial-gradient(circle,rgba(212,175,55,.22),transparent 65%);filter:blur(20px)}
|
||
.hero-glow.b{left:-160px;top:120px;right:auto;width:420px;height:420px;background:radial-gradient(circle,rgba(212,175,55,.1),transparent 65%)}
|
||
.hero-inner{position:relative;z-index:1;text-align:center;max-width:820px;margin:0 auto}
|
||
.hero-eyebrow{
|
||
display:inline-flex;align-items:center;gap:10px;
|
||
font-size:11px;letter-spacing:.34em;text-transform:uppercase;color:var(--gold);
|
||
border:1px solid var(--line-strong);border-radius:30px;padding:7px 18px;margin-bottom:28px;
|
||
background:rgba(212,175,55,.05);
|
||
}
|
||
.hero-eyebrow::before,.hero-eyebrow::after{content:"";width:18px;height:1px;background:linear-gradient(90deg,transparent,var(--gold))}
|
||
.hero h1{
|
||
font-family:var(--serif);font-weight:700;font-size:clamp(40px,6vw,76px);line-height:1.05;
|
||
letter-spacing:.01em;margin-bottom:24px;
|
||
}
|
||
.hero h1 .accent{
|
||
background:linear-gradient(120deg,var(--gold-bright) 10%,var(--gold) 50%,var(--gold-dim) 90%);
|
||
-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
|
||
font-style:italic;
|
||
}
|
||
.hero p{font-size:17px;color:var(--text-2);max-width:560px;margin:0 auto 38px}
|
||
.hero-cta{display:flex;gap:16px;justify-content:center;flex-wrap:wrap}
|
||
.btn{
|
||
display:inline-flex;align-items:center;gap:9px;
|
||
padding:15px 32px;border-radius:30px;font-size:14px;font-weight:600;letter-spacing:.04em;
|
||
transition:.28s;cursor:pointer;border:1px solid transparent;
|
||
}
|
||
.btn-gold{
|
||
background:linear-gradient(135deg,var(--gold-bright),var(--gold) 55%,var(--gold-dim));
|
||
color:#15151A;box-shadow:0 10px 30px -10px rgba(212,175,55,.5);
|
||
}
|
||
.btn-gold:hover{transform:translateY(-2px);box-shadow:0 16px 40px -12px rgba(212,175,55,.65)}
|
||
.btn-outline{border-color:var(--line-strong);color:var(--gold);background:rgba(212,175,55,.04)}
|
||
.btn-outline:hover{border-color:var(--gold);background:rgba(212,175,55,.12);transform:translateY(-2px)}
|
||
.hero-stats{display:flex;gap:48px;justify-content:center;margin-top:54px;flex-wrap:wrap}
|
||
.hero-stat{text-align:center}
|
||
.hero-stat .num{font-family:var(--serif);font-size:30px;color:var(--gold);font-weight:700}
|
||
.hero-stat .lbl{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--text-3);margin-top:2px}
|
||
|
||
/* ===== SECTION ===== */
|
||
section{padding:56px 0;position:relative;z-index:1}
|
||
.sec-head{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:32px;gap:20px;flex-wrap:wrap}
|
||
.sec-title{font-family:var(--serif);font-size:30px;font-weight:700;letter-spacing:.02em}
|
||
.sec-title .gold{color:var(--gold)}
|
||
.sec-sub{font-size:13px;color:var(--text-3);letter-spacing:.18em;text-transform:uppercase;margin-top:6px}
|
||
.sec-line{flex:1;height:1px;background:linear-gradient(90deg,var(--line-strong),transparent);margin:0 20px 14px;min-width:60px}
|
||
|
||
/* ===== CATEGORIES ===== */
|
||
.cat-bar{display:flex;gap:12px;overflow-x:auto;padding-bottom:10px;scrollbar-width:thin}
|
||
.cat-bar::-webkit-scrollbar{height:0}
|
||
.cat-pill{
|
||
flex-shrink:0;display:flex;align-items:center;gap:9px;
|
||
padding:11px 20px;border-radius:30px;font-size:14px;font-weight:500;
|
||
background:var(--bg-card);border:1px solid var(--line);color:var(--text-2);
|
||
transition:.25s;white-space:nowrap;
|
||
}
|
||
.cat-pill:hover{border-color:var(--line-strong);color:var(--text);transform:translateY(-1px)}
|
||
.cat-pill.active{
|
||
border-color:var(--gold);color:var(--gold);
|
||
background:linear-gradient(135deg,rgba(212,175,55,.16),rgba(212,175,55,.04));
|
||
box-shadow:0 0 0 1px var(--gold) inset,0 8px 24px -10px rgba(212,175,55,.4);
|
||
}
|
||
.cat-pill svg{width:18px;height:18px}
|
||
|
||
/* ===== FEATURED SCROLL ===== */
|
||
.feat-track{display:flex;gap:20px;overflow-x:auto;padding:6px 4px 16px;scroll-snap-type:x mandatory;scrollbar-width:thin}
|
||
.feat-card{
|
||
flex:0 0 320px;scroll-snap-align:start;
|
||
background:var(--bg-card);border:1px solid var(--line);border-radius:var(--radius);
|
||
overflow:hidden;transition:.3s;cursor:pointer;
|
||
}
|
||
.feat-card:hover{border-color:var(--gold);transform:translateY(-6px);box-shadow:0 24px 50px -24px rgba(212,175,55,.3)}
|
||
.feat-cover{height:150px;position:relative;overflow:hidden}
|
||
.feat-cover .grad{position:absolute;inset:0}
|
||
.feat-cover .ico{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:.85}
|
||
.feat-cover .ico svg{width:64px;height:64px;color:rgba(255,255,255,.5)}
|
||
.feat-badge{position:absolute;top:12px;left:12px;font-family:var(--serif);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--gold);background:rgba(15,15,18,.7);border:1px solid var(--line-strong);padding:5px 12px;border-radius:20px;backdrop-filter:blur(8px)}
|
||
.feat-body{padding:18px 20px 20px}
|
||
.feat-name{font-family:var(--serif);font-size:19px;font-weight:700;margin-bottom:4px;line-height:1.3}
|
||
.feat-cat{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:12px}
|
||
.feat-meta{display:flex;align-items:center;gap:14px;font-size:13px;color:var(--text-2);padding-top:12px;border-top:1px solid var(--line)}
|
||
.feat-meta .star{color:var(--gold);font-weight:700}
|
||
|
||
/* ===== TOOLBAR ===== */
|
||
.toolbar{display:flex;align-items:center;gap:14px;margin-bottom:24px;flex-wrap:wrap}
|
||
.toolbar .count{font-size:13px;color:var(--text-3)}
|
||
.toolbar .count b{color:var(--gold);font-family:var(--serif);font-size:16px}
|
||
.sort-group{display:flex;gap:6px;margin-left:auto;background:var(--bg-card);border:1px solid var(--line);border-radius:30px;padding:5px}
|
||
.sort-btn{padding:8px 16px;border-radius:24px;font-size:13px;color:var(--text-2);transition:.22s;font-weight:500}
|
||
.sort-btn:hover{color:var(--text)}
|
||
.sort-btn.active{background:linear-gradient(135deg,var(--gold-bright),var(--gold-dim));color:#15151A;font-weight:600}
|
||
|
||
/* ===== RESTAURANT LIST ===== */
|
||
.r-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:20px}
|
||
.r-card{
|
||
display:flex;gap:0;background:var(--bg-card);border:1px solid var(--line);border-radius:var(--radius);
|
||
overflow:hidden;transition:.3s;cursor:pointer;position:relative;
|
||
}
|
||
.r-card::before{content:"";position:absolute;inset:0;border-radius:var(--radius);pointer-events:none;box-shadow:0 0 0 1px transparent inset;transition:.3s}
|
||
.r-card:hover{transform:translateY(-5px);border-color:var(--gold);box-shadow:0 26px 55px -26px rgba(212,175,55,.35)}
|
||
.r-logo{
|
||
flex:0 0 116px;position:relative;display:flex;align-items:center;justify-content:center;
|
||
border-right:1px solid var(--line);
|
||
}
|
||
.r-logo .grad{position:absolute;inset:0}
|
||
.r-logo .ico{position:relative;z-index:1;width:46px;height:46px;color:rgba(255,255,255,.55)}
|
||
.r-logo .gframe{position:absolute;inset:8px;border:1px solid rgba(212,175,55,.3);border-radius:10px;pointer-events:none}
|
||
.r-info{flex:1;padding:16px 18px;display:flex;flex-direction:column;min-width:0}
|
||
.r-top{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:4px}
|
||
.r-name{font-family:var(--serif);font-size:18px;font-weight:700;line-height:1.3}
|
||
.r-rating{display:flex;align-items:center;gap:4px;font-size:13px;color:var(--gold);font-weight:700;flex-shrink:0;font-family:var(--serif)}
|
||
.r-rating svg{width:14px;height:14px}
|
||
.r-cat{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:10px}
|
||
.r-tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
|
||
.r-tag{font-size:11px;padding:3px 9px;border-radius:12px;background:rgba(212,175,55,.08);border:1px solid var(--line);color:var(--text-2)}
|
||
.r-foot{display:flex;gap:16px;font-size:12px;color:var(--text-3);margin-top:auto;padding-top:12px;border-top:1px solid var(--line);flex-wrap:wrap}
|
||
.r-foot span{display:inline-flex;align-items:center;gap:4px}
|
||
.r-foot svg{width:13px;height:13px;color:var(--gold-dim)}
|
||
.r-status{display:inline-block;width:6px;height:6px;border-radius:50%;background:#4ade80;box-shadow:0 0 6px #4ade80;margin-right:2px}
|
||
|
||
/* ===== PAGINATION ===== */
|
||
.pager{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:40px}
|
||
.pg-btn{min-width:40px;height:40px;padding:0 12px;border-radius:10px;border:1px solid var(--line);background:var(--bg-card);color:var(--text-2);font-size:14px;transition:.22s;display:flex;align-items:center;justify-content:center}
|
||
.pg-btn:hover:not(:disabled){border-color:var(--gold);color:var(--gold)}
|
||
.pg-btn.active{background:linear-gradient(135deg,var(--gold-bright),var(--gold-dim));color:#15151A;border-color:transparent;font-weight:700}
|
||
.pg-btn:disabled{opacity:.35;cursor:not-allowed}
|
||
|
||
/* ===== SKELETON ===== */
|
||
.sk{position:relative;overflow:hidden;background:var(--bg-card);border:1px solid var(--line);border-radius:var(--radius)}
|
||
.sk::after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,rgba(212,175,55,.06),transparent);animation:shimmer 1.5s infinite}
|
||
@keyframes shimmer{100%{transform:translateX(100%)}}
|
||
.sk-card{height:130px;display:flex;overflow:hidden}
|
||
.sk-block{flex:0 0 116px;background:var(--bg-card-2)}
|
||
.sk-lines{flex:1;padding:18px;display:flex;flex-direction:column;gap:10px}
|
||
.sk-l{height:13px;border-radius:6px;background:var(--bg-card-2)}
|
||
.sk-l.w60{width:60%}.sk-l.w40{width:40%}.sk-l.w80{width:80%}
|
||
.sk-feat{flex:0 0 320px;height:240px;border-radius:var(--radius)}
|
||
|
||
/* ===== MODAL ===== */
|
||
.modal{
|
||
position:fixed;inset:0;z-index:200;display:none;
|
||
background:rgba(8,8,10,.78);backdrop-filter:blur(14px);
|
||
opacity:0;transition:opacity .3s;
|
||
}
|
||
.modal.open{display:flex;opacity:1}
|
||
.modal-box{
|
||
width:100%;max-width:1100px;height:92vh;margin:auto;
|
||
background:linear-gradient(180deg,var(--bg-dark),var(--bg-darkest));
|
||
border:1px solid var(--line-strong);border-radius:20px;overflow:hidden;
|
||
display:flex;flex-direction:column;
|
||
transform:translateY(20px) scale(.98);transition:transform .3s cubic-bezier(.2,.8,.2,1);
|
||
box-shadow:0 40px 100px -30px rgba(0,0,0,.8);
|
||
}
|
||
.modal.open .modal-box{transform:translateY(0) scale(1)}
|
||
.modal-close{position:absolute;top:18px;right:18px;z-index:10;width:40px;height:40px;border-radius:50%;background:rgba(15,15,18,.8);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;color:var(--text-2);transition:.22s}
|
||
.modal-close:hover{border-color:var(--gold);color:var(--gold);transform:rotate(90deg)}
|
||
.modal-close svg{width:20px;height:20px}
|
||
|
||
.m-header{position:relative;padding:30px 36px 24px;border-bottom:1px solid var(--line);flex-shrink:0}
|
||
.m-header .gold-rule{width:48px;height:2px;background:linear-gradient(90deg,var(--gold),transparent);margin-bottom:16px}
|
||
.m-h-top{display:flex;gap:20px;align-items:flex-start}
|
||
.m-logo{flex:0 0 84px;height:84px;border-radius:14px;position:relative;display:flex;align-items:center;justify-content:center;border:1px solid var(--line-strong)}
|
||
.m-logo .grad{position:absolute;inset:0;border-radius:13px}
|
||
.m-logo .ico{position:relative;z-index:1;width:40px;height:40px;color:rgba(255,255,255,.6)}
|
||
.m-h-info{flex:1;min-width:0}
|
||
.m-name{font-family:var(--serif);font-size:26px;font-weight:700;margin-bottom:4px}
|
||
.m-cat{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);margin-bottom:12px}
|
||
.m-tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
|
||
.m-notice{font-size:13px;color:var(--text-2);background:rgba(212,175,55,.05);border-left:2px solid var(--gold);padding:8px 14px;border-radius:0 8px 8px 0;margin-top:6px}
|
||
.m-stats{display:flex;gap:28px;flex-wrap:wrap;margin-top:14px}
|
||
.m-stat{font-size:13px;color:var(--text-2)}
|
||
.m-stat b{font-family:var(--serif);color:var(--gold);font-size:16px;margin-right:3px}
|
||
|
||
.m-body{flex:1;display:flex;overflow:hidden;min-height:0}
|
||
.m-cats{flex:0 0 168px;overflow-y:auto;border-right:1px solid var(--line);padding:14px 0;background:rgba(15,15,18,.3)}
|
||
.m-cat-item{padding:13px 22px;font-size:14px;color:var(--text-2);cursor:pointer;transition:.2s;border-left:2px solid transparent;position:relative}
|
||
.m-cat-item:hover{color:var(--text);background:rgba(212,175,55,.04)}
|
||
.m-cat-item.active{color:var(--gold);border-left-color:var(--gold);background:linear-gradient(90deg,rgba(212,175,55,.1),transparent);font-weight:600}
|
||
.m-menus{flex:1;overflow-y:auto;padding:8px 28px 120px;scroll-behavior:smooth}
|
||
.m-group{margin-top:22px}
|
||
.m-group:first-child{margin-top:8px}
|
||
.m-group-title{font-family:var(--serif);font-size:18px;font-weight:700;padding:8px 0 14px;position:sticky;top:0;background:linear-gradient(180deg,var(--bg-dark) 70%,transparent);z-index:2;display:flex;align-items:center;gap:10px}
|
||
.m-group-title::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--line-strong),transparent)}
|
||
.m-item{display:flex;gap:16px;padding:16px 0;border-bottom:1px solid var(--line)}
|
||
.m-item:last-child{border-bottom:none}
|
||
.m-item-img{flex:0 0 88px;height:88px;border-radius:12px;position:relative;overflow:hidden;border:1px solid var(--line)}
|
||
.m-item-img .grad{position:absolute;inset:0}
|
||
.m-item-img .ico{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
|
||
.m-item-img .ico svg{width:36px;height:36px;color:rgba(255,255,255,.4)}
|
||
.m-item-info{flex:1;min-width:0}
|
||
.m-item-name{font-size:16px;font-weight:600;margin-bottom:4px}
|
||
.m-item-desc{font-size:12px;color:var(--text-3);line-height:1.55;margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.m-item-meta{display:flex;gap:12px;font-size:12px;color:var(--text-3);margin-bottom:8px;flex-wrap:wrap}
|
||
.m-item-meta .star{color:var(--gold)}
|
||
.m-item-price{font-family:var(--serif);color:var(--gold);font-size:20px;font-weight:700}
|
||
.m-item-price .cur{font-size:13px}
|
||
.m-item-price .orig{font-size:13px;color:var(--text-3);text-decoration:line-through;font-family:var(--sans);font-weight:400;margin-left:6px}
|
||
.m-item-right{display:flex;flex-direction:column;justify-content:space-between;align-items:flex-end}
|
||
.m-add{
|
||
width:34px;height:34px;border-radius:50%;
|
||
background:linear-gradient(135deg,var(--gold-bright),var(--gold-dim));
|
||
color:#15151A;display:flex;align-items:center;justify-content:center;transition:.22s;
|
||
box-shadow:0 6px 16px -6px rgba(212,175,55,.5);
|
||
}
|
||
.m-add:hover{transform:scale(1.12) rotate(90deg)}
|
||
.m-add svg{width:18px;height:18px;stroke-width:3}
|
||
.m-qty{display:flex;align-items:center;gap:8px}
|
||
.m-qty button{width:30px;height:30px;border-radius:50%;border:1px solid var(--line-strong);color:var(--gold);font-size:18px;display:flex;align-items:center;justify-content:center;transition:.2s}
|
||
.m-qty button:hover{border-color:var(--gold);background:rgba(212,175,55,.12)}
|
||
.m-qty span{font-family:var(--serif);font-weight:700;color:var(--text);min-width:18px;text-align:center}
|
||
|
||
/* ===== CART BAR ===== */
|
||
.cart-bar{
|
||
position:fixed;left:0;right:0;bottom:0;z-index:150;
|
||
background:var(--glass-2);backdrop-filter:blur(20px) saturate(150%);
|
||
border-top:1px solid var(--line-strong);
|
||
transform:translateY(110%);transition:transform .35s cubic-bezier(.2,.8,.2,1);
|
||
}
|
||
.cart-bar.show{transform:translateY(0)}
|
||
.cart-inner{max-width:1240px;margin:0 auto;padding:14px 24px;display:flex;align-items:center;gap:24px}
|
||
.cart-icon{position:relative;width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,var(--gold-bright),var(--gold-dim));display:flex;align-items:center;justify-content:center;color:#15151A;flex-shrink:0;box-shadow:0 8px 20px -6px rgba(212,175,55,.5)}
|
||
.cart-icon svg{width:26px;height:26px}
|
||
.cart-count{position:absolute;top:-6px;right:-6px;min-width:22px;height:22px;background:var(--bg-darkest);color:var(--gold);border:1px solid var(--gold);border-radius:11px;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 5px}
|
||
.cart-detail{flex:1;min-width:0}
|
||
.cart-total{font-family:var(--serif);font-size:24px;font-weight:700;color:var(--gold)}
|
||
.cart-total .cur{font-size:14px}
|
||
.cart-sub{font-size:12px;color:var(--text-3)}
|
||
.cart-delivery{font-size:12px;color:var(--text-2)}
|
||
.cart-checkout{padding:14px 36px;border-radius:30px;background:linear-gradient(135deg,var(--gold-bright),var(--gold) 55%,var(--gold-dim));color:#15151A;font-weight:700;font-size:15px;letter-spacing:.04em;transition:.25s;flex-shrink:0;box-shadow:0 10px 28px -10px rgba(212,175,55,.5)}
|
||
.cart-checkout:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 16px 36px -12px rgba(212,175,55,.65)}
|
||
.cart-checkout:disabled{opacity:.4;cursor:not-allowed;background:var(--bg-elev);color:var(--text-3);box-shadow:none}
|
||
.cart-clear{font-size:12px;color:var(--text-3);transition:.2s;flex-shrink:0}
|
||
.cart-clear:hover{color:var(--gold)}
|
||
|
||
/* cart drawer */
|
||
.cart-drawer{position:fixed;right:0;top:0;bottom:0;width:380px;max-width:90vw;z-index:210;background:var(--bg-dark);border-left:1px solid var(--line-strong);transform:translateX(100%);transition:transform .35s cubic-bezier(.2,.8,.2,1);display:flex;flex-direction:column;box-shadow:-20px 0 60px -20px rgba(0,0,0,.6)}
|
||
.cart-drawer.open{transform:translateX(0)}
|
||
.cd-head{padding:24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}
|
||
.cd-head h3{font-family:var(--serif);font-size:22px}
|
||
.cd-close{width:34px;height:34px;border-radius:50%;border:1px solid var(--line);display:flex;align-items:center;justify-content:center;color:var(--text-2);transition:.2s}
|
||
.cd-close:hover{color:var(--gold);border-color:var(--gold)}
|
||
.cd-body{flex:1;overflow-y:auto;padding:8px 0}
|
||
.cd-empty{text-align:center;padding:60px 20px;color:var(--text-3)}
|
||
.cd-empty svg{width:60px;height:60px;color:var(--gold-dim);opacity:.4;margin-bottom:16px}
|
||
.cd-group{padding:14px 24px;border-bottom:1px solid var(--line)}
|
||
.cd-group-name{font-size:12px;color:var(--gold);letter-spacing:.1em;margin-bottom:8px;font-family:var(--serif)}
|
||
.cd-item{display:flex;gap:12px;align-items:center;padding:10px 0}
|
||
.cd-item-info{flex:1;min-width:0}
|
||
.cd-item-name{font-size:14px;font-weight:600;margin-bottom:3px}
|
||
.cd-item-price{font-size:13px;color:var(--gold);font-family:var(--serif)}
|
||
.cd-qty{display:flex;align-items:center;gap:8px}
|
||
.cd-qty button{width:26px;height:26px;border-radius:50%;border:1px solid var(--line-strong);color:var(--gold);font-size:16px;display:flex;align-items:center;justify-content:center}
|
||
.cd-qty button:hover{border-color:var(--gold);background:rgba(212,175,55,.12)}
|
||
.cd-qty span{font-family:var(--serif);font-weight:700;min-width:20px;text-align:center}
|
||
.cd-foot{padding:20px 24px;border-top:1px solid var(--line-strong);background:rgba(15,15,18,.4)}
|
||
.cd-row{display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px;color:var(--text-2)}
|
||
.cd-row.total{font-size:16px;color:var(--text);margin-top:10px;padding-top:10px;border-top:1px solid var(--line)}
|
||
.cd-row.total b{font-family:var(--serif);color:var(--gold);font-size:22px}
|
||
.cd-checkout{width:100%;margin-top:14px;padding:14px;border-radius:30px;background:linear-gradient(135deg,var(--gold-bright),var(--gold-dim));color:#15151A;font-weight:700;font-size:15px;transition:.25s}
|
||
.cd-checkout:hover{transform:translateY(-1px);box-shadow:0 12px 30px -10px rgba(212,175,55,.5)}
|
||
|
||
/* ===== FOOTER ===== */
|
||
footer{background:var(--bg-darkest);border-top:1px solid var(--line);padding:60px 0 100px;margin-top:40px;position:relative;z-index:1}
|
||
.foot-inner{display:flex;justify-content:space-between;gap:40px;flex-wrap:wrap}
|
||
.foot-brand{max-width:320px}
|
||
.foot-brand .brand{margin-bottom:16px}
|
||
.foot-brand p{font-size:13px;color:var(--text-3);line-height:1.7}
|
||
.foot-col h4{font-family:var(--serif);font-size:15px;color:var(--gold);margin-bottom:16px;letter-spacing:.04em}
|
||
.foot-col a{display:block;font-size:13px;color:var(--text-2);margin-bottom:10px;transition:.2s}
|
||
.foot-col a:hover{color:var(--gold);padding-left:4px}
|
||
.foot-bottom{margin-top:48px;padding-top:24px;border-top:1px solid var(--line);text-align:center;font-size:12px;color:var(--text-3);letter-spacing:.06em}
|
||
|
||
/* ===== TOAST ===== */
|
||
.toast-wrap{position:fixed;top:88px;left:50%;transform:translateX(-50%);z-index:300;display:flex;flex-direction:column;gap:10px;align-items:center;pointer-events:none}
|
||
.toast{
|
||
background:rgba(15,15,18,.92);backdrop-filter:blur(12px);
|
||
border:1px solid var(--gold);color:var(--gold-bright);
|
||
padding:12px 24px;border-radius:30px;font-size:14px;font-weight:500;letter-spacing:.03em;
|
||
box-shadow:0 12px 36px -10px rgba(0,0,0,.6),0 0 0 1px rgba(212,175,55,.1);
|
||
display:flex;align-items:center;gap:10px;
|
||
transform:translateY(-20px);opacity:0;transition:.3s cubic-bezier(.34,1.56,.64,1);
|
||
}
|
||
.toast.show{transform:translateY(0);opacity:1}
|
||
.toast svg{width:18px;height:18px}
|
||
|
||
.empty{text-align:center;padding:80px 20px;color:var(--text-3)}
|
||
.empty svg{width:64px;height:64px;color:var(--gold-dim);opacity:.5;margin-bottom:18px}
|
||
.empty h3{font-family:var(--serif);font-size:20px;color:var(--text-2);margin-bottom:6px}
|
||
|
||
/* responsive */
|
||
@media(max-width:780px){
|
||
.nav-search{display:none}
|
||
.hero{padding:104px 0 40px}
|
||
.hero-stats{gap:28px}
|
||
.r-grid{grid-template-columns:1fr}
|
||
.m-header{padding:22px 20px 18px}
|
||
.m-cats{flex:0 0 132px}
|
||
.m-menus{padding:8px 18px 110px}
|
||
.m-item-img{flex:0 0 72px;height:72px}
|
||
.cart-inner{gap:14px;padding:12px 16px}
|
||
.cart-detail{min-width:0}
|
||
.cart-checkout{padding:12px 22px;font-size:14px}
|
||
.cart-clear{display:none}
|
||
.sec-line{display:none}
|
||
.modal-box{height:100vh;border-radius:0;border:none}
|
||
}
|
||
@media(max-width:520px){
|
||
.hero h1{font-size:42px}
|
||
.feat-card{flex:0 0 86vw}
|
||
.m-h-top{flex-direction:column}
|
||
.foot-inner{gap:30px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- NAVBAR -->
|
||
<nav class="nav">
|
||
<div class="wrap nav-inner">
|
||
<a class="brand" href="#">
|
||
<span class="brand-main">GOURMET</span>
|
||
<span class="brand-sub">Delivery</span>
|
||
</a>
|
||
<div class="nav-search">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||
<input id="navSearch" type="text" placeholder="搜索餐厅或美食标签...">
|
||
</div>
|
||
<div class="nav-actions">
|
||
<button class="icon-btn" id="cartBtn" title="购物车">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg>
|
||
<span class="cart-badge" id="cartBadge">0</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- HERO -->
|
||
<header class="hero">
|
||
<div class="hero-deco">
|
||
<div class="hero-glow"></div>
|
||
<div class="hero-glow b"></div>
|
||
</div>
|
||
<div class="hero-inner">
|
||
<span class="hero-eyebrow">Michelin Inspired · Night Delivery</span>
|
||
<h1>夜阑珍馐<br><span class="accent">Gourmet Delivery</span></h1>
|
||
<p>甄选城市顶尖餐厅,深夜亦享精致。从经典川粤到法式甜品,以星级标准守护每一道出品,为您奉上至臻味觉之旅。</p>
|
||
<div class="hero-cta">
|
||
<button class="btn btn-gold" onclick="document.getElementById('restaurants').scrollIntoView({behavior:'smooth'})">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 11h18M3 11a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2M3 11v8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-8"/><path d="M7 9V6a5 5 0 0 1 10 0v3"/></svg>
|
||
立即点餐
|
||
</button>
|
||
<button class="btn btn-outline" onclick="document.getElementById('featured').scrollIntoView({behavior:'smooth'})">
|
||
探索推荐
|
||
</button>
|
||
</div>
|
||
<div class="hero-stats">
|
||
<div class="hero-stat"><div class="num" id="statRest">0</div><div class="lbl">精选餐厅</div></div>
|
||
<div class="hero-stat"><div class="num">6</div><div class="lbl">风味品类</div></div>
|
||
<div class="hero-stat"><div class="num">24h</div><div class="lbl">深夜配送</div></div>
|
||
<div class="hero-stat"><div class="num">4.7+</div><div class="lbl">平均评分</div></div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- CATEGORIES -->
|
||
<section id="categories" style="padding-top:24px">
|
||
<div class="wrap">
|
||
<div class="sec-head">
|
||
<div>
|
||
<div class="sec-sub">Cuisine Categories</div>
|
||
<div class="sec-title">风味<span class="gold">分类</span></div>
|
||
</div>
|
||
<div class="sec-line"></div>
|
||
</div>
|
||
<div class="cat-bar" id="catBar"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- FEATURED -->
|
||
<section id="featured">
|
||
<div class="wrap">
|
||
<div class="sec-head">
|
||
<div>
|
||
<div class="sec-sub">Featured Selection</div>
|
||
<div class="sec-title">主厨<span class="gold">推荐</span></div>
|
||
</div>
|
||
<div class="sec-line"></div>
|
||
<span style="font-size:12px;color:var(--text-3);letter-spacing:.1em">← 横向滑动 →</span>
|
||
</div>
|
||
<div class="feat-track" id="featTrack"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- RESTAURANTS -->
|
||
<section id="restaurants">
|
||
<div class="wrap">
|
||
<div class="sec-head">
|
||
<div>
|
||
<div class="sec-sub">All Restaurants</div>
|
||
<div class="sec-title">全部<span class="gold">餐厅</span></div>
|
||
</div>
|
||
<div class="sec-line"></div>
|
||
</div>
|
||
<div class="toolbar">
|
||
<div class="count">共 <b id="totalCount">0</b> 家精选餐厅</div>
|
||
<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>
|
||
</div>
|
||
<div class="r-grid" id="rGrid"></div>
|
||
<div class="pager" id="pager"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- FOOTER -->
|
||
<footer>
|
||
<div class="wrap">
|
||
<div class="foot-inner">
|
||
<div class="foot-brand">
|
||
<a class="brand" href="#"><span class="brand-main">GOURMET</span><span class="brand-sub">Delivery</span></a>
|
||
<p>甄选城市顶尖餐厅,以星级标准守护每一道出品。深夜亦享精致,让美味与格调如期而至。</p>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4>关于我们</h4>
|
||
<a href="#">品牌故事</a><a href="#">主厨团队</a><a href="#">品质承诺</a><a href="#">加入我们</a>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4>服务支持</h4>
|
||
<a href="#">配送说明</a><a href="#">支付方式</a><a href="#">常见问题</a><a href="#">联系客服</a>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4>商务合作</h4>
|
||
<a href="#">餐厅入驻</a><a href="#">企业订餐</a><a href="#">媒体咨询</a><a href="#">隐私政策</a>
|
||
</div>
|
||
</div>
|
||
<div class="foot-bottom">© 2026 GOURMET DELIVERY · CRAFTED WITH REFINEMENT · 仅供展示</div>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- MODAL -->
|
||
<div class="modal" id="modal">
|
||
<div class="modal-box" style="position:relative">
|
||
<button class="modal-close" id="modalClose">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
<div id="modalContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CART BAR -->
|
||
<div class="cart-bar" id="cartBar">
|
||
<div class="wrap cart-inner">
|
||
<button class="cart-icon" id="cartIcon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg>
|
||
<span class="cart-count" id="cartCount">0</span>
|
||
</button>
|
||
<div class="cart-detail">
|
||
<div class="cart-total"><span class="cur">¥</span><span id="cartTotal">0.00</span></div>
|
||
<div class="cart-sub" id="cartSub">购物车空空如也</div>
|
||
</div>
|
||
<div class="cart-delivery" id="cartDelivery"></div>
|
||
<button class="cart-clear" id="cartClear">清空</button>
|
||
<button class="cart-checkout" id="cartCheckout" disabled>去结算</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CART DRAWER -->
|
||
<div class="cart-drawer" id="cartDrawer">
|
||
<div class="cd-head">
|
||
<h3>购物车</h3>
|
||
<button class="cd-close" id="cdClose">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="cd-body" id="cdBody"></div>
|
||
<div class="cd-foot" id="cdFoot" style="display:none">
|
||
<div class="cd-row"><span>商品小计</span><span id="cdSubtotal">¥0.00</span></div>
|
||
<div class="cd-row"><span>配送费</span><span id="cdDelivery">¥0.00</span></div>
|
||
<div class="cd-row total"><span>合计</span><b id="cdTotal">¥0.00</b></div>
|
||
<button class="cd-checkout" id="cdCheckout">结算订单</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast-wrap" id="toastWrap"></div>
|
||
|
||
<script>
|
||
/* ============================================================
|
||
GOURMET DELIVERY — 深色高端外卖模板
|
||
============================================================ */
|
||
const API = '/api/v1/food';
|
||
const state = {
|
||
categories: [],
|
||
featured: [],
|
||
list: [],
|
||
total: 0,
|
||
filter: { categoryId: 0, keyword: '', sort: '', page: 1, pageSize: 9 },
|
||
cart: [], // [{rid, rname, itemId, name, price, qty}]
|
||
menuCache: {}, // rid -> {restaurant, categories, items}
|
||
currentRid: null,
|
||
};
|
||
|
||
/* ---------- SVG icon library ---------- */
|
||
const ICONS = {
|
||
chinese: '<path d="M3 11h18M5 11V8a7 7 0 0 1 14 0v3M7 15h10M9 19h6"/><path d="M12 3v18"/>',
|
||
japanese: '<path d="M12 2v20M2 7h20M2 12h20M2 17h20"/>',
|
||
western: '<path d="M5 11h14a2 2 0 0 1 0 4h-1l-1 6H7l-1-6H5a2 2 0 0 1 0-4Z"/><path d="M9 3c0 1.5 1 2 1 3.5S9 8 9 9M15 3c0 1.5-1 2-1 3.5S15 8 15 9"/>',
|
||
hotpot: '<path d="M5 11h14l-1 8a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2l-1-8Z"/><path d="M3 11h18M9 7c0-1 1-1.5 1-3M13 7c0-1 1-1.5 1-3M11 5c0-1 .5-1.5 .5-3"/>',
|
||
dessert: '<path d="M6 8h12l-1 12a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1L6 8Z"/><path d="M4 8h16M10 8V4a2 2 0 0 1 4 0v4M12 4V2"/>',
|
||
healthy: '<path d="M11 20A7 7 0 0 1 4 13c0-4 3-7 7-9 4 2 7 5 7 9a7 7 0 0 1-7 7Z"/><path d="M11 20V8M8 12l3 3 3-3"/>',
|
||
bowl: '<path d="M3 11h18a9 9 0 0 1-18 0Z"/><path d="M12 11V5M9 5h6"/><path d="M5 19h14"/>',
|
||
rice: '<path d="M4 11h16l-1.5 9a1 1 0 0 1-1 .8H6.5a1 1 0 0 1-1-.8L4 11Z"/><path d="M4 11a8 8 0 0 1 16 0M9 4c0 1 .5 1.5 .5 2.5M14 4c0 1 .5 1.5 .5 2.5"/>',
|
||
noodle: '<path d="M4 11h16l-1.5 9a1 1 0 0 1-1 .8H6.5a1 1 0 0 1-1-.8L4 11Z"/><path d="M3 7c4 2 14 2 18 0M8 4c0 2 2 3 4 3s4-1 4-3"/>',
|
||
meat: '<path d="M14 4a4 4 0 0 0-5.6 5.6L4 14c-1 1-1 3 0 4s3 1 4 0l4.4-4.4A4 4 0 0 0 18 8l-2.5 2.5-2-2L16 6c-.6-.8-1.3-1.4-2-2Z"/>',
|
||
fish: '<path d="M2 12c3-5 9-6 13-3 2 1.5 4 3 7 3-3 0-5 1.5-7 3-4 3-10 2-13-3Z"/><path d="M16 9v.5M18 12h.5M5 12h2"/>',
|
||
cake: '<path d="M4 14h16v6H4zM4 14c2-2 4-2 6 0s4 2 6 0 4-2 4 0"/><path d="M12 8v6M12 8c-1 0-1-2 0-2s1 2 0 2Z"/>',
|
||
drink: '<path d="M6 4h12l-1 16a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L6 4Z"/><path d="M6.5 9h11M9 4V2M12 4V2M15 4V2"/>',
|
||
default: '<path d="M5 11h14a2 2 0 0 1 0 4h-1l-1 6H7l-1-6H5a2 2 0 0 1 0-4Z"/><path d="M9 3c0 1.5 1 2 1 3.5S9 8 9 9M15 3c0 1.5-1 2-1 3.5S15 8 15 9"/>',
|
||
};
|
||
function iconSvg(key, size){
|
||
const path = ICONS[key] || ICONS.default;
|
||
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" width="${size||24}" height="${size||24}">${path}</svg>`;
|
||
}
|
||
function catIcon(name, icon){
|
||
if(icon && ICONS[icon]) return icon;
|
||
const map = {'中式正餐':'chinese','日韩料理':'japanese','西式快餐':'western','火锅烧烤':'hotpot','奶茶甜品':'dessert','健康轻食':'healthy'};
|
||
return map[name] || 'default';
|
||
}
|
||
/* gradient palette by category for placeholders */
|
||
const GRADS = {
|
||
chinese:['#3a1a1a','#7a2d1a'],'日韩料理':['#2a2030','#4a3550'],'西式快餐':['#2a2a14','#5a4a1a'],
|
||
'火锅烧烤':['#2d1414','#5a2828'],'奶茶甜品':['#2a2418','#5a4830'],'健康轻食':['#16241a','#2a4a30'],
|
||
default:['#252530','#3a3a4a']
|
||
};
|
||
function gradStyle(catName){
|
||
const c = GRADS[catName] || GRADS.default;
|
||
return `linear-gradient(135deg,${c[0]},${c[1]})`;
|
||
}
|
||
|
||
/* ---------- API ---------- */
|
||
async function api(path){
|
||
try{
|
||
const res = await fetch(API + path);
|
||
if(!res.ok) throw new Error('HTTP '+res.status);
|
||
const json = await res.json();
|
||
if(json.code !== 0) throw new Error(json.message || '请求失败');
|
||
return json.data;
|
||
}catch(e){
|
||
console.error('API error', path, e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/* ---------- helpers ---------- */
|
||
function parseTags(t){ if(!t) return []; try{ return JSON.parse(t); }catch(e){ return []; } }
|
||
function stars(r){ return '★'; }
|
||
function money(n){ return Number(n||0).toFixed(2); }
|
||
function esc(s){ return String(s||'').replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||
|
||
/* ---------- TOAST ---------- */
|
||
function toast(msg, type){
|
||
const w = document.getElementById('toastWrap');
|
||
const t = document.createElement('div');
|
||
t.className = 'toast';
|
||
const ic = type === 'error'
|
||
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>'
|
||
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M20 6 9 17l-5-5"/></svg>';
|
||
t.innerHTML = ic + '<span>' + esc(msg) + '</span>';
|
||
w.appendChild(t);
|
||
requestAnimationFrame(()=> t.classList.add('show'));
|
||
setTimeout(()=>{ t.classList.remove('show'); setTimeout(()=>t.remove(),300); }, 2200);
|
||
}
|
||
|
||
/* ---------- RENDER CATEGORIES ---------- */
|
||
function renderCats(){
|
||
const bar = document.getElementById('catBar');
|
||
bar.innerHTML = state.categories.map(c=>{
|
||
const act = state.filter.categoryId === c.id ? 'active' : '';
|
||
return `<button class="cat-pill ${act}" data-id="${c.id}">${iconSvg(catIcon(c.name,c.icon),18)}<span>${esc(c.name)}</span></button>`;
|
||
}).join('');
|
||
bar.querySelectorAll('.cat-pill').forEach(b=>{
|
||
b.onclick = ()=>{
|
||
state.filter.categoryId = Number(b.dataset.id);
|
||
state.filter.page = 1;
|
||
renderCats();
|
||
loadList();
|
||
document.getElementById('restaurants').scrollIntoView({behavior:'smooth'});
|
||
};
|
||
});
|
||
}
|
||
|
||
/* ---------- RENDER FEATURED ---------- */
|
||
function renderFeat(){
|
||
const tr = document.getElementById('featTrack');
|
||
if(!state.featured.length){ tr.innerHTML = featSkeleton(3); return; }
|
||
tr.innerHTML = state.featured.map(r=>{
|
||
const ic = catIcon(r.category_name);
|
||
const tags = parseTags(r.tags).slice(0,2);
|
||
return `<div class="feat-card" data-id="${r.id}">
|
||
<div class="feat-cover">
|
||
<div class="grad" style="background:${gradStyle(r.category_name)}"></div>
|
||
${r.cover?`<img src="${esc(r.cover)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('.ico').style.display=''">`:''}
|
||
<div class="ico" style="${r.cover?'display:none':''}">${iconSvg(ic,64)}</div>
|
||
<span class="feat-badge">Featured</span>
|
||
</div>
|
||
<div class="feat-body">
|
||
<div class="feat-cat">${esc(r.category_name)}</div>
|
||
<div class="feat-name">${esc(r.name)}</div>
|
||
<div class="r-tags">${tags.map(t=>`<span class="r-tag">${esc(t)}</span>`).join('')}</div>
|
||
<div class="feat-meta">
|
||
<span><span class="star">${stars()}</span> ${r.rating}</span>
|
||
<span>月售 ${r.sales}</span>
|
||
<span>${r.delivery_time}分钟</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
tr.querySelectorAll('.feat-card').forEach(c=> c.onclick = ()=> openRestaurant(Number(c.dataset.id)));
|
||
}
|
||
|
||
/* ---------- RENDER LIST ---------- */
|
||
function renderList(){
|
||
const grid = document.getElementById('rGrid');
|
||
document.getElementById('totalCount').textContent = state.total;
|
||
if(!state.list.length){
|
||
grid.innerHTML = `<div class="empty" style="grid-column:1/-1">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
||
<h3>暂无符合条件的餐厅</h3><p>试试其他分类或关键词</p></div>`;
|
||
document.getElementById('pager').innerHTML = '';
|
||
return;
|
||
}
|
||
grid.innerHTML = state.list.map(r=>{
|
||
const ic = catIcon(r.category_name);
|
||
const tags = parseTags(r.tags).slice(0,3);
|
||
const statusDot = r.status === 1 ? '<span class="r-status"></span>营业中' : '<span class="r-status" style="background:#888;box-shadow:none"></span>休息中';
|
||
return `<div class="r-card" data-id="${r.id}">
|
||
<div class="r-logo">
|
||
<div class="grad" style="background:${gradStyle(r.category_name)}"></div>
|
||
${r.logo?`<img src="${esc(r.logo)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('.ico').style.display=''">`:''}
|
||
<div class="gframe"></div>
|
||
<div class="ico" style="${r.logo?'display:none':''}">${iconSvg(ic,46)}</div>
|
||
</div>
|
||
<div class="r-info">
|
||
<div class="r-top">
|
||
<div class="r-name">${esc(r.name)}</div>
|
||
<div class="r-rating">${iconSvg('default',14).replace(ICONS.default,'<path d="M12 2l3 6.5 7 .9-5 4.9 1.3 7L12 18l-6.3 3.3L7 14.3 2 9.4l7-.9L12 2z" fill="currentColor" stroke="none"/>')} ${r.rating}</div>
|
||
</div>
|
||
<div class="r-cat">${esc(r.category_name)}</div>
|
||
<div class="r-tags">${tags.map(t=>`<span class="r-tag">${esc(t)}</span>`).join('')}</div>
|
||
<div class="r-foot">
|
||
<span>${statusDot}</span>
|
||
<span>${iconSvg('bowl',13)} 月售 ${r.sales}</span>
|
||
<span>¥${money(r.delivery_fee)}配送</span>
|
||
<span>起送¥${money(r.min_order)}</span>
|
||
<span>${r.delivery_time}分钟 · ${esc(r.distance)}</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
grid.querySelectorAll('.r-card').forEach(c=> c.onclick = ()=> openRestaurant(Number(c.dataset.id)));
|
||
renderPager();
|
||
}
|
||
|
||
/* ---------- PAGER ---------- */
|
||
function renderPager(){
|
||
const pg = document.getElementById('pager');
|
||
const totalPages = Math.ceil(state.total / state.filter.pageSize) || 1;
|
||
const cur = state.filter.page;
|
||
if(totalPages <= 1){ pg.innerHTML = ''; return; }
|
||
let html = `<button class="pg-btn" ${cur<=1?'disabled':''} data-p="${cur-1}">‹</button>`;
|
||
const range = [];
|
||
for(let i=1;i<=totalPages;i++){
|
||
if(i===1||i===totalPages||(i>=cur-1&&i<=cur+1)) range.push(i);
|
||
else if(range[range.length-1] !== '...') range.push('...');
|
||
}
|
||
range.forEach(p=>{
|
||
if(p==='...') html += `<span style="color:var(--text-3);padding:0 4px">…</span>`;
|
||
else html += `<button class="pg-btn ${p===cur?'active':''}" data-p="${p}">${p}</button>`;
|
||
});
|
||
html += `<button class="pg-btn" ${cur>=totalPages?'disabled':''} data-p="${cur+1}">›</button>`;
|
||
pg.innerHTML = html;
|
||
pg.querySelectorAll('.pg-btn[data-p]').forEach(b=>{
|
||
b.onclick = ()=>{ state.filter.page = Number(b.dataset.p); loadList(); document.getElementById('restaurants').scrollIntoView({behavior:'smooth'}); };
|
||
});
|
||
}
|
||
|
||
/* ---------- SKELETONS ---------- */
|
||
function listSkeleton(n){
|
||
return Array.from({length:n}).map(()=>`<div class="sk-card"><div class="sk-block"></div><div class="sk-lines"><div class="sk-l w60"></div><div class="sk-l w40"></div><div class="sk-l w80"></div></div></div>`).join('');
|
||
}
|
||
function featSkeleton(n){ return Array.from({length:n}).map(()=>`<div class="sk-feat"></div>`).join(''); }
|
||
|
||
/* ---------- LOAD LIST ---------- */
|
||
async function loadList(){
|
||
const grid = document.getElementById('rGrid');
|
||
grid.innerHTML = listSkeleton(state.filter.pageSize);
|
||
document.getElementById('pager').innerHTML = '';
|
||
const f = state.filter;
|
||
const q = `?page=${f.page}&page_size=${f.pageSize}` + (f.categoryId?`&category_id=${f.categoryId}`:'') + (f.keyword?`&keyword=${encodeURIComponent(f.keyword)}`:'') + (f.sort?`&sort=${f.sort}`:'');
|
||
const data = await api('/restaurants'+q);
|
||
if(!data){ grid.innerHTML = '<div class="empty" style="grid-column:1/-1"><h3>加载失败</h3><p>请检查网络后刷新</p></div>'; return; }
|
||
state.list = data.list || [];
|
||
state.total = data.total || 0;
|
||
renderList();
|
||
}
|
||
|
||
/* ---------- SORT ---------- */
|
||
document.getElementById('sortGroup').addEventListener('click', e=>{
|
||
const b = e.target.closest('.sort-btn'); if(!b) return;
|
||
document.querySelectorAll('#sortGroup .sort-btn').forEach(x=>x.classList.remove('active'));
|
||
b.classList.add('active');
|
||
state.filter.sort = b.dataset.sort;
|
||
state.filter.page = 1;
|
||
loadList();
|
||
});
|
||
|
||
/* ---------- SEARCH ---------- */
|
||
let searchTimer;
|
||
function doSearch(v){
|
||
state.filter.keyword = v.trim();
|
||
state.filter.page = 1;
|
||
loadList();
|
||
document.getElementById('restaurants').scrollIntoView({behavior:'smooth'});
|
||
}
|
||
document.getElementById('navSearch').addEventListener('input', e=>{
|
||
clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(()=> doSearch(e.target.value), 400);
|
||
});
|
||
|
||
/* ---------- RESTAURANT DETAIL ---------- */
|
||
async function openRestaurant(id){
|
||
state.currentRid = id;
|
||
const modal = document.getElementById('modal');
|
||
const content = document.getElementById('modalContent');
|
||
modal.classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
content.innerHTML = detailSkeleton();
|
||
let data = state.menuCache[id];
|
||
if(!data){
|
||
data = await api(`/restaurants/${id}/menu`);
|
||
if(!data){ toast('菜单加载失败','error'); closeModal(); return; }
|
||
state.menuCache[id] = data;
|
||
}
|
||
renderDetail(data);
|
||
}
|
||
function detailSkeleton(){
|
||
return `<div style="padding:40px">
|
||
<div class="sk" style="height:120px;margin-bottom:24px"></div>
|
||
<div style="display:flex;gap:20px">
|
||
<div class="sk" style="flex:0 0 150px;height:400px"></div>
|
||
<div style="flex:1;display:flex;flex-direction:column;gap:16px">
|
||
${Array.from({length:5}).map(()=>'<div class="sk" style="height:90px"></div>').join('')}
|
||
</div>
|
||
</div></div>`;
|
||
}
|
||
function renderDetail(data){
|
||
const r = data.restaurant;
|
||
const cats = data.categories || [];
|
||
const items = data.menu_items || [];
|
||
const ic = catIcon(r.category_name);
|
||
const tags = parseTags(r.tags);
|
||
const byCat = {};
|
||
items.forEach(it=>{ (byCat[it.category_id] = byCat[it.category_id] || []).push(it); });
|
||
const orderedCats = cats.length ? cats : [{id:0,name:'全部菜品'}];
|
||
|
||
const content = document.getElementById('modalContent');
|
||
content.innerHTML = `
|
||
<div class="m-header">
|
||
<div class="gold-rule"></div>
|
||
<div class="m-h-top">
|
||
<div class="m-logo">
|
||
<div class="grad" style="background:${gradStyle(r.category_name)}"></div>
|
||
${r.logo?`<img src="${esc(r.logo)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('.ico').style.display=''">`:''}
|
||
<div class="ico" style="${r.logo?'display:none':''}">${iconSvg(ic,40)}</div>
|
||
</div>
|
||
<div class="m-h-info">
|
||
<div class="m-name">${esc(r.name)}</div>
|
||
<div class="m-cat">${esc(r.category_name)}</div>
|
||
<div class="m-tags">${tags.map(t=>`<span class="r-tag">${esc(t)}</span>`).join('')}</div>
|
||
${r.notice?`<div class="m-notice">${esc(r.notice)}</div>`:''}
|
||
<div class="m-stats">
|
||
<div class="m-stat"><b>${stars()}</b>${r.rating} 评分</div>
|
||
<div class="m-stat"><b>${r.sales}</b>月售</div>
|
||
<div class="m-stat"><b>¥${money(r.delivery_fee)}</b>配送费</div>
|
||
<div class="m-stat"><b>¥${money(r.min_order)}</b>起送</div>
|
||
<div class="m-stat"><b>${r.delivery_time}</b>分钟 · ${esc(r.distance)}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="m-body">
|
||
<div class="m-cats" id="mCats">
|
||
${orderedCats.map((c,i)=>`<div class="m-cat-item ${i===0?'active':''}" data-cid="${c.id}">${esc(c.name)}</div>`).join('')}
|
||
</div>
|
||
<div class="m-menus" id="mMenus">
|
||
${orderedCats.map(c=>{
|
||
const arr = byCat[c.id] || [];
|
||
if(!arr.length) return '';
|
||
return `<div class="m-group" id="grp-${c.id}" data-cid="${c.id}">
|
||
<div class="m-group-title">${esc(c.name)}</div>
|
||
${arr.map(it=> renderItem(it, ic)).join('')}
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>`;
|
||
bindDetailEvents(data);
|
||
}
|
||
function renderItem(it, ic){
|
||
const tags = parseTags(it.tags);
|
||
const inCart = cartQty(state.currentRid, it.id);
|
||
const origHtml = it.original_price > it.price ? `<span class="orig">¥${money(it.original_price)}</span>` : '';
|
||
return `<div class="m-item" data-id="${it.id}">
|
||
<div class="m-item-img">
|
||
<div class="grad" style="background:${gradStyle(state.menuCache[state.currentRid]?.restaurant?.category_name)}"></div>
|
||
${it.image?`<img src="${esc(it.image)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.parentNode.querySelector('.ico').style.display=''">`:''}
|
||
<div class="ico" style="${it.image?'display:none':''}">${iconSvg(ic,36)}</div>
|
||
</div>
|
||
<div class="m-item-info">
|
||
<div class="m-item-name">${esc(it.name)}</div>
|
||
<div class="m-item-desc">${esc(it.description||'')}</div>
|
||
<div class="m-item-meta">
|
||
<span class="star">★ ${it.rating}</span>
|
||
<span>月售 ${it.sales}</span>
|
||
${tags.map(t=>`<span class="r-tag">${esc(t)}</span>`).join('')}
|
||
</div>
|
||
<div class="m-item-price"><span class="cur">¥</span>${money(it.price)}${origHtml}</div>
|
||
</div>
|
||
<div class="m-item-right">
|
||
${inCart > 0 ? qtyControl(state.currentRid, it.id, inCart) : `<button class="m-add" data-add="${it.id}">${iconSvg('default',18).replace(ICONS.default,'<path d="M12 5v14M5 12h14" stroke="currentColor" fill="none"/>')}</button>`}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
function qtyControl(rid, itemId, qty){
|
||
return `<div class="m-qty" data-rid="${rid}" data-item="${itemId}">
|
||
<button data-act="minus">−</button>
|
||
<span>${qty}</span>
|
||
<button data-act="plus">+</button>
|
||
</div>`;
|
||
}
|
||
function bindDetailEvents(data){
|
||
const menus = document.getElementById('mMenus');
|
||
const catEls = document.querySelectorAll('#mCats .m-cat-item');
|
||
// click category -> scroll
|
||
catEls.forEach(el=>{
|
||
el.onclick = ()=>{
|
||
const cid = el.dataset.cid;
|
||
const grp = document.getElementById('grp-'+cid);
|
||
if(grp) menus.scrollTo({top: grp.offsetTop - 8, behavior:'smooth'});
|
||
};
|
||
});
|
||
// scroll spy
|
||
const groups = [...menus.querySelectorAll('.m-group')];
|
||
let spyTimer;
|
||
menus.addEventListener('scroll', ()=>{
|
||
clearTimeout(spyTimer);
|
||
spyTimer = setTimeout(()=>{
|
||
const top = menus.scrollTop;
|
||
let active = groups[0];
|
||
groups.forEach(g=>{ if(g.offsetTop - 30 <= top) active = g; });
|
||
catEls.forEach(e=> e.classList.toggle('active', e.dataset.cid === active.dataset.cid));
|
||
}, 40);
|
||
});
|
||
// add / qty
|
||
menus.addEventListener('click', e=>{
|
||
const addBtn = e.target.closest('[data-add]');
|
||
const qtyBtn = e.target.closest('[data-act]');
|
||
if(addBtn){
|
||
addToCart(state.currentRid, Number(addBtn.dataset.add), data);
|
||
}else if(qtyBtn){
|
||
const wrap = qtyBtn.closest('.m-qty');
|
||
const rid = Number(wrap.dataset.rid), itemId = Number(wrap.dataset.item);
|
||
changeQty(rid, itemId, qtyBtn.dataset.act === 'plus' ? 1 : -1, data);
|
||
}
|
||
});
|
||
}
|
||
function closeModal(){
|
||
document.getElementById('modal').classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
state.currentRid = null;
|
||
}
|
||
document.getElementById('modalClose').onclick = closeModal;
|
||
document.getElementById('modal').addEventListener('click', e=>{ if(e.target.id==='modal') closeModal(); });
|
||
document.addEventListener('keydown', e=>{ if(e.key==='Escape') closeModal(); });
|
||
|
||
/* ---------- CART ---------- */
|
||
function cartQty(rid, itemId){ const it = state.cart.find(c=>c.rid===rid && c.itemId===itemId); return it?it.qty:0; }
|
||
function addToCart(rid, itemId, data){
|
||
const r = data.restaurant;
|
||
const it = data.menu_items.find(m=>m.id===itemId);
|
||
if(!it) return;
|
||
const exist = state.cart.find(c=>c.rid===rid && c.itemId===itemId);
|
||
if(exist) exist.qty++;
|
||
else state.cart.push({rid, rname:r.name, deliveryFee:r.delivery_fee, minOrder:r.min_order, itemId, name:it.name, price:it.price, qty:1});
|
||
toast('已加入购物车');
|
||
refreshCartUI(data);
|
||
}
|
||
function changeQty(rid, itemId, delta, data){
|
||
const it = state.cart.find(c=>c.rid===rid && c.itemId===itemId);
|
||
if(!it) return;
|
||
it.qty += delta;
|
||
if(it.qty <= 0) state.cart = state.cart.filter(c=>!(c.rid===rid && c.itemId===itemId));
|
||
refreshCartUI(data);
|
||
}
|
||
function refreshCartUI(data){
|
||
// refresh menu item qty controls
|
||
if(data && state.currentRid){
|
||
document.querySelectorAll('#mMenus .m-item').forEach(el=>{
|
||
const itemId = Number(el.dataset.id);
|
||
const q = cartQty(state.currentRid, itemId);
|
||
const right = el.querySelector('.m-item-right');
|
||
if(q > 0) right.innerHTML = qtyControl(state.currentRid, itemId, q);
|
||
else right.innerHTML = `<button class="m-add" data-add="${itemId}">${iconSvg('default',18).replace(ICONS.default,'<path d="M12 5v14M5 12h14" stroke="currentColor" fill="none"/>')}</button>`;
|
||
});
|
||
}
|
||
renderCartBar();
|
||
renderCartDrawer();
|
||
}
|
||
function cartCount(){ return state.cart.reduce((s,c)=>s+c.qty,0); }
|
||
function cartSubtotal(){ return state.cart.reduce((s,c)=>s+c.price*c.qty,0); }
|
||
function cartRids(){ return [...new Set(state.cart.map(c=>c.rid))]; }
|
||
function cartDeliveryFee(){
|
||
// use max delivery fee among restaurants in cart
|
||
const rids = cartRids(); if(!rids.length) return 0;
|
||
return Math.max(...state.cart.filter(c=>rids.includes(c.rid)).map(c=>c.deliveryFee||0));
|
||
}
|
||
function renderCartBar(){
|
||
const count = cartCount();
|
||
const bar = document.getElementById('cartBar');
|
||
document.getElementById('cartBadge').textContent = count;
|
||
document.getElementById('cartBadge').classList.toggle('show', count>0);
|
||
document.getElementById('cartCount').textContent = count;
|
||
document.getElementById('cartTotal').textContent = money(cartSubtotal());
|
||
const rids = cartRids();
|
||
if(!count){ document.getElementById('cartSub').textContent = '购物车空空如也'; document.getElementById('cartDelivery').textContent=''; }
|
||
else{
|
||
const names = rids.length<=2 ? rids.map(rid=> state.cart.find(c=>c.rid===rid).rname).join(' · ') : `${rids.length} 家餐厅`;
|
||
document.getElementById('cartSub').textContent = names;
|
||
document.getElementById('cartDelivery').textContent = `配送费 ¥${money(cartDeliveryFee())}`;
|
||
}
|
||
const checkout = document.getElementById('cartCheckout');
|
||
checkout.disabled = count === 0;
|
||
bar.classList.toggle('show', count>0);
|
||
}
|
||
function renderCartDrawer(){
|
||
const body = document.getElementById('cdBody');
|
||
const foot = document.getElementById('cdFoot');
|
||
if(!state.cart.length){
|
||
body.innerHTML = `<div class="cd-empty">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg>
|
||
<p>购物车空空如也<br>去挑选心仪的美食吧</p></div>`;
|
||
foot.style.display = 'none';
|
||
return;
|
||
}
|
||
const groups = {};
|
||
state.cart.forEach(c=>{ (groups[c.rid] = groups[c.rid] || {name:c.rname, items:[]}).items.push(c); });
|
||
body.innerHTML = Object.values(groups).map(g=>`
|
||
<div class="cd-group">
|
||
<div class="cd-group-name">${esc(g.name)}</div>
|
||
${g.items.map(it=>`
|
||
<div class="cd-item">
|
||
<div class="cd-item-info">
|
||
<div class="cd-item-name">${esc(it.name)}</div>
|
||
<div class="cd-item-price">¥${money(it.price)}</div>
|
||
</div>
|
||
<div class="cd-qty" data-rid="${it.rid}" data-item="${it.itemId}">
|
||
<button data-act="minus">−</button>
|
||
<span>${it.qty}</span>
|
||
<button data-act="plus">+</button>
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>`).join('');
|
||
body.querySelectorAll('.cd-qty button').forEach(b=>{
|
||
b.onclick = ()=>{
|
||
const w = b.closest('.cd-qty');
|
||
changeQty(Number(w.dataset.rid), Number(w.dataset.item), b.dataset.act==='plus'?1:-1);
|
||
};
|
||
});
|
||
const sub = cartSubtotal(), del = cartDeliveryFee();
|
||
document.getElementById('cdSubtotal').textContent = '¥'+money(sub);
|
||
document.getElementById('cdDelivery').textContent = '¥'+money(del);
|
||
document.getElementById('cdTotal').textContent = '¥'+money(sub+del);
|
||
foot.style.display = 'block';
|
||
}
|
||
/* cart drawer open/close */
|
||
function openDrawer(){ document.getElementById('cartDrawer').classList.add('open'); }
|
||
function closeDrawer(){ document.getElementById('cartDrawer').classList.remove('open'); }
|
||
document.getElementById('cartBtn').onclick = openDrawer;
|
||
document.getElementById('cartIcon').onclick = openDrawer;
|
||
document.getElementById('cdClose').onclick = closeDrawer;
|
||
document.getElementById('cartClear').onclick = ()=>{ if(state.cart.length){ state.cart=[]; refreshCartUI(); toast('购物车已清空'); } };
|
||
document.getElementById('cartCheckout').onclick = checkout;
|
||
document.getElementById('cdCheckout').onclick = checkout;
|
||
function checkout(){
|
||
if(!state.cart.length){ toast('购物车为空','error'); return; }
|
||
const sub = cartSubtotal(), del = cartDeliveryFee();
|
||
toast(`订单提交成功 · 合计 ¥${money(sub+del)}`);
|
||
state.cart = [];
|
||
refreshCartUI();
|
||
closeDrawer();
|
||
closeModal();
|
||
}
|
||
|
||
/* ---------- INIT ---------- */
|
||
async function init(){
|
||
// featured skeleton immediately
|
||
document.getElementById('featTrack').innerHTML = featSkeleton(4);
|
||
const [home, cats] = await Promise.all([
|
||
api('/home'),
|
||
api('/categories')
|
||
]);
|
||
if(home){
|
||
state.categories = home.categories || [];
|
||
state.featured = home.featured || [];
|
||
document.getElementById('statRest').textContent = state.featured.length ? '12+' : state.featured.length;
|
||
}
|
||
if(cats && (!state.categories.length)) state.categories = cats;
|
||
renderCats();
|
||
renderFeat();
|
||
// restaurants
|
||
const grid = document.getElementById('rGrid');
|
||
grid.innerHTML = listSkeleton(state.filter.pageSize);
|
||
await loadList();
|
||
renderCartBar();
|
||
}
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|