Files
web_template/web/dist/shop3/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

1965 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>NORDI · 生活美学严选</title>
<style>
/* ========== 主题变量 · 北欧极简 ========== */
:root {
--bg: #FAFAFA;
--bg-warm: #F5F5F0;
--surface: #FFFFFF;
--text: #2C2C2C;
--text-soft: #5A5A55;
--text-muted: #9A9A93;
--line: #ECECE6;
--line-soft: #F0EFEA;
--sage: #A8C5A0; /* 灰绿 */
--clay: #D4A5A5; /* 灰粉 */
--slate: #A0B8C5; /* 灰蓝 */
--taupe: #C5B8A0; /* 暖灰棕 */
--mauve: #B8A5C5; /* 灰紫 */
--mint: #A0C5B8; /* 灰薄荷 */
--radius: 4px;
--radius-md: 8px;
--radius-lg: 14px;
--serif: Georgia, 'Times New Roman', 'Songti SC', serif;
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--ease: cubic-bezier(0.4, 0, 0.2, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
line-height: 1.7;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 15px;
}
a { text-decoration: none; color: inherit; }
button { cursor: pointer; border: none; background: none; font-family: inherit; color: inherit; }
input { font-family: inherit; outline: none; }
::selection { background: var(--sage); color: #fff; }
.container { max-width: 1280px; margin: 0 auto; padding: 0 40px; }
/* ========== 顶部细条 ========== */
.top-strip {
background: var(--text);
color: #fff;
font-size: 12px;
letter-spacing: 1.5px;
text-align: center;
padding: 9px 0;
font-weight: 300;
}
.top-strip span { opacity: 0.85; }
/* ========== 导航栏 ========== */
.navbar {
background: rgba(250, 250, 250, 0.82);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line-soft);
position: sticky;
top: 0;
z-index: 100;
transition: all 0.3s var(--ease);
}
.nav-inner {
max-width: 1280px;
margin: 0 auto;
padding: 0 40px;
display: flex;
align-items: center;
justify-content: space-between;
height: 76px;
gap: 40px;
}
.logo {
font-family: var(--serif);
font-size: 26px;
font-weight: 400;
letter-spacing: 4px;
color: var(--text);
display: flex;
align-items: center;
gap: 10px;
white-space: nowrap;
}
.logo-mark {
width: 30px; height: 30px;
border: 1.5px solid var(--text);
border-radius: 50%;
position: relative;
flex-shrink: 0;
}
.logo-mark::after {
content: '';
position: absolute;
inset: 6px;
background: var(--sage);
border-radius: 50%;
opacity: 0.55;
}
.nav-search {
flex: 1;
max-width: 420px;
position: relative;
display: flex;
align-items: center;
}
.nav-search input {
width: 100%;
border: none;
border-bottom: 1px solid var(--line);
background: transparent;
padding: 10px 36px 10px 4px;
font-size: 14px;
color: var(--text);
letter-spacing: 0.5px;
transition: border-color 0.3s var(--ease);
}
.nav-search input::placeholder { color: var(--text-muted); font-weight: 300; }
.nav-search input:focus { border-bottom-color: var(--text); }
.nav-search svg {
position: absolute;
right: 6px;
width: 17px; height: 17px;
color: var(--text-muted);
transition: color 0.3s var(--ease);
}
.nav-search input:focus ~ svg { color: var(--text); }
.nav-actions { display: flex; align-items: center; gap: 6px; }
.icon-btn {
width: 42px; height: 42px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
position: relative;
transition: background 0.25s var(--ease);
}
.icon-btn:hover { background: var(--bg-warm); }
.icon-btn svg { width: 21px; height: 21px; stroke-width: 1.4; color: var(--text); }
.cart-badge {
position: absolute;
top: 4px; right: 4px;
background: var(--text);
color: #fff;
font-size: 10px;
font-weight: 500;
min-width: 17px; height: 17px;
border-radius: 9px;
display: none;
align-items: center;
justify-content: center;
padding: 0 4px;
font-family: var(--sans);
}
/* ========== 语言切换器(北欧极简风格)========== */
.lang-switcher{position:relative;margin-left:4px}
.lang-btn{
display:flex;align-items:center;gap:6px;padding:7px 12px;border-radius:var(--radius);
font-size:13px;font-weight:400;color:var(--text-muted);cursor:pointer;
background:transparent;border:1px solid var(--line);
transition:all .25s var(--ease);letter-spacing:0.5px;
}
.lang-btn:hover{color:var(--text);border-color:var(--text-soft);background:var(--bg-warm)}
.lang-btn svg{width:15px;height:15px}
.lang-dropdown{
position:absolute;top:calc(100% + 8px);right:0;min-width:160px;
background:var(--surface);border:1px solid var(--line-soft);
border-radius:var(--radius);box-shadow:0 12px 40px rgba(0,0,0,0.10);overflow:hidden;
opacity:0;transform:translateY(-8px);pointer-events:none;
transition:all .25s var(--ease);z-index:200;
}
.lang-dropdown.open{opacity:1;transform:none;pointer-events:auto}
.lang-option{
display:flex;align-items:center;gap:10px;padding:11px 16px;
font-size:13px;color:var(--text-muted);cursor:pointer;transition:all .2s;letter-spacing:0.3px;
}
.lang-option:hover{background:var(--bg-warm);color:var(--text)}
.lang-option.active{color:var(--text);font-weight:500}
.lang-option .check{width:14px;height:14px;opacity:0;transition:opacity .2s}
.lang-option.active .check{opacity:1}
/* ========== Hero ========== */
.hero {
background: var(--bg-warm);
padding: 90px 0 100px;
position: relative;
overflow: hidden;
}
.hero-grid {
max-width: 1280px;
margin: 0 auto;
padding: 0 40px;
display: grid;
grid-template-columns: 1.05fr 0.95fr;
align-items: center;
gap: 60px;
}
.hero-eyebrow {
font-size: 12px;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--text-soft);
margin-bottom: 26px;
display: flex;
align-items: center;
gap: 14px;
font-weight: 400;
}
.hero-eyebrow::before {
content: '';
width: 36px; height: 1px;
background: var(--text-soft);
}
.hero h1 {
font-family: var(--serif);
font-size: 64px;
font-weight: 400;
line-height: 1.08;
letter-spacing: -1px;
color: var(--text);
margin-bottom: 28px;
}
.hero h1 em { font-style: italic; color: var(--sage); }
.hero-desc {
font-size: 16px;
color: var(--text-soft);
max-width: 440px;
margin-bottom: 44px;
line-height: 1.85;
font-weight: 300;
}
.hero-cta {
display: inline-flex;
align-items: center;
gap: 12px;
border: 1.5px solid var(--text);
color: var(--text);
padding: 15px 38px;
font-size: 13px;
letter-spacing: 2px;
text-transform: uppercase;
border-radius: 0;
transition: all 0.3s var(--ease);
background: transparent;
}
.hero-cta:hover { background: var(--text); color: #fff; }
.hero-cta svg { width: 16px; height: 16px; transition: transform 0.3s var(--ease); }
.hero-cta:hover svg { transform: translateX(4px); }
/* Hero 几何装饰 */
.hero-visual {
position: relative;
height: 420px;
}
.hv-circle {
position: absolute;
border-radius: 50%;
}
.hv-c1 { width: 300px; height: 300px; background: var(--sage); opacity: 0.32; top: 20px; right: 40px; }
.hv-c2 { width: 180px; height: 180px; background: var(--clay); opacity: 0.30; bottom: 30px; left: 30px; }
.hv-c3 { width: 120px; height: 120px; background: var(--surface); border: 1px solid var(--line); top: 130px; left: 110px; }
.hv-ring {
position: absolute;
width: 240px; height: 240px;
border: 1px solid var(--text);
border-radius: 50%;
bottom: 50px; right: 110px;
opacity: 0.5;
}
.hv-line {
position: absolute;
background: var(--text);
opacity: 0.18;
}
.hv-l1 { width: 1px; height: 320px; top: 40px; left: 50%; }
.hv-l2 { width: 200px; height: 1px; top: 60%; right: 20px; }
.hv-dot {
position: absolute;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--slate);
top: 40px; left: 20px;
}
.hv-label {
position: absolute;
font-family: var(--serif);
font-style: italic;
font-size: 14px;
color: var(--text-soft);
bottom: 0; right: 0;
letter-spacing: 1px;
}
/* ========== 特性条 ========== */
.values {
border-bottom: 1px solid var(--line-soft);
background: var(--surface);
}
.values-grid {
max-width: 1280px;
margin: 0 auto;
padding: 34px 40px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
}
.value-item {
display: flex;
align-items: center;
gap: 16px;
}
.value-item svg { width: 28px; height: 28px; color: var(--sage); flex-shrink: 0; stroke-width: 1.3; }
.value-item h4 { font-size: 13px; font-weight: 500; letter-spacing: 0.5px; }
.value-item p { font-size: 12px; color: var(--text-muted); font-weight: 300; }
/* ========== 区块通用 ========== */
.section {
max-width: 1280px;
margin: 0 auto;
padding: 0 40px;
}
.section-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 44px;
gap: 24px;
}
.section-title {
font-family: var(--serif);
font-size: 36px;
font-weight: 400;
letter-spacing: -0.5px;
line-height: 1.1;
}
.section-sub {
font-size: 13px;
color: var(--text-muted);
margin-top: 8px;
letter-spacing: 0.5px;
font-weight: 300;
}
.section-link {
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text);
border-bottom: 1px solid var(--text);
padding-bottom: 3px;
transition: opacity 0.25s var(--ease);
white-space: nowrap;
}
.section-link:hover { opacity: 0.5; }
/* ========== 分类导航 ========== */
.category-bar {
border-top: 1px solid var(--line-soft);
border-bottom: 1px solid var(--line-soft);
background: var(--surface);
position: sticky;
top: 76px;
z-index: 50;
}
.category-inner {
max-width: 1280px;
margin: 0 auto;
padding: 0 40px;
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
scrollbar-width: none;
}
.category-inner::-webkit-scrollbar { display: none; }
.cat-label {
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-muted);
padding-right: 24px;
border-right: 1px solid var(--line);
margin-right: 8px;
white-space: nowrap;
font-weight: 400;
}
.cat-chip {
padding: 20px 4px;
margin: 0 16px;
font-size: 14px;
color: var(--text-soft);
white-space: nowrap;
position: relative;
transition: color 0.25s var(--ease);
letter-spacing: 0.5px;
}
.cat-chip::after {
content: '';
position: absolute;
left: 0; right: 0; bottom: 0;
height: 1.5px;
background: var(--text);
transform: scaleX(0);
transition: transform 0.3s var(--ease);
}
.cat-chip:hover { color: var(--text); }
.cat-chip.active { color: var(--text); }
.cat-chip.active::after { transform: scaleX(1); }
/* ========== 商品区 ========== */
.shop { padding: 70px 0 90px; }
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40px;
flex-wrap: wrap;
gap: 16px;
}
.toolbar-count {
font-size: 13px;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.toolbar-count strong { color: var(--text); font-weight: 500; }
.sort-group {
display: flex;
align-items: center;
gap: 28px;
}
.sort-btn {
font-size: 13px;
color: var(--text-muted);
letter-spacing: 1px;
padding: 6px 0;
position: relative;
transition: color 0.25s var(--ease);
}
.sort-btn:hover { color: var(--text); }
.sort-btn.active { color: var(--text); }
.sort-btn.active::after {
content: '';
position: absolute;
left: 0; right: 0; bottom: -6px;
height: 1px;
background: var(--text);
}
.products-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 44px 32px;
}
.product-card {
cursor: pointer;
transition: transform 0.4s var(--ease);
}
.product-card:hover { transform: translateY(-6px); }
.product-img {
width: 100%;
aspect-ratio: 4 / 5;
border-radius: var(--radius-md);
overflow: hidden;
position: relative;
border: 1px solid transparent;
transition: border-color 0.4s var(--ease);
display: flex;
align-items: center;
justify-content: center;
}
.product-card:hover .product-img { border-color: var(--line); }
.product-img-fill {
width: 100%; height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.product-img-fill svg {
width: 46%; height: 46%;
opacity: 0.42;
stroke-width: 1;
transition: transform 0.5s var(--ease), opacity 0.4s var(--ease);
}
.product-card:hover .product-img-fill svg { transform: scale(1.06); opacity: 0.55; }
.product-tags {
position: absolute;
top: 14px; left: 14px;
display: flex;
flex-direction: column;
gap: 6px;
align-items: flex-start;
}
.product-tag {
font-size: 10px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--text);
background: rgba(255,255,255,0.86);
padding: 3px 9px;
border-radius: 2px;
font-weight: 500;
backdrop-filter: blur(4px);
}
.product-tag.t-sage { color: #5e7a57; }
.product-tag.t-clay { color: #9a6363; }
.product-tag.t-slate { color: #5c7480; }
.product-wish {
position: absolute;
top: 12px; right: 12px;
width: 32px; height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.8);
display: flex; align-items: center; justify-content: center;
opacity: 0;
transform: translateY(-4px);
transition: all 0.3s var(--ease);
backdrop-filter: blur(4px);
}
.product-card:hover .product-wish { opacity: 1; transform: translateY(0); }
.product-wish svg { width: 16px; height: 16px; stroke-width: 1.4; color: var(--text); }
.product-wish:hover svg { fill: var(--clay); color: var(--clay); }
.product-info { padding: 18px 2px 0; }
.product-cat {
font-size: 11px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 7px;
font-weight: 400;
}
.product-name {
font-size: 14px;
font-weight: 400;
line-height: 1.5;
color: var(--text);
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 42px;
transition: color 0.25s var(--ease);
}
.product-card:hover .product-name { color: var(--text); }
.product-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.product-price { display: flex; align-items: baseline; gap: 8px; }
.price-now {
font-family: var(--serif);
font-size: 19px;
color: var(--text);
font-weight: 400;
}
.price-was {
font-size: 12px;
color: var(--text-muted);
text-decoration: line-through;
font-weight: 300;
}
.product-rate {
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 4px;
}
.product-rate svg { width: 12px; height: 12px; color: var(--taupe); }
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 6px;
margin-top: 70px;
}
.page-btn {
min-width: 40px; height: 40px;
border: 1px solid var(--line);
background: var(--surface);
border-radius: 0;
font-size: 13px;
color: var(--text-soft);
transition: all 0.25s var(--ease);
display: flex; align-items: center; justify-content: center;
padding: 0 14px;
}
.page-btn:hover:not(:disabled) { border-color: var(--text); color: var(--text); }
.page-btn.active { background: var(--text); color: #fff; border-color: var(--text); }
.page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.page-dots { color: var(--text-muted); padding: 0 4px; }
/* 骨架屏 */
.skeleton-card { display: flex; flex-direction: column; gap: 14px; }
.sk-box {
background: linear-gradient(90deg, #F0EFEA 25%, #F7F6F1 37%, #F0EFEA 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
border-radius: var(--radius-md);
}
.sk-img { width: 100%; aspect-ratio: 4/5; }
.sk-line { height: 12px; border-radius: 3px; }
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
/* 空状态 */
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 90px 20px;
color: var(--text-muted);
}
.empty-state svg { width: 56px; height: 56px; margin: 0 auto 22px; opacity: 0.3; stroke-width: 1; }
.empty-state h3 { font-family: var(--serif); font-size: 22px; font-weight: 400; color: var(--text-soft); margin-bottom: 8px; }
.empty-state p { font-size: 14px; font-weight: 300; }
/* ========== 精选区 ========== */
.featured { background: var(--bg-warm); padding: 80px 0; }
.featured .section { padding: 0 40px; }
.featured-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
/* ========== 模态框 ========== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(44,44,44,0.45);
backdrop-filter: blur(6px);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 30px;
opacity: 0;
transition: opacity 0.3s var(--ease);
}
.modal-overlay.open { display: flex; opacity: 1; }
.modal {
background: var(--surface);
border-radius: var(--radius-lg);
max-width: 920px;
width: 100%;
max-height: 90vh;
overflow: hidden;
display: grid;
grid-template-columns: 1fr 1fr;
transform: translateY(20px) scale(0.98);
transition: transform 0.4s var(--ease);
position: relative;
}
.modal-overlay.open .modal { transform: translateY(0) scale(1); }
.modal-close {
position: absolute;
top: 18px; right: 18px;
width: 38px; height: 38px;
border-radius: 50%;
background: rgba(255,255,255,0.9);
display: flex; align-items: center; justify-content: center;
z-index: 5;
font-size: 20px;
color: var(--text);
transition: background 0.25s var(--ease);
}
.modal-close:hover { background: var(--bg-warm); }
.modal-img {
background: var(--bg-warm);
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
}
.modal-img svg { width: 42%; height: 42%; opacity: 0.4; stroke-width: 1; }
.modal-body { padding: 50px 46px; overflow-y: auto; display: flex; flex-direction: column; }
.modal-cat { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 12px; }
.modal-body h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; line-height: 1.2; margin-bottom: 18px; color: var(--text); }
.modal-tags { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
.modal-price { display: flex; align-items: baseline; gap: 12px; margin-bottom: 24px; padding-bottom: 24px; border-bottom: 1px solid var(--line-soft); }
.modal-price .price-now { font-size: 30px; }
.modal-desc { font-size: 14px; color: var(--text-soft); line-height: 1.9; margin-bottom: 26px; font-weight: 300; }
.modal-meta { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 34px; }
.modal-meta-item { text-align: center; padding: 14px 6px; border: 1px solid var(--line-soft); border-radius: var(--radius); }
.modal-meta-item .mm-val { font-family: var(--serif); font-size: 18px; color: var(--text); display: block; }
.modal-meta-item .mm-label { font-size: 11px; letter-spacing: 1px; color: var(--text-muted); text-transform: uppercase; }
.modal-qty { display: flex; align-items: center; gap: 18px; margin-bottom: 28px; }
.modal-qty-label { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: var(--text-muted); }
.qty-control { display: flex; align-items: center; border: 1px solid var(--line); }
.qty-control button { width: 38px; height: 38px; font-size: 16px; color: var(--text-soft); transition: background 0.2s var(--ease); }
.qty-control button:hover { background: var(--bg-warm); color: var(--text); }
.qty-control span { width: 44px; text-align: center; font-size: 14px; }
.modal-add-cart {
background: var(--text);
color: #fff;
padding: 17px 40px;
font-size: 13px;
letter-spacing: 2px;
text-transform: uppercase;
border-radius: 0;
transition: all 0.3s var(--ease);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.modal-add-cart:hover { background: #000; transform: translateY(-2px); }
.modal-add-cart svg { width: 17px; height: 17px; }
/* ========== 购物车抽屉 ========== */
.cart-overlay {
position: fixed;
inset: 0;
background: rgba(44,44,44,0.4);
backdrop-filter: blur(4px);
z-index: 180;
opacity: 0;
visibility: hidden;
transition: all 0.3s var(--ease);
}
.cart-overlay.open { opacity: 1; visibility: visible; }
.cart-drawer {
position: fixed;
top: 0; right: 0; bottom: 0;
width: 420px;
max-width: 90vw;
background: var(--surface);
z-index: 190;
transform: translateX(100%);
transition: transform 0.4s var(--ease);
display: flex;
flex-direction: column;
}
.cart-drawer.open { transform: translateX(0); }
.cart-header {
padding: 26px 32px;
border-bottom: 1px solid var(--line-soft);
display: flex;
align-items: center;
justify-content: space-between;
}
.cart-header h3 { font-family: var(--serif); font-size: 22px; font-weight: 400; letter-spacing: 0.5px; }
.cart-header .close-x { font-size: 22px; color: var(--text-muted); transition: color 0.2s var(--ease); width: 34px; height: 34px; }
.cart-header .close-x:hover { color: var(--text); }
.cart-items { flex: 1; overflow-y: auto; padding: 12px 32px; }
.cart-empty {
text-align: center;
padding: 80px 20px;
color: var(--text-muted);
}
.cart-empty svg { width: 52px; height: 52px; margin: 0 auto 20px; opacity: 0.3; stroke-width: 1; }
.cart-empty p { font-size: 14px; font-weight: 300; }
.cart-empty .ce-sub { font-size: 12px; margin-top: 6px; }
.cart-item {
display: flex;
gap: 16px;
padding: 22px 0;
border-bottom: 1px solid var(--line-soft);
}
.cart-item-img {
width: 78px; height: 96px;
border-radius: var(--radius);
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: var(--bg-warm);
}
.cart-item-img svg { width: 40%; height: 40%; opacity: 0.4; stroke-width: 1; }
.cart-item-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.cart-item-name { font-size: 13px; line-height: 1.5; margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.cart-item-price { font-family: var(--serif); font-size: 16px; color: var(--text); margin-bottom: 12px; }
.cart-item-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }
.cart-qty { display: flex; align-items: center; border: 1px solid var(--line); }
.cart-qty button { width: 28px; height: 28px; font-size: 14px; color: var(--text-soft); transition: background 0.2s var(--ease); }
.cart-qty button:hover { background: var(--bg-warm); }
.cart-qty span { width: 34px; text-align: center; font-size: 13px; }
.cart-remove { font-size: 11px; color: var(--text-muted); letter-spacing: 0.5px; transition: color 0.2s var(--ease); text-decoration: underline; text-underline-offset: 3px; }
.cart-remove:hover { color: var(--clay); }
.cart-footer { padding: 26px 32px; border-top: 1px solid var(--line-soft); }
.cart-summary { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 22px; }
.cart-summary .cs-label { font-size: 13px; color: var(--text-soft); letter-spacing: 1px; }
.cart-summary .cs-total { font-family: var(--serif); font-size: 28px; color: var(--text); }
.cart-note { font-size: 11px; color: var(--text-muted); margin-bottom: 18px; letter-spacing: 0.5px; }
.cart-checkout {
width: 100%;
background: var(--text);
color: #fff;
padding: 17px;
font-size: 13px;
letter-spacing: 2px;
text-transform: uppercase;
transition: all 0.3s var(--ease);
}
.cart-checkout:hover:not(:disabled) { background: #000; }
.cart-checkout:disabled { opacity: 0.3; cursor: not-allowed; }
/* ========== 页脚 ========== */
.footer { background: var(--bg-warm); border-top: 1px solid var(--line-soft); padding: 70px 0 36px; }
.footer-grid {
max-width: 1280px;
margin: 0 auto;
padding: 0 40px;
display: grid;
grid-template-columns: 1.6fr 1fr 1fr 1fr 1.4fr;
gap: 50px;
margin-bottom: 56px;
}
.footer-brand h4 { font-family: var(--serif); font-size: 24px; font-weight: 400; letter-spacing: 3px; margin-bottom: 16px; }
.footer-brand p { font-size: 13px; color: var(--text-soft); line-height: 1.8; font-weight: 300; max-width: 280px; }
.footer-col h5 { font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--text); margin-bottom: 20px; font-weight: 500; }
.footer-col a { display: block; font-size: 13px; color: var(--text-soft); margin-bottom: 12px; font-weight: 300; transition: color 0.2s var(--ease); }
.footer-col a:hover { color: var(--text); }
.footer-news p { font-size: 13px; color: var(--text-soft); margin-bottom: 16px; font-weight: 300; line-height: 1.7; }
.news-form { display: flex; border-bottom: 1px solid var(--text); }
.news-form input { flex: 1; border: none; background: transparent; padding: 8px 0; font-size: 13px; color: var(--text); }
.news-form input::placeholder { color: var(--text-muted); }
.news-form button { padding: 0 6px; color: var(--text); }
.news-form button svg { width: 18px; height: 18px; }
.footer-bottom {
max-width: 1280px;
margin: 0 auto;
padding: 24px 40px 0;
border-top: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-muted);
letter-spacing: 0.5px;
font-weight: 300;
}
.footer-bottom .fb-links { display: flex; gap: 24px; }
.footer-bottom .fb-links a:hover { color: var(--text); }
/* ========== Toast ========== */
.toast {
position: fixed;
bottom: 36px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--text);
color: #fff;
padding: 15px 32px;
border-radius: 0;
font-size: 13px;
letter-spacing: 0.5px;
z-index: 300;
opacity: 0;
visibility: hidden;
transition: all 0.35s var(--ease);
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 12px 40px rgba(0,0,0,0.18);
font-weight: 300;
}
.toast.show { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
.toast svg { width: 16px; height: 16px; color: var(--sage); }
/* ========== 响应式 ========== */
@media (max-width: 1100px) {
.products-grid, .featured-grid { grid-template-columns: repeat(3, 1fr); }
.footer-grid { grid-template-columns: 1fr 1fr 1fr; gap: 36px; }
.footer-brand { grid-column: 1 / -1; }
.footer-news { grid-column: 1 / -1; }
}
@media (max-width: 860px) {
.container, .nav-inner, .hero-grid, .values-grid, .section, .category-inner, .footer-grid, .footer-bottom { padding-left: 24px; padding-right: 24px; }
.nav-inner { height: 64px; gap: 16px; }
.nav-search { max-width: none; }
.top-strip { font-size: 11px; letter-spacing: 1px; }
.hero { padding: 56px 0 64px; }
.hero-grid { grid-template-columns: 1fr; gap: 40px; }
.hero h1 { font-size: 40px; }
.hero-visual { height: 280px; }
.hv-c1 { width: 200px; height: 200px; }
.hv-c2 { width: 130px; height: 130px; }
.hv-c3 { width: 90px; height: 90px; }
.hv-ring { width: 170px; height: 170px; }
.values-grid { grid-template-columns: 1fr 1fr; gap: 22px; }
.section-title { font-size: 28px; }
.products-grid, .featured-grid { grid-template-columns: repeat(2, 1fr); gap: 32px 20px; }
.category-bar { top: 64px; }
.cat-label { display: none; }
.cat-chip { margin: 0 12px; }
.modal { grid-template-columns: 1fr; max-height: 92vh; }
.modal-img { min-height: 240px; }
.modal-body { padding: 32px 28px; }
.footer-grid { grid-template-columns: 1fr 1fr; }
.footer-brand, .footer-news { grid-column: 1 / -1; }
.footer-bottom { flex-direction: column; gap: 12px; text-align: center; }
.sort-group { gap: 18px; }
.lang-switcher{position:static;margin-left:0}
.lang-dropdown{right:auto;left:0}
}
@media (max-width: 480px) {
.products-grid, .featured-grid { grid-template-columns: 1fr; }
.hero h1 { font-size: 34px; }
.section-head { flex-direction: column; align-items: flex-start; }
.toolbar { flex-direction: column; align-items: flex-start; }
.modal-meta { grid-template-columns: 1fr 1fr 1fr; }
}
/* ========== 3D 动效 & 鼠标光影(北欧极简 · 克制优雅)========== */
/* 全局鼠标跟随柔光斑(灰粉 → 灰绿 → 透明,multiply 柔和叠加)*/
.cursor-glow{
position:fixed;
top:0; left:0;
width:500px; height:500px;
border-radius:50%;
background:radial-gradient(circle, rgba(212,165,165,0.10) 0%, rgba(168,197,160,0.06) 40%, transparent 70%);
pointer-events:none;
z-index:150; /* 高于导航栏(100),低于所有弹窗(180+) */
transform:translate(-50%,-50%);
mix-blend-mode:multiply;
filter:blur(10px);
will-change:transform;
}
/* 3D 透视容器 */
.products-grid{ perspective:1000px; }
.featured-grid{ perspective:1000px; }
/* 卡片 3D 倾斜基础(preserve-3d + 柔和过渡)*/
.product-card{
position:relative;
transform-style:preserve-3d;
transition:transform .3s var(--ease), box-shadow .3s var(--ease);
}
.product-card .product-img{ transform:translateZ(20px); }
.product-card .product-info{ transform:translateZ(12px); }
/* 卡片表面柔和高光反射(::after 跟随鼠标位置,浮于卡片最前层)*/
.product-card::after{
content:'';
position:absolute;
inset:0;
border-radius:inherit;
background:radial-gradient(circle at var(--mx,50%) var(--my,50%), rgba(255,255,255,0.40) 0%, transparent 50%);
opacity:0;
transition:opacity .4s var(--ease);
pointer-events:none;
z-index:2;
transform:translateZ(30px);
}
.product-card:hover::after{ opacity:1; }
/* 页面入场动画(各区块淡入上滑,错开延迟)*/
@keyframes fadeInUp{ from{ opacity:0; transform:translateY(30px); } to{ opacity:1; transform:translateY(0); } }
.top-strip{ animation:fadeInUp .8s var(--ease) both; animation-delay:0s; }
.navbar{ animation:fadeInUp .8s var(--ease) both; animation-delay:.10s; }
.hero{ animation:fadeInUp .8s var(--ease) both; animation-delay:.20s; }
.values{ animation:fadeInUp .8s var(--ease) both; animation-delay:.30s; }
.category-bar{ animation:fadeInUp .8s var(--ease) both; animation-delay:.40s; }
.shop{ animation:fadeInUp .8s var(--ease) both; animation-delay:.50s; }
.featured{ animation:fadeInUp .8s var(--ease) both; animation-delay:.60s; }
.footer{ animation:fadeInUp .8s var(--ease) both; animation-delay:.70s; }
/* 移动端关闭光斑 / 尊重「减弱动效」偏好 */
@media (max-width: 860px){ .cursor-glow{ display:none; } }
@media (prefers-reduced-motion: reduce){
.cursor-glow{ display:none; }
.top-strip,.navbar,.hero,.values,.category-bar,.shop,.featured,.footer{ animation:none; }
.product-card{ transition:none; }
.product-card::after{ transition:none; }
}
</style>
</head>
<body>
<!-- 顶部细条 -->
<div class="top-strip"><span data-i18n="top_strip">全球免运 · 满 $99 包邮 · 30 天无忧退换 · 全线正品保证</span></div>
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-inner">
<a href="#" class="logo"><span class="logo-mark"></span>NORDI</a>
<div class="nav-search">
<input type="text" id="searchInput" data-i18n-placeholder="search_placeholder" placeholder="搜索生活好物…" autocomplete="off">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>
</div>
<div class="nav-actions">
<button class="icon-btn" data-i18n-title="title_favorites" title="收藏">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</button>
<button class="icon-btn" data-i18n-title="title_cart" title="购物车" onclick="toggleCart()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></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="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></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 -->
<section class="hero">
<div class="hero-grid">
<div class="hero-text">
<div class="hero-eyebrow">NORDIC · LIVING · 2026</div>
<h1>Lifestyle<br><em>Collection</em></h1>
<p class="hero-desc" data-i18n="hero_desc">源自北欧的极简生活哲学。精选全球匠心好物,以克制的设计与温润的质感,为日常注入从容与诗意。</p>
<button class="hero-cta" onclick="scrollToShop()"><span data-i18n="hero_cta">探索精选</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</button>
</div>
<div class="hero-visual">
<div class="hv-line hv-l1"></div>
<div class="hv-line hv-l2"></div>
<div class="hv-circle hv-c1"></div>
<div class="hv-ring"></div>
<div class="hv-circle hv-c2"></div>
<div class="hv-circle hv-c3"></div>
<div class="hv-dot"></div>
<div class="hv-label">— Slow Living</div>
</div>
</div>
</section>
<!-- 特性条 -->
<section class="values">
<div class="values-grid">
<div class="value-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<div><h4 data-i18n="val_global_title">全球直采</h4><p data-i18n="val_global_desc">原产地严选溯源</p></div>
</div>
<div class="value-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="1" y="3" width="15" height="13"/><path d="M16 8h4l3 3v5h-7"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
<div><h4 data-i18n="val_shipping_title">极速跨境</h4><p data-i18n="val_shipping_desc">平均 5 日达</p></div>
</div>
<div class="value-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
<div><h4 data-i18n="val_auth_title">正品保证</h4><p data-i18n="val_auth_desc">假一赔十承诺</p></div>
</div>
<div class="value-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
<div><h4 data-i18n="val_return_title">30天退换</h4><p data-i18n="val_return_desc">无忧售后保障</p></div>
</div>
</div>
</section>
<!-- 分类导航 -->
<div class="category-bar">
<div class="category-inner" id="categoryChips">
<span class="cat-label" data-i18n="cat_label">分类</span>
<span class="cat-chip active" onclick="selectCategory(0)" data-i18n="cat_all">全部</span>
</div>
</div>
<!-- 商品区 -->
<section class="shop" id="shop">
<div class="section">
<div class="section-head">
<div>
<div class="section-title">Curated Goods</div>
<div class="section-sub" data-i18n="shop_sub">每一件,皆经严选 / Every piece, thoughtfully chosen</div>
</div>
<a href="#" class="section-link" data-i18n="view_featured" onclick="loadFeatured();return false;">查看精选推荐</a>
</div>
<div class="toolbar">
<div class="toolbar-count" id="productsCount">— 件商品</div>
<div class="sort-group">
<button class="sort-btn active" data-sort="" onclick="changeSort('')" data-i18n="sort_recommend">推荐</button>
<button class="sort-btn" data-sort="sales" onclick="changeSort('sales')" data-i18n="sort_best_selling">畅销</button>
<button class="sort-btn" data-sort="price" onclick="changeSort('price')" data-i18n="sort_price">价格</button>
<button class="sort-btn" data-sort="rating" onclick="changeSort('rating')" data-i18n="sort_rating">好评</button>
</div>
</div>
<div class="products-grid" id="productsGrid"></div>
<div class="pagination" id="pagination"></div>
</div>
</section>
<!-- 精选区 -->
<section class="featured">
<div class="section">
<div class="section-head">
<div>
<div class="section-title">Editor's Pick</div>
<div class="section-sub" data-i18n="featured_sub">编辑精选 · 本季值得拥有的好物</div>
</div>
</div>
<div class="featured-grid" id="featuredGrid"></div>
</div>
</section>
<!-- 页脚 -->
<footer class="footer">
<div class="footer-grid">
<div class="footer-brand">
<h4>NORDI</h4>
<p data-i18n="footer_brand_desc">以斯堪的纳维亚的极简美学,为世界各地的家,带来经久耐用的生活良品。Less, but better.</p>
</div>
<div class="footer-col">
<h5 data-i18n="footer_shop">购物</h5>
<a href="#" onclick="selectCategory(0);return false;" data-i18n="footer_all_products">全部商品</a>
<a href="#" onclick="loadFeatured();return false;" data-i18n="footer_editors_pick">编辑精选</a>
<a href="#" data-i18n="footer_new_arrivals">新品上架</a>
<a href="#" data-i18n="footer_deals">限时优惠</a>
</div>
<div class="footer-col">
<h5 data-i18n="footer_service">服务</h5>
<a href="#" data-i18n="footer_shipping_info">配送说明</a>
<a href="#" data-i18n="footer_return_policy">退换政策</a>
<a href="#" data-i18n="footer_size_guide">尺码指南</a>
<a href="#" data-i18n="footer_faq">常见问题</a>
</div>
<div class="footer-col">
<h5 data-i18n="footer_about">关于</h5>
<a href="#" data-i18n="footer_brand_story">品牌故事</a>
<a href="#" data-i18n="footer_sustainability">可持续</a>
<a href="#" data-i18n="footer_contact_us">联系我们</a>
<a href="#" data-i18n="footer_privacy">隐私条款</a>
</div>
<div class="footer-col footer-news">
<h5 data-i18n="footer_subscribe">订阅</h5>
<p data-i18n="footer_newsletter_desc">订阅 Newsletter,获取新品资讯与会员专属礼遇。</p>
<form class="news-form" onsubmit="event.preventDefault();showToast(t('subscribe_success'));this.reset();">
<input type="email" data-i18n-placeholder="footer_email_placeholder" placeholder="您的邮箱地址" required>
<button type="submit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></button>
</form>
</div>
</div>
<div class="footer-bottom">
<span>© 2026 NORDI. All rights reserved. · <span data-i18n="footer_tagline">以克制之美,抵达日常</span></span>
<div class="fb-links">
<a href="#">Instagram</a>
<a href="#">Pinterest</a>
<a href="#" data-i18n="social_wechat">微信</a>
</div>
</div>
</footer>
<!-- 商品详情模态框 -->
<div class="modal-overlay" id="productModal" onclick="closeModal(event)">
<div class="modal" id="modalContent"></div>
</div>
<!-- 购物车抽屉 -->
<div class="cart-overlay" id="cartOverlay" onclick="toggleCart()"></div>
<aside class="cart-drawer" id="cartDrawer">
<div class="cart-header">
<h3 data-i18n="cart_title">购物袋</h3>
<button class="close-x" onclick="toggleCart()">&times;</button>
</div>
<div class="cart-items" id="cartItems"></div>
<div class="cart-footer">
<div class="cart-summary">
<span class="cs-label" data-i18n="cart_total_label">合计</span>
<span class="cs-total" id="cartTotal">$0.00</span>
</div>
<div class="cart-note" data-i18n="cart_note">含税 · 运费于结算时计算</div>
<button class="cart-checkout" id="checkoutBtn" onclick="checkout()" disabled data-i18n="cart_checkout">前往结算</button>
</div>
</aside>
<!-- Toast -->
<div class="toast" id="toast">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"/></svg>
<span id="toastMsg"></span>
</div>
<script>
/* =========================================================
国际化(i18n)引擎
========================================================= */
const I18N_LOCALES = {
'zh-CN': {
top_strip: '全球免运 · 满 $99 包邮 · 30 天无忧退换 · 全线正品保证',
search_placeholder: '搜索生活好物…',
title_favorites: '收藏',
title_cart: '购物车',
hero_desc: '源自北欧的极简生活哲学。精选全球匠心好物,以克制的设计与温润的质感,为日常注入从容与诗意。',
hero_cta: '探索精选',
val_global_title: '全球直采',
val_global_desc: '原产地严选溯源',
val_shipping_title: '极速跨境',
val_shipping_desc: '平均 5 日达',
val_auth_title: '正品保证',
val_auth_desc: '假一赔十承诺',
val_return_title: '30天退换',
val_return_desc: '无忧售后保障',
cat_label: '分类',
cat_all: '全部',
shop_sub: '每一件,皆经严选 / Every piece, thoughtfully chosen',
view_featured: '查看精选推荐',
sort_recommend: '推荐',
sort_best_selling: '畅销',
sort_price: '价格',
sort_rating: '好评',
products_count: '共 {n} 件商品',
products_count_featured: '编辑精选 · {n} 件商品',
featured_sub: '编辑精选 · 本季值得拥有的好物',
footer_brand_desc: '以斯堪的纳维亚的极简美学,为世界各地的家,带来经久耐用的生活良品。Less, but better.',
footer_shop: '购物',
footer_all_products: '全部商品',
footer_editors_pick: '编辑精选',
footer_new_arrivals: '新品上架',
footer_deals: '限时优惠',
footer_service: '服务',
footer_shipping_info: '配送说明',
footer_return_policy: '退换政策',
footer_size_guide: '尺码指南',
footer_faq: '常见问题',
footer_about: '关于',
footer_brand_story: '品牌故事',
footer_sustainability: '可持续',
footer_contact_us: '联系我们',
footer_privacy: '隐私条款',
footer_subscribe: '订阅',
footer_newsletter_desc: '订阅 Newsletter,获取新品资讯与会员专属礼遇。',
footer_email_placeholder: '您的邮箱地址',
footer_tagline: '以克制之美,抵达日常',
social_wechat: '微信',
cart_title: '购物袋',
cart_total_label: '合计',
cart_note: '含税 · 运费于结算时计算',
cart_checkout: '前往结算',
cart_empty_title: '购物袋还是空的',
cart_empty_desc: '去挑选心仪的好物吧',
cart_remove: '移除',
modal_loading: '载入中…',
modal_nordi_pick: 'NORDI 严选',
modal_save_pct: '省 {n}%',
modal_rating: '评分',
modal_sales: '销量',
modal_stock: '库存',
modal_qty: '数量',
modal_add_cart: '加入购物袋',
modal_desc_fallback: '每一处细节皆经匠心打磨,以克制的线条与温润的质感,融入你的日常生活。',
modal_load_error: '商品详情加载失败,请稍后重试',
error_load_failed: '加载失败',
error_load_desc: '商品信息暂时无法获取,请稍后重试',
error_network: '网络异常',
error_request_failed: '请求失败',
error_category_load: '分类加载失败',
empty_title: '暂无商品',
empty_desc: '没有找到匹配的商品,换个关键词试试',
toast_wishlist: '已加入心愿单',
toast_added_to_cart: '已加入购物袋 · {name}',
toast_add_failed: '添加失败,请重试',
toast_featured_loading: '正在为你挑选编辑精选…',
toast_featured_failed: '精选加载失败,请稍后重试',
toast_order_submitted: '订单已提交 · 合计 {total} · 感谢惠顾',
toast_subscribe_success: '订阅成功,感谢关注',
category_default: '生活好物',
page_title: '生活美学严选'
},
'en': {
top_strip: 'Free Shipping · Free on $99+ · 30-Day Returns · Authentic Guarantee',
search_placeholder: 'Search lifestyle goods…',
title_favorites: 'Favorites',
title_cart: 'Cart',
hero_desc: 'Nordic-inspired minimalist living philosophy. Thoughtfully curated goods from around the world, bringing calm and poetry to your everyday life through restrained design and warm textures.',
hero_cta: 'Explore Collection',
val_global_title: 'Global Sourcing',
val_global_desc: 'Direct from origin',
val_shipping_title: 'Express Cross-border',
val_shipping_desc: 'Avg. 5-day delivery',
val_auth_title: 'Authentic Guarantee',
val_auth_desc: '10× compensation for fakes',
val_return_title: '30-Day Returns',
val_return_desc: 'Worry-free after-sales',
cat_label: 'Categories',
cat_all: 'All',
shop_sub: 'Every piece, thoughtfully chosen',
view_featured: 'View Editor\'s Picks',
sort_recommend: 'Recommended',
sort_best_selling: 'Best Selling',
sort_price: 'Price',
sort_rating: 'Top Rated',
products_count: '{n} items',
products_count_featured: 'Editor\'s Picks · {n} items',
featured_sub: 'Editor\'s Picks · Must-haves this season',
footer_brand_desc: 'Scandinavian minimalist aesthetics, bringing durable lifestyle goods to homes around the world. Less, but better.',
footer_shop: 'Shop',
footer_all_products: 'All Products',
footer_editors_pick: 'Editor\'s Picks',
footer_new_arrivals: 'New Arrivals',
footer_deals: 'Deals',
footer_service: 'Service',
footer_shipping_info: 'Shipping Info',
footer_return_policy: 'Return Policy',
footer_size_guide: 'Size Guide',
footer_faq: 'FAQ',
footer_about: 'About',
footer_brand_story: 'Brand Story',
footer_sustainability: 'Sustainability',
footer_contact_us: 'Contact Us',
footer_privacy: 'Privacy Policy',
footer_subscribe: 'Subscribe',
footer_newsletter_desc: 'Subscribe to our Newsletter for new arrivals and exclusive member perks.',
footer_email_placeholder: 'Your email address',
footer_tagline: 'Refined simplicity for everyday living',
social_wechat: 'WeChat',
cart_title: 'Shopping Bag',
cart_total_label: 'Subtotal',
cart_note: 'Tax included · Shipping calculated at checkout',
cart_checkout: 'Proceed to Checkout',
cart_empty_title: 'Your bag is empty',
cart_empty_desc: 'Discover something you love',
cart_remove: 'Remove',
modal_loading: 'Loading…',
modal_nordi_pick: 'NORDI Curated',
modal_save_pct: 'Save {n}%',
modal_rating: 'Rating',
modal_sales: 'Sales',
modal_stock: 'Stock',
modal_qty: 'Quantity',
modal_add_cart: 'Add to Bag',
modal_desc_fallback: 'Every detail is meticulously crafted, blending restrained lines and warm textures into your daily life.',
modal_load_error: 'Failed to load details, please try again later',
error_load_failed: 'Load Failed',
error_load_desc: 'Product info temporarily unavailable, please try again later',
error_network: 'Network Error',
error_request_failed: 'Request Failed',
error_category_load: 'Failed to load categories',
empty_title: 'No Products',
empty_desc: 'No matching products found, try different keywords',
toast_wishlist: 'Added to wishlist',
toast_added_to_cart: 'Added to bag · {name}',
toast_add_failed: 'Failed to add, please try again',
toast_featured_loading: 'Curating editor\'s picks for you…',
toast_featured_failed: 'Failed to load picks, please try again later',
toast_order_submitted: 'Order placed · Total {total} · Thank you',
toast_subscribe_success: 'Subscribed, thank you!',
category_default: 'Lifestyle',
page_title: 'Lifestyle Curated'
},
'zh-TW': {
top_strip: '全球免運 · 滿 $99 包郵 · 30 天無憂退換 · 全線正品保證',
search_placeholder: '搜尋生活好物…',
title_favorites: '收藏',
title_cart: '購物車',
hero_desc: '源自北歐的極簡生活哲學。精選全球匠心好物,以克制的設計與溫潤的質感,為日常注入從容與詩意。',
hero_cta: '探索精選',
val_global_title: '全球直採',
val_global_desc: '原產地嚴選溯源',
val_shipping_title: '極速跨境',
val_shipping_desc: '平均 5 日達',
val_auth_title: '正品保證',
val_auth_desc: '假一賠十承諾',
val_return_title: '30天退換',
val_return_desc: '無憂售後保障',
cat_label: '分類',
cat_all: '全部',
shop_sub: '每一件,皆經嚴選 / Every piece, thoughtfully chosen',
view_featured: '查看精選推薦',
sort_recommend: '推薦',
sort_best_selling: '暢銷',
sort_price: '價格',
sort_rating: '好評',
products_count: '共 {n} 件商品',
products_count_featured: '編輯精選 · {n} 件商品',
featured_sub: '編輯精選 · 本季值得擁有的好物',
footer_brand_desc: '以斯堪的納維亞的極簡美學,為世界各地的家,帶來經久耐用的生活良品。Less, but better.',
footer_shop: '購物',
footer_all_products: '全部商品',
footer_editors_pick: '編輯精選',
footer_new_arrivals: '新品上架',
footer_deals: '限時優惠',
footer_service: '服務',
footer_shipping_info: '配送說明',
footer_return_policy: '退換政策',
footer_size_guide: '尺碼指南',
footer_faq: '常見問題',
footer_about: '關於',
footer_brand_story: '品牌故事',
footer_sustainability: '可持續',
footer_contact_us: '聯絡我們',
footer_privacy: '私隱條款',
footer_subscribe: '訂閱',
footer_newsletter_desc: '訂閱 Newsletter,獲取新品資訊與會員專屬禮遇。',
footer_email_placeholder: '您的電郵地址',
footer_tagline: '以克制之美,抵達日常',
social_wechat: '微信',
cart_title: '購物袋',
cart_total_label: '合計',
cart_note: '含稅 · 運費於結帳時計算',
cart_checkout: '前往結帳',
cart_empty_title: '購物袋還是空的',
cart_empty_desc: '去挑選心儀的好物吧',
cart_remove: '移除',
modal_loading: '載入中…',
modal_nordi_pick: 'NORDI 嚴選',
modal_save_pct: '省 {n}%',
modal_rating: '評分',
modal_sales: '銷量',
modal_stock: '庫存',
modal_qty: '數量',
modal_add_cart: '加入購物袋',
modal_desc_fallback: '每一處細節皆經匠心打磨,以克制的線條與溫潤的質感,融入你的日常生活。',
modal_load_error: '商品詳情載入失敗,請稍後重試',
error_load_failed: '載入失敗',
error_load_desc: '商品資訊暫時無法取得,請稍後重試',
error_network: '網路異常',
error_request_failed: '請求失敗',
error_category_load: '分類載入失敗',
empty_title: '暫無商品',
empty_desc: '沒有找到匹配的商品,換個關鍵詞試試',
toast_wishlist: '已加入心願單',
toast_added_to_cart: '已加入購物袋 · {name}',
toast_add_failed: '新增失敗,請重試',
toast_featured_loading: '正在為你挑選編輯精選…',
toast_featured_failed: '精選載入失敗,請稍後重試',
toast_order_submitted: '訂單已提交 · 合計 {total} · 感謝惠顧',
toast_subscribe_success: '訂閱成功,感謝關注',
category_default: '生活好物',
page_title: '生活美學嚴選'
}
};
const LANG_LABELS = {'zh-CN': '中文', 'en': 'EN', 'zh-TW': '繁中'};
let currentLang = (function(){
try{
var u = navigator.language || navigator.userLanguage || '';
var s = localStorage.getItem('lang');
if (s && I18N_LOCALES[s]) return s;
if (u.startsWith('zh-TW') || u.startsWith('zh-Hant')) return 'zh-TW';
if (u.startsWith('zh')) return 'en';
return 'en';
} catch(e) {
return 'en';
}
})();
function t(key, params) {
var str = (I18N_LOCALES[currentLang] && I18N_LOCALES[currentLang][key])
|| (I18N_LOCALES['zh-CN'][key])
|| key;
if (params) {
Object.keys(params).forEach(function(k) {
str = str.replace(new RegExp('\\{' + k + '\\}', 'g'), params[k]);
});
}
return str;
}
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.title = 'NORDI · ' + t('page_title');
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');
});
// ========== 全局状态 ==========
let allCategories = [];
let currentPage = 1;
const pageSize = 12;
let selectedCategory = 0;
let searchKeyword = '';
let currentSort = '';
let cart = [];
let productCache = {};
let featuredLoaded = false;
// ========== 莫兰迪渐变 (极浅) ==========
const morandi = [
{ c: '#A8C5A0', t: 'sage' },
{ c: '#D4A5A5', t: 'clay' },
{ c: '#A0B8C5', t: 'slate' },
{ c: '#C5B8A0', t: 'taupe' },
{ c: '#B8A5C5', t: 'mauve' },
{ c: '#A0C5B8', t: 'mint' },
];
function getGradient(seed) {
const m = morandi[(seed - 1) % morandi.length] || morandi[0];
const hex = m.c.replace('#', '');
const r = parseInt(hex.slice(0,2),16), g = parseInt(hex.slice(2,4),16), b = parseInt(hex.slice(4,6),16);
return `linear-gradient(150deg, rgba(${r},${g},${b},0.16), rgba(${r},${g},${b},0.30))`;
}
function tagClass(tag) {
if (tag.includes('新') || tag.includes('新')) return 't-sage';
if (tag.includes('热') || tag.includes('熱') || tag.includes('销') || tag.includes('銷') || tag.includes('爆')) return 't-clay';
return 't-slate';
}
// ========== 优雅线条图标 (按分类) ==========
const iconPaths = {
1: '<path d="M3 8h18l-1.5 11a2 2 0 0 1-2 1.8H6.5a2 2 0 0 1-2-1.8z"/><path d="M8 8V6a4 4 0 0 1 8 0v2"/><path d="M9 13v2"/><path d="M15 13v2"/>',
2: '<path d="M3 9.5 12 3l9 6.5V20a1 1 0 0 1-1 1h-6v-7h-4v7H4a1 1 0 0 1-1-1z"/><path d="M9 11h6"/>',
3: '<path d="M12 3c0 4-4 5-4 9a4 4 0 0 0 8 0c0-4-4-5-4-9z"/><path d="M12 21v-4"/>',
4: '<circle cx="12" cy="12" r="8"/><path d="M12 4v16"/><path d="M4 12h16"/><path d="M7 7c2 2 2 8 0 10"/><path d="M17 7c-2 2-2 8 0 10"/>',
5: '<path d="M6 3h9l3 3v15H6z"/><path d="M9 9h6"/><path d="M9 13h6"/><path d="M9 17h4"/>',
6: '<rect x="4" y="6" width="16" height="14" rx="1"/><path d="M9 6V4h6v2"/><path d="M4 11h16"/>',
7: '<path d="M12 2 4 7v6c0 4 3 7 8 9 5-2 8-5 8-9V7z"/><path d="M9 12l2 2 4-4"/>',
8: '<path d="M5 7h14l-1 13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1z"/><path d="M9 7V5a3 3 0 0 1 6 0v2"/><path d="M12 12v4"/>',
};
function productIconSVG(catId) {
const p = iconPaths[catId] || iconPaths[1];
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
}
function starSVG() {
return `<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 2l2.9 6.3 6.9.8-5.1 4.7 1.4 6.8L12 17.8 5.9 20.6l1.4-6.8L2.2 9.1l6.9-.8z"/></svg>`;
}
// ========== API ==========
async function fetchJSON(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(t('error_network') + ' (' + res.status + ')');
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || t('error_request_failed'));
return json.data;
}
// ========== 加载 ==========
async function loadCategories() {
try {
allCategories = await fetchJSON('/api/v1/shop/categories');
renderCategoryChips();
} catch (e) { console.error(t('error_category_load') + ':', e); }
}
async function loadProducts() {
const grid = document.getElementById('productsGrid');
grid.innerHTML = Array(8).fill(0).map(() => `
<div class="skeleton-card">
<div class="sk-box sk-img"></div>
<div class="sk-box sk-line" style="width:50%"></div>
<div class="sk-box sk-line" style="width:90%"></div>
<div class="sk-box sk-line" style="width:60%"></div>
</div>`).join('');
try {
let url = `/api/v1/shop/products?page=${currentPage}&page_size=${pageSize}`;
if (currentSort) url += `&sort=${currentSort}`;
if (selectedCategory > 0) url += `&category_id=${selectedCategory}`;
if (searchKeyword) url += `&keyword=${encodeURIComponent(searchKeyword)}`;
const data = await fetchJSON(url);
renderProducts(data.list || [], data.total || 0);
renderPagination(data.total || 0);
} catch (e) {
grid.innerHTML = `<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
<h3>${t('error_load_failed')}</h3><p>${t('error_load_desc')}</p></div>`;
}
}
async function loadFeatured() {
showToast(t('toast_featured_loading'));
try {
const products = await fetchJSON('/api/v1/shop/featured?limit=8');
renderProducts(products, products.length);
document.getElementById('pagination').innerHTML = '';
document.getElementById('productsCount').innerHTML = t('products_count_featured', {n: products.length});
scrollToShop();
} catch (e) { showToast(t('toast_featured_failed')); }
}
// ========== 渲染 ==========
function renderCategoryChips() {
const container = document.getElementById('categoryChips');
const head = `<span class="cat-label" data-i18n="cat_label">${t('cat_label')}</span><span class="cat-chip ${selectedCategory === 0 ? 'active' : ''}" onclick="selectCategory(0)" data-i18n="cat_all">${t('cat_all')}</span>`;
container.innerHTML = head + allCategories
.sort((a,b) => (a.sort||0) - (b.sort||0))
.map(c => `<span class="cat-chip ${selectedCategory === c.id ? 'active' : ''}" onclick="selectCategory(${c.id})">${c.name}</span>`).join('');
}
function categoryName(id) {
const c = allCategories.find(x => x.id === id);
return c ? c.name : t('category_default');
}
function renderProducts(products, total) {
const grid = document.getElementById('productsGrid');
document.getElementById('productsCount').innerHTML = t('products_count', {n: total});
if (!products || products.length === 0) {
grid.innerHTML = `<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>
<h3>${t('empty_title')}</h3><p>${t('empty_desc')}</p></div>`;
return;
}
grid.innerHTML = products.map(p => {
const discount = p.original_price > p.price ? Math.round((1 - p.price / p.original_price) * 100) : 0;
let tags = [];
try { tags = JSON.parse(p.tags || '[]'); } catch (e) {}
const tagHtml = tags.slice(0, 2).map(tg => `<span class="product-tag ${tagClass(tg)}">${tg}</span>`).join('');
const rating = typeof p.rating === 'number' ? p.rating : 0;
return `
<div class="product-card" onclick="showProductDetail(${p.id})">
<div class="product-img">
${p.image ? `<img src="${p.image}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">` : ''}
<div class="product-img-fill" style="background:${getGradient(p.category_id)};${p.image ? 'display:none' : 'display:flex'}">${productIconSVG(p.category_id)}</div>
<div class="product-tags">${tagHtml}</div>
<button class="product-wish" onclick="event.stopPropagation();showToast(t('toast_wishlist'))" data-i18n-title="title_favorites" title="${t('title_favorites')}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</button>
</div>
<div class="product-info">
<div class="product-cat">${categoryName(p.category_id)}${discount > 0 ? ` · -${discount}%` : ''}</div>
<div class="product-name">${p.name}</div>
<div class="product-meta">
<div class="product-price">
<span class="price-now">$${Number(p.price).toFixed(2)}</span>
${p.original_price > p.price ? `<span class="price-was">$${Number(p.original_price).toFixed(2)}</span>` : ''}
</div>
<span class="product-rate">${starSVG()}${rating.toFixed(1)}</span>
</div>
</div>
</div>`;
}).join('');
}
function renderPagination(total) {
const pages = Math.ceil(total / pageSize);
const pag = document.getElementById('pagination');
if (pages <= 1) { pag.innerHTML = ''; return; }
let html = `<button class="page-btn" ${currentPage === 1 ? 'disabled' : ''} onclick="goToPage(${currentPage - 1})">&lsaquo;</button>`;
const maxVisible = 5;
let start = Math.max(1, currentPage - 2);
let end = Math.min(pages, start + maxVisible - 1);
if (end - start < maxVisible - 1) start = Math.max(1, end - maxVisible + 1);
if (start > 1) { html += `<button class="page-btn" onclick="goToPage(1)">1</button>`; if (start > 2) html += `<span class="page-dots">…</span>`; }
for (let i = start; i <= end; i++) {
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="goToPage(${i})">${i}</button>`;
}
if (end < pages) { if (end < pages - 1) html += `<span class="page-dots">…</span>`; html += `<button class="page-btn" onclick="goToPage(${pages})">${pages}</button>`; }
html += `<button class="page-btn" ${currentPage === pages ? 'disabled' : ''} onclick="goToPage(${currentPage + 1})">&rsaquo;</button>`;
pag.innerHTML = html;
}
// ========== 交互 ==========
function selectCategory(catId) {
selectedCategory = catId;
currentPage = 1;
renderCategoryChips();
loadProducts();
scrollToShop();
}
function changeSort(sort) {
currentSort = sort;
currentPage = 1;
document.querySelectorAll('.sort-btn').forEach(b => b.classList.toggle('active', b.dataset.sort === sort));
loadProducts();
}
function handleSearch(e) {
searchKeyword = e.target.value.trim();
currentPage = 1;
clearTimeout(window._searchTimer);
window._searchTimer = setTimeout(loadProducts, 380);
}
function goToPage(page) {
currentPage = page;
loadProducts();
document.getElementById('shop').scrollIntoView({ behavior: 'smooth' });
}
function scrollToShop() {
document.getElementById('shop').scrollIntoView({ behavior: 'smooth' });
}
// ========== 商品详情 ==========
async function showProductDetail(id) {
const modal = document.getElementById('productModal');
const content = document.getElementById('modalContent');
content.innerHTML = `<div style="grid-column:1/-1;padding:90px;text-align:center;color:var(--text-muted);font-size:13px;letter-spacing:1px;">${t('modal_loading')}</div>`;
modal.classList.add('open');
document.body.style.overflow = 'hidden';
try {
const p = await fetchJSON(`/api/v1/shop/products/${id}`);
productCache[id] = p;
let tags = [];
try { tags = JSON.parse(p.tags || '[]'); } catch (e) {}
let imgList = [];
try { imgList = JSON.parse(p.images || '[]'); } catch(e) {}
if (!Array.isArray(imgList)) imgList = [];
if (p.image) imgList = [p.image, ...imgList.filter(img => img !== p.image)];
const discount = p.original_price > p.price ? Math.round((1 - p.price / p.original_price) * 100) : 0;
const rating = typeof p.rating === 'number' ? p.rating : 0;
const stock = typeof p.stock === 'number' ? p.stock : 0;
content.innerHTML = `
<button class="modal-close" onclick="closeModal()">&times;</button>
<div class="modal-img" style="background:${getGradient(p.category_id)};position:relative;overflow:hidden">
${imgList.length > 0 ? `<img id="modalMainImg" src="${imgList[0]}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.style.display='none';this.nextElementSibling.style.display=''">` : ''}
<div style="${imgList.length > 0 ? 'display:none' : ''}">${productIconSVG(p.category_id)}</div>
${imgList.length > 1 ? `<div style="position:absolute;bottom:8px;left:0;right:0;display:flex;gap:6px;justify-content:center;z-index:5">${imgList.map((img,idx) => `<img src="${img}" onclick="document.getElementById('modalMainImg').src=this.src" style="width:40px;height:40px;border-radius:4px;object-fit:cover;cursor:pointer;border:2px solid ${idx===0?'#fff':'rgba(255,255,255,0.5)'}" onerror="this.remove()">`).join('')}</div>` : ''}
</div>
<div class="modal-body">
<div class="modal-cat">${categoryName(p.category_id)} · ${t('modal_nordi_pick')}</div>
<h2>${p.name}</h2>
<div class="modal-tags">${tags.map(tg => `<span class="product-tag ${tagClass(tg)}">${tg}</span>`).join('')}</div>
<div class="modal-price">
<span class="price-now">$${Number(p.price).toFixed(2)}</span>
${p.original_price > p.price ? `<span class="price-was">$${Number(p.original_price).toFixed(2)}</span>` : ''}
${discount > 0 ? `<span class="product-tag t-sage">${t('modal_save_pct', {n: discount})}</span>` : ''}
</div>
<p class="modal-desc">${p.description || t('modal_desc_fallback')}</p>
<div class="modal-meta">
<div class="modal-meta-item"><span class="mm-val">${rating.toFixed(1)}</span><span class="mm-label">${t('modal_rating')}</span></div>
<div class="modal-meta-item"><span class="mm-val">${formatSales(p.sales)}</span><span class="mm-label">${t('modal_sales')}</span></div>
<div class="modal-meta-item"><span class="mm-val">${stock}</span><span class="mm-label">${t('modal_stock')}</span></div>
</div>
<div class="modal-qty">
<span class="modal-qty-label">${t('modal_qty')}</span>
<div class="qty-control">
<button onclick="changeModalQty(-1)">−</button>
<span id="modalQty">1</span>
<button onclick="changeModalQty(1)">+</button>
</div>
</div>
<button class="modal-add-cart" onclick="addToCart(${p.id}, getModalQty());closeModal()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
${t('modal_add_cart')}</button>
</div>`;
} catch (e) {
content.innerHTML = `<div style="grid-column:1/-1;padding:80px;text-align:center;color:var(--text-muted)"><p>${t('modal_load_error')}</p></div>`;
}
}
function getModalQty() {
const el = document.getElementById('modalQty');
return el ? parseInt(el.textContent) || 1 : 1;
}
function changeModalQty(delta) {
const el = document.getElementById('modalQty');
if (!el) return;
let v = parseInt(el.textContent) + delta;
if (v < 1) v = 1;
el.textContent = v;
}
function closeModal(e) {
if (e && e.target !== e.currentTarget) return;
document.getElementById('productModal').classList.remove('open');
document.body.style.overflow = '';
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal(); toggleCart(false); } });
// ========== 购物车 ==========
async function addToCart(productId, qty = 1) {
let product = productCache[productId];
if (!product) {
try {
product = await fetchJSON(`/api/v1/shop/products/${productId}`);
productCache[productId] = product;
} catch (e) { showToast(t('toast_add_failed')); return; }
}
const existing = cart.find(i => i.id === productId);
if (existing) existing.qty += qty;
else cart.push({ id: productId, name: product.name, price: Number(product.price), qty, category_id: product.category_id, image: product.image || '' });
updateCartUI();
var displayName = product.name.length > 14 ? product.name.substring(0, 14) + '…' : product.name;
showToast(t('toast_added_to_cart', {name: displayName}));
}
function removeFromCart(productId) {
cart = cart.filter(i => i.id !== productId);
updateCartUI();
}
function changeQty(productId, delta) {
const item = cart.find(i => i.id === productId);
if (!item) return;
item.qty += delta;
if (item.qty <= 0) removeFromCart(productId);
else updateCartUI();
}
function updateCartUI() {
const count = cart.reduce((s, i) => s + i.qty, 0);
const total = cart.reduce((s, i) => s + i.price * i.qty, 0);
const badge = document.getElementById('cartBadge');
badge.textContent = count;
badge.style.display = count > 0 ? 'flex' : 'none';
document.getElementById('cartTotal').textContent = '$' + total.toFixed(2);
document.getElementById('checkoutBtn').disabled = count === 0;
const container = document.getElementById('cartItems');
if (cart.length === 0) {
container.innerHTML = `<div class="cart-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
<p>${t('cart_empty_title')}</p><p class="ce-sub">${t('cart_empty_desc')}</p></div>`;
return;
}
container.innerHTML = cart.map(item => `
<div class="cart-item">
<div class="cart-item-img" style="background:${getGradient(item.category_id)};position:relative;overflow:hidden">${item.image ? `<img src="${item.image}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.remove()">` : ''}${productIconSVG(item.category_id)}</div>
<div class="cart-item-info">
<div class="cart-item-name">${item.name}</div>
<div class="cart-item-price">$${item.price.toFixed(2)}</div>
<div class="cart-item-bottom">
<div class="cart-qty">
<button onclick="changeQty(${item.id}, -1)">−</button>
<span>${item.qty}</span>
<button onclick="changeQty(${item.id}, 1)">+</button>
</div>
<span class="cart-remove" onclick="removeFromCart(${item.id})">${t('cart_remove')}</span>
</div>
</div>
</div>`).join('');
}
function toggleCart(force) {
const open = force === undefined ? !document.getElementById('cartDrawer').classList.contains('open') : force;
document.getElementById('cartDrawer').classList.toggle('open', open);
document.getElementById('cartOverlay').classList.toggle('open', open);
document.body.style.overflow = open ? 'hidden' : '';
}
function checkout() {
if (cart.length === 0) return;
const total = cart.reduce((s, i) => s + i.price * i.qty, 0);
showToast(t('toast_order_submitted', {total: '$' + total.toFixed(2)}));
cart = [];
updateCartUI();
toggleCart(false);
}
// ========== 工具 ==========
function formatSales(n) {
n = Number(n) || 0;
if (n >= 10000) return (n / 10000).toFixed(1) + (currentLang === 'en' ? '0k' : '万');
return n.toString();
}
function showToast(msg) {
const toast = document.getElementById('toast');
document.getElementById('toastMsg').textContent = msg;
toast.classList.add('show');
clearTimeout(window._toastTimer);
window._toastTimer = setTimeout(() => toast.classList.remove('show'), 2600);
}
// ========== 语言切换回调:重新渲染动态内容 ==========
function onLangChange(lang) {
renderCategoryChips();
if (featuredLoaded) {
loadFeatured();
} else {
loadProducts();
}
updateCartUI();
var searchInput = document.getElementById('searchInput');
if (searchInput) searchInput.placeholder = t('search_placeholder');
loadFeatured();
}
// ========== 初始化 ==========
document.getElementById('searchInput').addEventListener('input', handleSearch);
async function init() {
applyI18n();
await loadCategories();
await loadProducts();
updateCartUI();
}
init();
// ========== 3D 动效 & 鼠标光影(北欧极简 · 克制优雅)==========
(function(){
var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) return;
var cursorGlow = document.createElement('div');
cursorGlow.className = 'cursor-glow';
document.body.appendChild(cursorGlow);
var gx = window.innerWidth / 2, gy = window.innerHeight / 2;
var tx = gx, ty = gy;
cursorGlow.style.transform = 'translate3d(' + gx + 'px,' + gy + 'px,0) translate(-50%,-50%)';
document.addEventListener('mousemove', function(e){ tx = e.clientX; ty = e.clientY; });
(function loop(){
gx += (tx - gx) * 0.16;
gy += (ty - gy) * 0.16;
cursorGlow.style.transform = 'translate3d(' + gx.toFixed(2) + 'px,' + gy.toFixed(2) + 'px,0) translate(-50%,-50%)';
requestAnimationFrame(loop);
})();
var activeCard = null;
document.addEventListener('mousemove', function(e){
var card = (e.target && e.target.closest) ? e.target.closest('.product-card') : null;
if (card !== activeCard){
if (activeCard){ activeCard.style.transform = ''; }
activeCard = card;
}
if (!card) return;
var rect = card.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
var rx = ((e.clientY - cy) / rect.height) * -6;
var ry = ((e.clientX - cx) / rect.width) * 6;
card.style.transform = 'perspective(1000px) rotateX(' + rx.toFixed(2) + 'deg) rotateY(' + ry.toFixed(2) + 'deg) translateY(-6px)';
card.style.setProperty('--mx', ((e.clientX - rect.left) / rect.width * 100).toFixed(1) + '%');
card.style.setProperty('--my', ((e.clientY - rect.top) / rect.height * 100).toFixed(1) + '%');
});
document.addEventListener('mouseleave', function(){
if (activeCard){ activeCard.style.transform = ''; activeCard = null; }
});
})();
</script>
</body>
</html>