Files
web_template/web/dist/shop3/index.html
admin 8b00f14099 feat: 跨境电商3套模板添加3D动态光影效果
- shop: 红色经典风格 - 鼠标红色光斑+卡片8度3D倾斜+玻璃高光+Hero视差
- shop2: 暗黑霓虹风格 - 霓虹紫青光斑+卡片10度3D倾斜+霓虹高光+12个浮动粒子
- shop3: 北欧极简风格 - 柔和暖色光斑+卡片6度3D倾斜+柔和高光+入场动画
2026-07-11 15:31:22 +08:00

1588 lines
57 KiB
HTML
Raw 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);
}
/* ========== 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; }
}
@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>全球免运 · 满 $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" 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" 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" 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>
</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">源自北欧的极简生活哲学。精选全球匠心好物,以克制的设计与温润的质感,为日常注入从容与诗意。</p>
<button class="hero-cta" onclick="scrollToShop()">探索精选
<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>全球直采</h4><p>原产地严选溯源</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>极速跨境</h4><p>平均 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>正品保证</h4><p>假一赔十承诺</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>30天退换</h4><p>无忧售后保障</p></div>
</div>
</div>
</section>
<!-- 分类导航 -->
<div class="category-bar">
<div class="category-inner" id="categoryChips">
<span class="cat-label">分类</span>
<span class="cat-chip active" onclick="selectCategory(0)">全部</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">每一件,皆经严选 / Every piece, thoughtfully chosen</div>
</div>
<a href="#" class="section-link" 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('')">推荐</button>
<button class="sort-btn" data-sort="sales" onclick="changeSort('sales')">畅销</button>
<button class="sort-btn" data-sort="price" onclick="changeSort('price')">价格</button>
<button class="sort-btn" data-sort="rating" onclick="changeSort('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">编辑精选 · 本季值得拥有的好物</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>以斯堪的纳维亚的极简美学,为世界各地的家,带来经久耐用的生活良品。Less, but better.</p>
</div>
<div class="footer-col">
<h5>购物</h5>
<a href="#" onclick="selectCategory(0);return false;">全部商品</a>
<a href="#" onclick="loadFeatured();return false;">编辑精选</a>
<a href="#">新品上架</a>
<a href="#">限时优惠</a>
</div>
<div class="footer-col">
<h5>服务</h5>
<a href="#">配送说明</a>
<a href="#">退换政策</a>
<a href="#">尺码指南</a>
<a href="#">常见问题</a>
</div>
<div class="footer-col">
<h5>关于</h5>
<a href="#">品牌故事</a>
<a href="#">可持续</a>
<a href="#">联系我们</a>
<a href="#">隐私条款</a>
</div>
<div class="footer-col footer-news">
<h5>订阅</h5>
<p>订阅 Newsletter,获取新品资讯与会员专属礼遇。</p>
<form class="news-form" onsubmit="event.preventDefault();showToast('订阅成功,感谢关注');this.reset();">
<input type="email" 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>
<div class="fb-links">
<a href="#">Instagram</a>
<a href="#">Pinterest</a>
<a href="#">微信</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>购物袋</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">合计</span>
<span class="cs-total" id="cartTotal">$0.00</span>
</div>
<div class="cart-note">含税 · 运费于结算时计算</div>
<button class="cart-checkout" id="checkoutBtn" onclick="checkout()" disabled>前往结算</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>
// ========== 全局状态 ==========
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('新')) return 't-sage';
if (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('网络异常 (' + res.status + ')');
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || '请求失败');
return json.data;
}
// ========== 加载 ==========
async function loadCategories() {
try {
allCategories = await fetchJSON('/api/v1/shop/categories');
renderCategoryChips();
} catch (e) { console.error('分类加载失败:', 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>加载失败</h3><p>商品信息暂时无法获取,请稍后重试</p></div>`;
}
}
async function loadFeatured() {
showToast('正在为你挑选编辑精选…');
try {
const products = await fetchJSON('/api/v1/shop/featured?limit=8');
renderProducts(products, products.length);
document.getElementById('pagination').innerHTML = '';
document.getElementById('productsCount').innerHTML = `编辑精选 · <strong>${products.length}</strong> 件商品`;
scrollToShop();
} catch (e) { showToast('精选加载失败,请稍后重试'); }
}
// ========== 渲染 ==========
function renderCategoryChips() {
const container = document.getElementById('categoryChips');
const head = `<span class="cat-label">分类</span><span class="cat-chip ${selectedCategory === 0 ? 'active' : ''}" onclick="selectCategory(0)">全部</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 : '生活好物';
}
function renderProducts(products, total) {
const grid = document.getElementById('productsGrid');
document.getElementById('productsCount').innerHTML = `共 <strong>${total}</strong> 件商品`;
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>暂无商品</h3><p>没有找到匹配的商品,换个关键词试试</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(t => `<span class="product-tag ${tagClass(t)}">${t}</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('已加入心愿单')" 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>
</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;">载入中…</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)} · NORDI 严选</div>
<h2>${p.name}</h2>
<div class="modal-tags">${tags.map(t => `<span class="product-tag ${tagClass(t)}">${t}</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">省 ${discount}%</span>` : ''}
</div>
<p class="modal-desc">${p.description || '每一处细节皆经匠心打磨,以克制的线条与温润的质感,融入你的日常生活。'}</p>
<div class="modal-meta">
<div class="modal-meta-item"><span class="mm-val">${rating.toFixed(1)}</span><span class="mm-label">评分</span></div>
<div class="modal-meta-item"><span class="mm-val">${formatSales(p.sales)}</span><span class="mm-label">销量</span></div>
<div class="modal-meta-item"><span class="mm-val">${stock}</span><span class="mm-label">库存</span></div>
</div>
<div class="modal-qty">
<span class="modal-qty-label">数量</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>
加入购物袋</button>
</div>`;
} catch (e) {
content.innerHTML = `<div style="grid-column:1/-1;padding:80px;text-align:center;color:var(--text-muted)"><p>商品详情加载失败,请稍后重试</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('添加失败,请重试'); 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();
showToast(`已加入购物袋 · ${product.name.length > 14 ? product.name.substring(0, 14) + '…' : product.name}`);
}
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>购物袋还是空的</p><p class="ce-sub">去挑选心仪的好物吧</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})">移除</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(`订单已提交 · 合计 $${total.toFixed(2)} · 感谢惠顾`);
cart = [];
updateCartUI();
toggleCart(false);
}
// ========== 工具 ==========
function formatSales(n) {
n = Number(n) || 0;
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
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);
}
// ========== 初始化 ==========
document.getElementById('searchInput').addEventListener('input', handleSearch);
async function init() {
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);
})();
/* —— 产品卡片 3D 倾斜(最大 6°,柔和流畅;事件委托兼容动态渲染)—— */
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; // 北欧克制:最大 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>