Files
web_template/web/dist/food2/index.html
Geliebte 10979351f8 feat: 默认语言改为英语 + 导航页添加i18n三语支持
- 导航页(index.html)添加完整i18n引擎,支持zh-CN/zh-TW/en
- 导航页添加语言切换按钮(EN/中/繁)
- 所有20个前端模板默认语言从zh-CN改为en
- 导航页SEO meta标签改为英文
2026-09-09 19:48:45 +08:00

1505 lines
74 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>