- 导航页(index.html)添加完整i18n引擎,支持zh-CN/zh-TW/en - 导航页添加语言切换按钮(EN/中/繁) - 所有20个前端模板默认语言从zh-CN改为en - 导航页SEO meta标签改为英文
1505 lines
74 KiB
HTML
1505 lines
74 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)}
|
||
|
||
/* ===== LANGUAGE SWITCHER ===== */
|
||
.lang-switcher{position:relative;margin-left:8px}
|
||
.lang-btn{
|
||
display:flex;align-items:center;gap:6px;padding:8px 14px;border-radius:30px;
|
||
font-size:13px;font-weight:600;color:var(--text-2);cursor:pointer;
|
||
background:rgba(15,15,18,.5);border:1px solid var(--line);
|
||
transition:all .25s;
|
||
}
|
||
.lang-btn:hover{color:var(--gold);border-color:var(--gold);background:rgba(212,175,55,.06)}
|
||
.lang-btn svg{width:16px;height:16px}
|
||
.lang-dropdown{
|
||
position:absolute;top:calc(100% + 8px);right:0;min-width:160px;
|
||
background:rgba(26,26,30,.96);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
||
border:1px solid var(--line-strong);border-radius:12px;
|
||
box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden;
|
||
opacity:0;transform:translateY(-8px);pointer-events:none;
|
||
transition:all .25s;z-index:100;
|
||
}
|
||
.lang-dropdown.open{opacity:1;transform:none;pointer-events:auto}
|
||
.lang-option{
|
||
display:flex;align-items:center;gap:10px;padding:12px 16px;
|
||
font-size:14px;color:var(--text-2);cursor:pointer;transition:all .2s;
|
||
}
|
||
.lang-option:hover{background:rgba(212,175,55,.1);color:var(--text)}
|
||
.lang-option.active{color:var(--gold);font-weight:600}
|
||
.lang-option .check{width:16px;height:16px;opacity:0;transition:opacity .2s}
|
||
.lang-option.active .check{opacity: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}
|
||
.lang-switcher{position:static;margin-left:0}
|
||
.lang-dropdown{right:auto;left:0}
|
||
}
|
||
@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" data-i18n-placeholder="search_placeholder" placeholder="搜索餐厅或美食标签...">
|
||
</div>
|
||
<div class="nav-actions">
|
||
<button class="icon-btn" id="cartBtn" data-i18n-title="cart_title" 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 class="lang-switcher" id="langSwitcher">
|
||
<button class="lang-btn" id="langBtn" onclick="toggleLangDropdown()">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20z"/></svg>
|
||
<span id="langLabel">中文</span>
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:12px;height:12px"><polyline points="6 9 12 15 18 9"/></svg>
|
||
</button>
|
||
<div class="lang-dropdown" id="langDropdown">
|
||
<div class="lang-option" data-lang="zh-CN" onclick="switchLang('zh-CN')">
|
||
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||
<span>简体中文</span>
|
||
</div>
|
||
<div class="lang-option" data-lang="zh-TW" onclick="switchLang('zh-TW')">
|
||
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||
<span>繁體中文</span>
|
||
</div>
|
||
<div class="lang-option" data-lang="en" onclick="switchLang('en')">
|
||
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||
<span>English</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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" data-i18n="hero_eyebrow">Michelin Inspired · Night Delivery</span>
|
||
<h1 data-i18n="hero_title">夜阑珍馐<br><span class="accent">Gourmet Delivery</span></h1>
|
||
<p data-i18n="hero_desc">甄选城市顶尖餐厅,深夜亦享精致。从经典川粤到法式甜品,以星级标准守护每一道出品,为您奉上至臻味觉之旅。</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>
|
||
<span data-i18n="hero_cta1">立即点餐</span>
|
||
</button>
|
||
<button class="btn btn-outline" onclick="document.getElementById('featured').scrollIntoView({behavior:'smooth'})">
|
||
<span data-i18n="hero_cta2">探索推荐</span>
|
||
</button>
|
||
</div>
|
||
<div class="hero-stats">
|
||
<div class="hero-stat"><div class="num" id="statRest">0</div><div class="lbl" data-i18n="stat_restaurants">精选餐厅</div></div>
|
||
<div class="hero-stat"><div class="num">6</div><div class="lbl" data-i18n="stat_categories">风味品类</div></div>
|
||
<div class="hero-stat"><div class="num">24h</div><div class="lbl" data-i18n="stat_delivery">深夜配送</div></div>
|
||
<div class="hero-stat"><div class="num">4.7+</div><div class="lbl" data-i18n="stat_rating">平均评分</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" data-i18n="cat_sub">Cuisine Categories</div>
|
||
<div class="sec-title"><span data-i18n="cat_title_1">风味</span><span class="gold" data-i18n="cat_title_2">分类</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" data-i18n="feat_sub">Featured Selection</div>
|
||
<div class="sec-title"><span data-i18n="feat_title_1">主厨</span><span class="gold" data-i18n="feat_title_2">推荐</span></div>
|
||
</div>
|
||
<div class="sec-line"></div>
|
||
<span style="font-size:12px;color:var(--text-3);letter-spacing:.1em" data-i18n="feat_scroll_hint">← 横向滑动 →</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" data-i18n="rest_sub">All Restaurants</div>
|
||
<div class="sec-title"><span data-i18n="rest_title_1">全部</span><span class="gold" data-i18n="rest_title_2">餐厅</span></div>
|
||
</div>
|
||
<div class="sec-line"></div>
|
||
</div>
|
||
<div class="toolbar">
|
||
<div class="count"><span data-i18n="rest_count_prefix">共</span> <b id="totalCount">0</b> <span data-i18n="rest_count_suffix">家精选餐厅</span></div>
|
||
<div class="sort-group" id="sortGroup">
|
||
<button class="sort-btn active" data-sort="" data-i18n="sort_comprehensive">综合</button>
|
||
<button class="sort-btn" data-sort="rating" data-i18n="sort_rating">评分</button>
|
||
<button class="sort-btn" data-sort="sales" data-i18n="sort_sales">销量</button>
|
||
<button class="sort-btn" data-sort="delivery_time" data-i18n="sort_delivery">配送</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 data-i18n="footer_brand_desc">甄选城市顶尖餐厅,以星级标准守护每一道出品。深夜亦享精致,让美味与格调如期而至。</p>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4 data-i18n="footer_about">关于我们</h4>
|
||
<a href="#" data-i18n="footer_story">品牌故事</a><a href="#" data-i18n="footer_chefs">主厨团队</a><a href="#" data-i18n="footer_quality">品质承诺</a><a href="#" data-i18n="footer_join">加入我们</a>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4 data-i18n="footer_support">服务支持</h4>
|
||
<a href="#" data-i18n="footer_delivery_info">配送说明</a><a href="#" data-i18n="footer_payment">支付方式</a><a href="#" data-i18n="footer_faq">常见问题</a><a href="#" data-i18n="footer_contact_us">联系客服</a>
|
||
</div>
|
||
<div class="foot-col">
|
||
<h4 data-i18n="footer_business">商务合作</h4>
|
||
<a href="#" data-i18n="footer_partner">餐厅入驻</a><a href="#" data-i18n="footer_corporate">企业订餐</a><a href="#" data-i18n="footer_media">媒体咨询</a><a href="#" data-i18n="footer_privacy">隐私政策</a>
|
||
</div>
|
||
</div>
|
||
<div class="foot-bottom"><span data-i18n="copyright">© 2026 GOURMET DELIVERY · CRAFTED WITH REFINEMENT · 仅供展示</span></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" data-i18n="cart_empty">购物车空空如也</div>
|
||
</div>
|
||
<div class="cart-delivery" id="cartDelivery"></div>
|
||
<button class="cart-clear" id="cartClear" data-i18n="cart_clear">清空</button>
|
||
<button class="cart-checkout" id="cartCheckout" disabled data-i18n="cart_checkout">去结算</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CART DRAWER -->
|
||
<div class="cart-drawer" id="cartDrawer">
|
||
<div class="cd-head">
|
||
<h3 data-i18n="cart_drawer_title">购物车</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 data-i18n="cd_subtotal">商品小计</span><span id="cdSubtotal">¥0.00</span></div>
|
||
<div class="cd-row"><span data-i18n="cd_delivery">配送费</span><span id="cdDelivery">¥0.00</span></div>
|
||
<div class="cd-row total"><span data-i18n="cd_total">合计</span><b id="cdTotal">¥0.00</b></div>
|
||
<button class="cd-checkout" id="cdCheckout" data-i18n="cd_checkout">结算订单</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast-wrap" id="toastWrap"></div>
|
||
|
||
<script>
|
||
/* =========================================================
|
||
i18n 国际化引擎
|
||
========================================================= */
|
||
const I18N_LOCALES = {
|
||
'zh-CN': {
|
||
search_placeholder: '搜索餐厅或美食标签...',
|
||
cart_title: '购物车',
|
||
hero_eyebrow: 'Michelin Inspired · Night Delivery',
|
||
hero_title: '夜阑珍馐',
|
||
hero_desc: '甄选城市顶尖餐厅,深夜亦享精致。从经典川粤到法式甜品,以星级标准守护每一道出品,为您奉上至臻味觉之旅。',
|
||
hero_cta1: '立即点餐',
|
||
hero_cta2: '探索推荐',
|
||
stat_restaurants: '精选餐厅',
|
||
stat_categories: '风味品类',
|
||
stat_delivery: '深夜配送',
|
||
stat_rating: '平均评分',
|
||
cat_sub: 'Cuisine Categories',
|
||
cat_title_1: '风味',
|
||
cat_title_2: '分类',
|
||
feat_sub: 'Featured Selection',
|
||
feat_title_1: '主厨',
|
||
feat_title_2: '推荐',
|
||
feat_scroll_hint: '← 横向滑动 →',
|
||
rest_sub: 'All Restaurants',
|
||
rest_title_1: '全部',
|
||
rest_title_2: '餐厅',
|
||
rest_count_prefix: '共',
|
||
rest_count_suffix: '家精选餐厅',
|
||
sort_comprehensive: '综合',
|
||
sort_rating: '评分',
|
||
sort_sales: '销量',
|
||
sort_delivery: '配送',
|
||
footer_brand_desc: '甄选城市顶尖餐厅,以星级标准守护每一道出品。深夜亦享精致,让美味与格调如期而至。',
|
||
footer_about: '关于我们',
|
||
footer_story: '品牌故事',
|
||
footer_chefs: '主厨团队',
|
||
footer_quality: '品质承诺',
|
||
footer_join: '加入我们',
|
||
footer_support: '服务支持',
|
||
footer_delivery_info: '配送说明',
|
||
footer_payment: '支付方式',
|
||
footer_faq: '常见问题',
|
||
footer_contact_us: '联系客服',
|
||
footer_business: '商务合作',
|
||
footer_partner: '餐厅入驻',
|
||
footer_corporate: '企业订餐',
|
||
footer_media: '媒体咨询',
|
||
footer_privacy: '隐私政策',
|
||
copyright: '© 2026 GOURMET DELIVERY · CRAFTED WITH REFINEMENT · 仅供展示',
|
||
cart_empty: '购物车空空如也',
|
||
cart_clear: '清空',
|
||
cart_checkout: '去结算',
|
||
cart_drawer_title: '购物车',
|
||
cd_subtotal: '商品小计',
|
||
cd_delivery: '配送费',
|
||
cd_total: '合计',
|
||
cd_checkout: '结算订单',
|
||
toast_added: '已加入购物车',
|
||
toast_cart_cleared: '购物车已清空',
|
||
toast_cart_empty: '购物车为空',
|
||
toast_order_success: '订单提交成功',
|
||
toast_menu_error: '菜单加载失败',
|
||
empty_restaurants: '暂无符合条件的餐厅',
|
||
empty_restaurants_hint: '试试其他分类或关键词',
|
||
load_error: '加载失败',
|
||
load_error_hint: '请检查网络后刷新',
|
||
status_open: '营业中',
|
||
status_closed: '休息中',
|
||
monthly_sales: '月售',
|
||
delivery_fee: '配送费',
|
||
min_order: '起送',
|
||
minutes: '分钟',
|
||
rating_label: '评分',
|
||
all_dishes: '全部菜品',
|
||
subtotal_label: '商品小计',
|
||
delivery_fee_label: '配送费',
|
||
total_label: '合计',
|
||
checkout_label: '去结算',
|
||
cart_empty_drawer: '购物车空空如也',
|
||
cart_empty_hint: '去挑选心仪的美食吧',
|
||
restaurants_count: '家餐厅',
|
||
delivery_yuan: '配送费',
|
||
order_success_detail: '订单提交成功 · 合计',
|
||
},
|
||
'en': {
|
||
search_placeholder: 'Search restaurants or food tags...',
|
||
cart_title: 'Cart',
|
||
hero_eyebrow: 'Michelin Inspired · Night Delivery',
|
||
hero_title: 'Late Night Gourmet',
|
||
hero_desc: 'Curating top city restaurants for exquisite late-night dining. From classic Sichuan-Cantonese to French pastries, star-quality standards for every dish, delivering an ultimate taste journey.',
|
||
hero_cta1: 'Order Now',
|
||
hero_cta2: 'Explore Picks',
|
||
stat_restaurants: 'Top Restaurants',
|
||
stat_categories: 'Cuisine Types',
|
||
stat_delivery: 'Late Delivery',
|
||
stat_rating: 'Avg Rating',
|
||
cat_sub: 'Cuisine Categories',
|
||
cat_title_1: 'Cuisine ',
|
||
cat_title_2: 'Categories',
|
||
feat_sub: 'Featured Selection',
|
||
feat_title_1: "Chef's ",
|
||
feat_title_2: 'Picks',
|
||
feat_scroll_hint: '← Swipe →',
|
||
rest_sub: 'All Restaurants',
|
||
rest_title_1: 'All ',
|
||
rest_title_2: 'Restaurants',
|
||
rest_count_prefix: '',
|
||
rest_count_suffix: 'restaurants',
|
||
sort_comprehensive: 'Default',
|
||
sort_rating: 'Rating',
|
||
sort_sales: 'Sales',
|
||
sort_delivery: 'Delivery',
|
||
footer_brand_desc: 'Curating top restaurants with star-quality standards. Late-night elegance delivered on time.',
|
||
footer_about: 'About',
|
||
footer_story: 'Our Story',
|
||
footer_chefs: 'Our Chefs',
|
||
footer_quality: 'Quality Promise',
|
||
footer_join: 'Join Us',
|
||
footer_support: 'Support',
|
||
footer_delivery_info: 'Delivery Info',
|
||
footer_payment: 'Payment',
|
||
footer_faq: 'FAQ',
|
||
footer_contact_us: 'Contact Us',
|
||
footer_business: 'Partners',
|
||
footer_partner: 'Join Platform',
|
||
footer_corporate: 'Corporate Orders',
|
||
footer_media: 'Press',
|
||
footer_privacy: 'Privacy',
|
||
copyright: '© 2026 GOURMET DELIVERY · CRAFTED WITH REFINEMENT · Demo Only',
|
||
cart_empty: 'Cart is empty',
|
||
cart_clear: 'Clear',
|
||
cart_checkout: 'Checkout',
|
||
cart_drawer_title: 'Shopping Cart',
|
||
cd_subtotal: 'Subtotal',
|
||
cd_delivery: 'Delivery',
|
||
cd_total: 'Total',
|
||
cd_checkout: 'Checkout',
|
||
toast_added: 'Added to cart',
|
||
toast_cart_cleared: 'Cart cleared',
|
||
toast_cart_empty: 'Cart is empty',
|
||
toast_order_success: 'Order submitted',
|
||
toast_menu_error: 'Failed to load menu',
|
||
empty_restaurants: 'No restaurants found',
|
||
empty_restaurants_hint: 'Try different categories or keywords',
|
||
load_error: 'Load failed',
|
||
load_error_hint: 'Please check your network',
|
||
status_open: 'Open',
|
||
status_closed: 'Closed',
|
||
monthly_sales: 'monthly sales',
|
||
delivery_fee: 'Delivery',
|
||
min_order: 'Min order',
|
||
minutes: 'min',
|
||
rating_label: 'Rating',
|
||
all_dishes: 'All Dishes',
|
||
subtotal_label: 'Subtotal',
|
||
delivery_fee_label: 'Delivery',
|
||
total_label: 'Total',
|
||
checkout_label: 'Checkout',
|
||
cart_empty_drawer: 'Cart is empty',
|
||
cart_empty_hint: 'Pick some delicious food!',
|
||
restaurants_count: 'restaurants',
|
||
delivery_yuan: 'Delivery',
|
||
order_success_detail: 'Order placed · Total',
|
||
},
|
||
'zh-TW': {
|
||
search_placeholder: '搜尋餐廳或美食標籤...',
|
||
cart_title: '購物車',
|
||
hero_eyebrow: 'Michelin Inspired · Night Delivery',
|
||
hero_title: '夜闌珍饈',
|
||
hero_desc: '甄選城市頂尖餐廳,深夜亦享精緻。從經典川粵到法式甜品,以星級標準守護每一道出品,為您奉上至臻味覺之旅。',
|
||
hero_cta1: '立即點餐',
|
||
hero_cta2: '探索推薦',
|
||
stat_restaurants: '精選餐廳',
|
||
stat_categories: '風味品類',
|
||
stat_delivery: '深夜配送',
|
||
stat_rating: '平均評分',
|
||
cat_sub: 'Cuisine Categories',
|
||
cat_title_1: '風味',
|
||
cat_title_2: '分類',
|
||
feat_sub: 'Featured Selection',
|
||
feat_title_1: '主廚',
|
||
feat_title_2: '推薦',
|
||
feat_scroll_hint: '← 橫向滑動 →',
|
||
rest_sub: 'All Restaurants',
|
||
rest_title_1: '全部',
|
||
rest_title_2: '餐廳',
|
||
rest_count_prefix: '共',
|
||
rest_count_suffix: '家精選餐廳',
|
||
sort_comprehensive: '綜合',
|
||
sort_rating: '評分',
|
||
sort_sales: '銷量',
|
||
sort_delivery: '配送',
|
||
footer_brand_desc: '甄選城市頂尖餐廳,以星級標準守護每一道出品。深夜亦享精緻,讓美味與格調如期而至。',
|
||
footer_about: '關於我們',
|
||
footer_story: '品牌故事',
|
||
footer_chefs: '主廚團隊',
|
||
footer_quality: '品質承諾',
|
||
footer_join: '加入我們',
|
||
footer_support: '服務支援',
|
||
footer_delivery_info: '配送說明',
|
||
footer_payment: '支付方式',
|
||
footer_faq: '常見問題',
|
||
footer_contact_us: '聯繫客服',
|
||
footer_business: '商務合作',
|
||
footer_partner: '餐廳入駐',
|
||
footer_corporate: '企業訂餐',
|
||
footer_media: '媒體諮詢',
|
||
footer_privacy: '隱私政策',
|
||
copyright: '© 2026 GOURMET DELIVERY · CRAFTED WITH REFINEMENT · 僅供展示',
|
||
cart_empty: '購物車空空如也',
|
||
cart_clear: '清空',
|
||
cart_checkout: '去結算',
|
||
cart_drawer_title: '購物車',
|
||
cd_subtotal: '商品小計',
|
||
cd_delivery: '配送費',
|
||
cd_total: '合計',
|
||
cd_checkout: '結算訂單',
|
||
toast_added: '已加入購物車',
|
||
toast_cart_cleared: '購物車已清空',
|
||
toast_cart_empty: '購物車為空',
|
||
toast_order_success: '訂單提交成功',
|
||
toast_menu_error: '菜單載入失敗',
|
||
empty_restaurants: '暫無符合條件的餐廳',
|
||
empty_restaurants_hint: '試試其他分類或關鍵詞',
|
||
load_error: '載入失敗',
|
||
load_error_hint: '請檢查網路後重新整理',
|
||
status_open: '營業中',
|
||
status_closed: '休息中',
|
||
monthly_sales: '月售',
|
||
delivery_fee: '配送費',
|
||
min_order: '起送',
|
||
minutes: '分鐘',
|
||
rating_label: '評分',
|
||
all_dishes: '全部菜品',
|
||
subtotal_label: '商品小計',
|
||
delivery_fee_label: '配送費',
|
||
total_label: '合計',
|
||
checkout_label: '去結算',
|
||
cart_empty_drawer: '購物車空空如也',
|
||
cart_empty_hint: '去挑選心儀的美食吧',
|
||
restaurants_count: '家餐廳',
|
||
delivery_yuan: '配送費',
|
||
order_success_detail: '訂單提交成功 · 合計',
|
||
}
|
||
};
|
||
|
||
const LANG_LABELS = {'zh-CN':'中文','en':'EN','zh-TW':'繁中'};
|
||
|
||
let currentLang = (function(){
|
||
try{
|
||
var u=new URLSearchParams(location.search).get('lang');
|
||
if(u&&I18N_LOCALES[u]) return u;
|
||
}catch(e){}
|
||
try{var s=localStorage.getItem('lang');if(s&&I18N_LOCALES[s]) return s;}catch(e){}
|
||
var b=navigator.language||navigator.userLanguage||'';
|
||
if(b.startsWith('zh-TW')||b.startsWith('zh-Hant')) return 'zh-TW';
|
||
if(b.startsWith('zh')) return 'en';
|
||
return 'en';
|
||
})();
|
||
|
||
function t(key){return (I18N_LOCALES[currentLang]&&I18N_LOCALES[currentLang][key])||(I18N_LOCALES['zh-CN'][key])||key;}
|
||
|
||
function applyI18n(){
|
||
document.querySelectorAll('[data-i18n]').forEach(function(el){
|
||
var key=el.getAttribute('data-i18n');
|
||
el.textContent=t(key);
|
||
});
|
||
document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el){
|
||
var key=el.getAttribute('data-i18n-placeholder');
|
||
el.placeholder=t(key);
|
||
});
|
||
document.querySelectorAll('[data-i18n-title]').forEach(function(el){
|
||
var key=el.getAttribute('data-i18n-title');
|
||
el.title=t(key);
|
||
});
|
||
document.documentElement.lang=currentLang==='zh-TW'?'zh-TW':currentLang==='zh-CN'?'zh-CN':'en';
|
||
document.getElementById('langLabel').textContent=LANG_LABELS[currentLang]||currentLang;
|
||
document.querySelectorAll('.lang-option').forEach(function(opt){
|
||
opt.classList.toggle('active',opt.getAttribute('data-lang')===currentLang);
|
||
});
|
||
}
|
||
|
||
function switchLang(lang){
|
||
if(!I18N_LOCALES[lang]) return;
|
||
currentLang=lang;
|
||
try{localStorage.setItem('lang',lang);}catch(e){}
|
||
applyI18n();
|
||
if(typeof onLangChange==='function') onLangChange(lang);
|
||
document.getElementById('langDropdown').classList.remove('open');
|
||
}
|
||
|
||
function toggleLangDropdown(){
|
||
document.getElementById('langDropdown').classList.toggle('open');
|
||
}
|
||
document.addEventListener('click',function(e){
|
||
var sw=document.getElementById('langSwitcher');
|
||
if(sw&&!sw.contains(e.target)) document.getElementById('langDropdown').classList.remove('open');
|
||
});
|
||
|
||
/* ============================================================
|
||
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 toastMsg(msg, type){
|
||
const w = document.getElementById('toastWrap');
|
||
const el = document.createElement('div');
|
||
el.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>';
|
||
el.innerHTML = ic + '<span>' + esc(msg) + '</span>';
|
||
w.appendChild(el);
|
||
requestAnimationFrame(()=> el.classList.add('show'));
|
||
setTimeout(()=>{ el.classList.remove('show'); setTimeout(()=>el.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>${t('monthly_sales')} ${r.sales}</span>
|
||
<span>${r.delivery_time}${t('minutes')}</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>${t('empty_restaurants')}</h3><p>${t('empty_restaurants_hint')}</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>'+t('status_open') : '<span class="r-status" style="background:#888;box-shadow:none"></span>'+t('status_closed');
|
||
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)} ${t('monthly_sales')} ${r.sales}</span>
|
||
<span>¥${money(r.delivery_fee)}${t('delivery_fee')}</span>
|
||
<span>${t('min_order')}¥${money(r.min_order)}</span>
|
||
<span>${r.delivery_time}${t('minutes')} · ${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>${t('load_error')}</h3><p>${t('load_error_hint')}</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){ toastMsg(t('toast_menu_error'),'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:t('all_dishes')}];
|
||
|
||
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} ${t('rating_label')}</div>
|
||
<div class="m-stat"><b>${r.sales}</b>${t('monthly_sales')}</div>
|
||
<div class="m-stat"><b>¥${money(r.delivery_fee)}</b>${t('delivery_fee')}</div>
|
||
<div class="m-stat"><b>¥${money(r.min_order)}</b>${t('min_order')}</div>
|
||
<div class="m-stat"><b>${r.delivery_time}</b>${t('minutes')} · ${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>${t('monthly_sales')} ${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});
|
||
toastMsg(t('toast_added'));
|
||
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 = t('cart_empty'); document.getElementById('cartDelivery').textContent=''; }
|
||
else{
|
||
const names = rids.length<=2 ? rids.map(rid=> state.cart.find(c=>c.rid===rid).rname).join(' · ') : `${rids.length} ${t('restaurants_count')}`;
|
||
document.getElementById('cartSub').textContent = names;
|
||
document.getElementById('cartDelivery').textContent = `${t('delivery_yuan')} ¥${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>${t('cart_empty_drawer')}<br>${t('cart_empty_hint')}</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(); toastMsg(t('toast_cart_cleared')); } };
|
||
document.getElementById('cartCheckout').onclick = checkout;
|
||
document.getElementById('cdCheckout').onclick = checkout;
|
||
function checkout(){
|
||
if(!state.cart.length){ toastMsg(t('toast_cart_empty'),'error'); return; }
|
||
const sub = cartSubtotal(), del = cartDeliveryFee();
|
||
toastMsg(`${t('order_success_detail')} ¥${money(sub+del)}`);
|
||
state.cart = [];
|
||
refreshCartUI();
|
||
closeDrawer();
|
||
closeModal();
|
||
}
|
||
|
||
/* ---------- INIT ---------- */
|
||
async function init(){
|
||
applyI18n();
|
||
// 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();
|
||
}
|
||
|
||
var lastData={};
|
||
function onLangChange(){
|
||
applyI18n();
|
||
renderCats();
|
||
renderFeat();
|
||
renderList();
|
||
renderCartBar();
|
||
renderCartDrawer();
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|