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:
504
web/dist/food2/index.html
vendored
504
web/dist/food2/index.html
vendored
@@ -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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
|
||||
Reference in New Issue
Block a user