feat: 全站20个模板添加国际化(i18n)支持 - 简体中文/繁体中文/英文三语切换

- 所有18套前端模板 + 管理后台共20个HTML文件添加i18n引擎
- 前端JavaScript方案:I18N_LOCALES翻译字典 + data-i18n属性 + t()函数调用
- 语言切换器UI:下拉菜单选择,匹配各模板主题风格
- 语言持久化:localStorage存储 + URL参数覆盖 + 浏览器语言自动检测
- onLangChange回调:语言切换时动态重新渲染所有内容
- 支持语言:zh-CN(简体中文) / zh-TW(繁体中文) / en(English)
This commit is contained in:
2026-09-09 16:58:40 +08:00
parent 69c59f4fbd
commit a84129330e
20 changed files with 9343 additions and 3855 deletions

View File

@@ -107,6 +107,34 @@
}
.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}
@@ -415,6 +443,8 @@
.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}
@@ -435,13 +465,34 @@
</a>
<div class="nav-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="navSearch" type="text" placeholder="搜索餐厅或美食标签...">
<input id="navSearch" type="text" data-i18n-placeholder="search_placeholder" placeholder="搜索餐厅或美食标签...">
</div>
<div class="nav-actions">
<button class="icon-btn" id="cartBtn" title="购物车">
<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>
@@ -453,23 +504,23 @@
<div class="hero-glow b"></div>
</div>
<div class="hero-inner">
<span class="hero-eyebrow">Michelin Inspired · Night Delivery</span>
<h1>夜阑珍馐<br><span class="accent">Gourmet Delivery</span></h1>
<p>甄选城市顶尖餐厅,深夜亦享精致。从经典川粤到法式甜品,以星级标准守护每一道出品,为您奉上至臻味觉之旅。</p>
<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">精选餐厅</div></div>
<div class="hero-stat"><div class="num">6</div><div class="lbl">风味品类</div></div>
<div class="hero-stat"><div class="num">24h</div><div class="lbl">深夜配送</div></div>
<div class="hero-stat"><div class="num">4.7+</div><div class="lbl">平均评分</div></div>
<div 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>
@@ -479,8 +530,8 @@
<div class="wrap">
<div class="sec-head">
<div>
<div class="sec-sub">Cuisine Categories</div>
<div class="sec-title">风味<span class="gold">分类</span></div>
<div 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>
@@ -493,11 +544,11 @@
<div class="wrap">
<div class="sec-head">
<div>
<div class="sec-sub">Featured Selection</div>
<div class="sec-title">主厨<span class="gold">推荐</span></div>
<div 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">← 横向滑动 →</span>
<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>
@@ -508,18 +559,18 @@
<div class="wrap">
<div class="sec-head">
<div>
<div class="sec-sub">All Restaurants</div>
<div class="sec-title">全部<span class="gold">餐厅</span></div>
<div 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">共 <b id="totalCount">0</b> 家精选餐厅</div>
<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="">综合</button>
<button class="sort-btn" data-sort="rating">评分</button>
<button class="sort-btn" data-sort="sales">销量</button>
<button class="sort-btn" data-sort="delivery_time">配送</button>
<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>
@@ -533,22 +584,22 @@
<div class="foot-inner">
<div class="foot-brand">
<a class="brand" href="#"><span class="brand-main">GOURMET</span><span class="brand-sub">Delivery</span></a>
<p>甄选城市顶尖餐厅,以星级标准守护每一道出品。深夜亦享精致,让美味与格调如期而至。</p>
<p data-i18n="footer_brand_desc">甄选城市顶尖餐厅,以星级标准守护每一道出品。深夜亦享精致,让美味与格调如期而至。</p>
</div>
<div class="foot-col">
<h4>关于我们</h4>
<a href="#">品牌故事</a><a href="#">主厨团队</a><a href="#">品质承诺</a><a href="#">加入我们</a>
<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>服务支持</h4>
<a href="#">配送说明</a><a href="#">支付方式</a><a href="#">常见问题</a><a href="#">联系客服</a>
<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>商务合作</h4>
<a href="#">餐厅入驻</a><a href="#">企业订餐</a><a href="#">媒体咨询</a><a href="#">隐私政策</a>
<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">© 2026 GOURMET DELIVERY · CRAFTED WITH REFINEMENT · 仅供展示</div>
<div class="foot-bottom"><span data-i18n="copyright">© 2026 GOURMET DELIVERY · CRAFTED WITH REFINEMENT · 仅供展示</span></div>
</div>
</footer>
@@ -571,34 +622,333 @@
</button>
<div class="cart-detail">
<div class="cart-total"><span class="cur">¥</span><span id="cartTotal">0.00</span></div>
<div class="cart-sub" id="cartSub">购物车空空如也</div>
<div class="cart-sub" id="cartSub" data-i18n="cart_empty">购物车空空如也</div>
</div>
<div class="cart-delivery" id="cartDelivery"></div>
<button class="cart-clear" id="cartClear">清空</button>
<button class="cart-checkout" id="cartCheckout" disabled>去结算</button>
<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>购物车</h3>
<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>商品小计</span><span id="cdSubtotal">¥0.00</span></div>
<div class="cd-row"><span>配送费</span><span id="cdDelivery">¥0.00</span></div>
<div class="cd-row total"><span>合计</span><b id="cdTotal">¥0.00</b></div>
<button class="cd-checkout" id="cdCheckout">结算订单</button>
<div 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 'zh-CN';
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 — 深色高端外卖模板
============================================================ */
@@ -672,17 +1022,17 @@ 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 toast(msg, type){
function toastMsg(msg, type){
const w = document.getElementById('toastWrap');
const t = document.createElement('div');
t.className = 'toast';
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>';
t.innerHTML = ic + '<span>' + esc(msg) + '</span>';
w.appendChild(t);
requestAnimationFrame(()=> t.classList.add('show'));
setTimeout(()=>{ t.classList.remove('show'); setTimeout(()=>t.remove(),300); }, 2200);
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 ---------- */
@@ -723,8 +1073,8 @@ function renderFeat(){
<div class="r-tags">${tags.map(t=>`<span class="r-tag">${esc(t)}</span>`).join('')}</div>
<div class="feat-meta">
<span><span class="star">${stars()}</span> ${r.rating}</span>
<span>月售 ${r.sales}</span>
<span>${r.delivery_time}分钟</span>
<span>${t('monthly_sales')} ${r.sales}</span>
<span>${r.delivery_time}${t('minutes')}</span>
</div>
</div>
</div>`;
@@ -739,14 +1089,14 @@ function renderList(){
if(!state.list.length){
grid.innerHTML = `<div class="empty" style="grid-column:1/-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<h3>暂无符合条件的餐厅</h3><p>试试其他分类或关键词</p></div>`;
<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>营业中' : '<span class="r-status" style="background:#888;box-shadow:none"></span>休息中';
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>
@@ -763,10 +1113,10 @@ function renderList(){
<div class="r-tags">${tags.map(t=>`<span class="r-tag">${esc(t)}</span>`).join('')}</div>
<div class="r-foot">
<span>${statusDot}</span>
<span>${iconSvg('bowl',13)} 月售 ${r.sales}</span>
<span>¥${money(r.delivery_fee)}配送</span>
<span>起送¥${money(r.min_order)}</span>
<span>${r.delivery_time}分钟 · ${esc(r.distance)}</span>
<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>`;
@@ -812,7 +1162,7 @@ async function loadList(){
const f = state.filter;
const q = `?page=${f.page}&page_size=${f.pageSize}` + (f.categoryId?`&category_id=${f.categoryId}`:'') + (f.keyword?`&keyword=${encodeURIComponent(f.keyword)}`:'') + (f.sort?`&sort=${f.sort}`:'');
const data = await api('/restaurants'+q);
if(!data){ grid.innerHTML = '<div class="empty" style="grid-column:1/-1"><h3>加载失败</h3><p>请检查网络后刷新</p></div>'; return; }
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();
@@ -852,7 +1202,7 @@ async function openRestaurant(id){
let data = state.menuCache[id];
if(!data){
data = await api(`/restaurants/${id}/menu`);
if(!data){ toast('菜单加载失败','error'); closeModal(); return; }
if(!data){ toastMsg(t('toast_menu_error'),'error'); closeModal(); return; }
state.menuCache[id] = data;
}
renderDetail(data);
@@ -875,7 +1225,7 @@ function renderDetail(data){
const tags = parseTags(r.tags);
const byCat = {};
items.forEach(it=>{ (byCat[it.category_id] = byCat[it.category_id] || []).push(it); });
const orderedCats = cats.length ? cats : [{id:0,name:'全部菜品'}];
const orderedCats = cats.length ? cats : [{id:0,name:t('all_dishes')}];
const content = document.getElementById('modalContent');
content.innerHTML = `
@@ -893,11 +1243,11 @@ function renderDetail(data){
<div class="m-tags">${tags.map(t=>`<span class="r-tag">${esc(t)}</span>`).join('')}</div>
${r.notice?`<div class="m-notice">${esc(r.notice)}</div>`:''}
<div class="m-stats">
<div class="m-stat"><b>${stars()}</b>${r.rating} 评分</div>
<div class="m-stat"><b>${r.sales}</b>月售</div>
<div class="m-stat"><b>¥${money(r.delivery_fee)}</b>配送费</div>
<div class="m-stat"><b>¥${money(r.min_order)}</b>起送</div>
<div class="m-stat"><b>${r.delivery_time}</b>分钟 · ${esc(r.distance)}</div>
<div 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>
@@ -934,7 +1284,7 @@ function renderItem(it, ic){
<div class="m-item-desc">${esc(it.description||'')}</div>
<div class="m-item-meta">
<span class="star">★ ${it.rating}</span>
<span>月售 ${it.sales}</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>
@@ -1005,7 +1355,7 @@ function addToCart(rid, itemId, data){
const exist = state.cart.find(c=>c.rid===rid && c.itemId===itemId);
if(exist) exist.qty++;
else state.cart.push({rid, rname:r.name, deliveryFee:r.delivery_fee, minOrder:r.min_order, itemId, name:it.name, price:it.price, qty:1});
toast('已加入购物车');
toastMsg(t('toast_added'));
refreshCartUI(data);
}
function changeQty(rid, itemId, delta, data){
@@ -1045,11 +1395,11 @@ function renderCartBar(){
document.getElementById('cartCount').textContent = count;
document.getElementById('cartTotal').textContent = money(cartSubtotal());
const rids = cartRids();
if(!count){ document.getElementById('cartSub').textContent = '购物车空空如也'; document.getElementById('cartDelivery').textContent=''; }
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} 家餐厅`;
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 = `配送费 ¥${money(cartDeliveryFee())}`;
document.getElementById('cartDelivery').textContent = `${t('delivery_yuan')} ¥${money(cartDeliveryFee())}`;
}
const checkout = document.getElementById('cartCheckout');
checkout.disabled = count === 0;
@@ -1061,7 +1411,7 @@ function renderCartDrawer(){
if(!state.cart.length){
body.innerHTML = `<div class="cd-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg>
<p>购物车空空如也<br>去挑选心仪的美食吧</p></div>`;
<p>${t('cart_empty_drawer')}<br>${t('cart_empty_hint')}</p></div>`;
foot.style.display = 'none';
return;
}
@@ -1101,13 +1451,13 @@ function closeDrawer(){ document.getElementById('cartDrawer').classList.remove('
document.getElementById('cartBtn').onclick = openDrawer;
document.getElementById('cartIcon').onclick = openDrawer;
document.getElementById('cdClose').onclick = closeDrawer;
document.getElementById('cartClear').onclick = ()=>{ if(state.cart.length){ state.cart=[]; refreshCartUI(); toast('购物车已清空'); } };
document.getElementById('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){ toast('购物车为空','error'); return; }
if(!state.cart.length){ toastMsg(t('toast_cart_empty'),'error'); return; }
const sub = cartSubtotal(), del = cartDeliveryFee();
toast(`订单提交成功 · 合计 ¥${money(sub+del)}`);
toastMsg(`${t('order_success_detail')} ¥${money(sub+del)}`);
state.cart = [];
refreshCartUI();
closeDrawer();
@@ -1116,6 +1466,7 @@ function checkout(){
/* ---------- INIT ---------- */
async function init(){
applyI18n();
// featured skeleton immediately
document.getElementById('featTrack').innerHTML = featSkeleton(4);
const [home, cats] = await Promise.all([
@@ -1136,6 +1487,17 @@ async function init(){
await loadList();
renderCartBar();
}
var lastData={};
function onLangChange(){
applyI18n();
renderCats();
renderFeat();
renderList();
renderCartBar();
renderCartDrawer();
}
init();
</script>
</body>