865
src/pages/index.astro
Normal file
865
src/pages/index.astro
Normal file
@ -0,0 +1,865 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro"
|
||||
import ComingSoon from "../components/ComingSoon.astro"
|
||||
import { getShows, seriesLabel } from "../lib/data"
|
||||
|
||||
// 默认 tab(Shows)走 SSR:构建期从接口预取真实数据与多图,首屏立即可见。
|
||||
// 其余 tab(Backstage / Events / Street)点击时才由前端请求接口动态渲染,避免一次性拉全量拖慢首屏。
|
||||
const shows = await getShows(20)
|
||||
// 精选轮播:取前 5 场秀,做成「左图右文」卡片式轮播(Featured Carousel)
|
||||
const slides = shows.slice(0, 5)
|
||||
// 右侧品牌栏:跨分页拉取「与轮播不同品牌」的其他精选品牌,按品牌去重取 2 个
|
||||
const API_BASE = (import.meta.env.PUBLIC_API_BASE || "").replace(/\/$/, "")
|
||||
const slideBrands = new Set(slides.map(s => s.brand))
|
||||
const rail: typeof shows = []
|
||||
const railSeen = new Set<string>()
|
||||
if (API_BASE) {
|
||||
for (let pg = 1; pg <= 6 && rail.length < 6; pg++) {
|
||||
try {
|
||||
const rr = await fetch(`${API_BASE}/api/public/articles?page=${pg}&size=40&with_images=5`)
|
||||
if (!rr.ok) break
|
||||
const rj = await rr.json()
|
||||
const items: any[] = rj?.data ?? []
|
||||
for (const r of items) {
|
||||
const b = r.brand_name || "Unknown"
|
||||
if (slideBrands.has(b) || railSeen.has(b)) continue
|
||||
railSeen.add(b)
|
||||
rail.push({
|
||||
id: r.id, brand: b, title: r.title || "",
|
||||
collection_type: r.collection_type, season_code: r.season_code,
|
||||
year: Number(r.year) || 0,
|
||||
count: r.image_count || 0, img: r.cover || "",
|
||||
gallery: (r.images || []).map((im: any) => im.image || "").filter(Boolean).slice(0, 5),
|
||||
})
|
||||
if (rail.length >= 2) break
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
const railHot = [...rail].sort((a, b) => (b.count || 0) - (a.count || 0))
|
||||
// 最新取前 2;最热取 count 最高的 2;若两者完全相同(库内品牌少),改取最新之外的其它品牌,保证切换有可见差异
|
||||
const latestSet = rail.slice(0, 2)
|
||||
let hotSet = railHot.slice(0, 2)
|
||||
if (hotSet.length === 2 && latestSet.length === 2 &&
|
||||
hotSet[0].brand === latestSet[0].brand && hotSet[1].brand === latestSet[1].brand) {
|
||||
const lb = new Set(latestSet.map((r) => r.brand))
|
||||
const others = rail.filter((r) => !lb.has(r.brand)).slice(0, 2)
|
||||
if (others.length === 2) hotSet = others
|
||||
}
|
||||
|
||||
// 拼装右侧卡片的文案字段
|
||||
function railEyebrow(r: typeof shows[number]) {
|
||||
const m = (r.title || "").match(/(\d{4}).*?(Spring|Summer|Fall|Autumn|Winter|Resort|Pre-Fall|Cruise)/i)
|
||||
const season = m ? `${m[1]} ${m[2]}` : (r.season_code || "")
|
||||
const yr = r.year ? `${r.year}` : ""
|
||||
return [yr || (m ? m[1] : ""), season].filter(Boolean).join(" · ").toUpperCase() || "Featured Runway"
|
||||
}
|
||||
function railTagline(r: typeof shows[number]) {
|
||||
return `${seriesLabel(r.collection_type)}${r.season_code ? " · " + r.season_code : ""}`
|
||||
}
|
||||
function railDesc(r: typeof shows[number]) {
|
||||
const yr = r.year || ((r.title || "").match(/\b(19|20)\d{2}\b/) || [""])[0]
|
||||
const seasonTxt = (() => {
|
||||
const m = (r.title || "").match(/(Spring|Summer|Fall|Autumn|Winter|Resort|Pre-Fall|Cruise)/i)
|
||||
if (m) {
|
||||
const map: Record<string, string> = { Spring: "春", Summer: "夏", Fall: "秋冬", Autumn: "秋冬", Winter: "冬", Resort: "度假", "Pre-Fall": "早秋", Cruise: "度假" }
|
||||
return (map[m[1].toLowerCase().replace(/(^|\s)\w/g, (s) => s.toUpperCase())] || map[m[1]] || "") + (yr || "")
|
||||
}
|
||||
return yr ? `${yr} 发布` : ""
|
||||
})()
|
||||
return { seasonTxt: seasonTxt || (yr ? `${yr} 发布` : "") }
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<!-- 二级大字号分类(标签页):仅「发布会」上线,其余标注 Coming Soon -->
|
||||
<div id="tabbar" class="mt-16 md:mt-24">
|
||||
<ul class="flex items-center gap-6 list-none p-0 m-0 overflow-x-auto">
|
||||
<li class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap" data-tab="backstage" data-active="true">发布会
|
||||
<span class="text-xs uppercase tracking-widest bg-black text-white px-2 py-0.5 rounded-full font-mono font-bold align-super">Live</span>
|
||||
</li>
|
||||
<li class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap" data-tab="shows" data-soon="true">灵感
|
||||
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
||||
</li>
|
||||
<li class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap" data-tab="events" data-soon="true">社媒
|
||||
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
||||
</li>
|
||||
<li class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap" data-tab="street" data-soon="true">街拍
|
||||
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
||||
</li>
|
||||
<a href="/brands" class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap hover:opacity-80">品牌</a>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 中间内容展示区:标签内容区,点击标签仅局部更新此区域 -->
|
||||
<main class="grow min-h-[400px] mt-10">
|
||||
<!-- 发布会 面板(默认激活,SSR 预渲染真实走秀内容) -->
|
||||
<div class="panel" data-panel="backstage" data-show="true" data-loaded="true">
|
||||
<!-- 区块标题 -->
|
||||
<div class="sec-head">
|
||||
<div>
|
||||
<h2 class="sec-title">RUNWAY ARCHIVE</h2>
|
||||
<p class="sec-sub">走秀档案 · 精选最新上架的秀场</p>
|
||||
</div>
|
||||
<a href="/shows" class="sec-more">全部走秀 →</a>
|
||||
</div>
|
||||
|
||||
<!-- 系列快速筛选(前端本地过滤,data-ct 对应 collection_type) -->
|
||||
<div class="seriesbar" id="seriesbar">
|
||||
<button class="schip on" data-ct="">全部</button>
|
||||
<button class="schip" data-ct="rtw">成衣</button>
|
||||
<button class="schip" data-ct="menswear">男装</button>
|
||||
<button class="schip" data-ct="couture">高定</button>
|
||||
<button class="schip" data-ct="resort">度假</button>
|
||||
<button class="schip" data-ct="pre_fall">早秋</button>
|
||||
</div>
|
||||
|
||||
<!-- 精选:左轮播(图上文下)+ 右品牌栏(横版卡片 ×2) -->
|
||||
{slides.length > 0 && (
|
||||
<section class="hero-carousel" id="heroCarousel" data-count={slides.length}>
|
||||
<div class="hc-stage">
|
||||
<div class="hc-rotator">
|
||||
{slides.map((s, i) => (
|
||||
<div class="hc-slide" data-index={i} data-ct={s.collection_type || "rtw"} style={i === 0 ? "" : "display:none"}>
|
||||
<a class="hc-media" href={`/article?id=${s.id}`} aria-label={s.brand}>
|
||||
<img src={s.gallery[0] || s.img} alt={s.brand} class="hc-img" loading={i === 0 ? "eager" : "lazy"} />
|
||||
<span class="prod-tag tl">Featured</span>
|
||||
</a>
|
||||
<div class="hc-text">
|
||||
<div class="hc-info">
|
||||
<span class="hc-eyebrow">{((s.title || "").match(/(\d{4}).*?(Spring|Summer|Fall|Autumn|Winter|Resort|Pre-Fall|Cruise)/i) || []).slice(1,3).join(" ") || s.season_code || "Featured Runway"}</span>
|
||||
<div class="hc-head">
|
||||
<h2 class="hc-brand">{s.brand}</h2>
|
||||
<p class="hc-tagline">{seriesLabel(s.collection_type)}{s.season_code ? " · " + s.season_code : ""}</p>
|
||||
</div>
|
||||
<div class="hc-bar-row">
|
||||
<a class="hc-bar" href={`/article?id=${s.id}`}>
|
||||
<span>Brand Analysis</span>
|
||||
<span class="hc-bar-plus">+</span>
|
||||
</a>
|
||||
<span class="hc-meta-count">{s.count} Look{s.count > 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hc-controls">
|
||||
<span class="hc-index"><span class="hc-cur">{String(i + 1).padStart(2, "0")}</span> / {String(slides.length).padStart(2, "0")}</span>
|
||||
<div class="hc-nav">
|
||||
<button class="hc-arrow hc-prev" type="button" aria-label="上一张">←</button>
|
||||
<div class="hc-dots">
|
||||
{slides.map((_, di) => (
|
||||
<button class="hc-dot" type="button" data-i={di} aria-label={`第 ${di + 1} 张`} class:list={["hc-dot", di === 0 && "on"]}></button>
|
||||
))}
|
||||
</div>
|
||||
<button class="hc-arrow hc-next" type="button" aria-label="下一张">→</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{rail.length > 0 && (
|
||||
<aside class="hc-rail">
|
||||
<div class="hc-rail-head">
|
||||
<div class="hc-rail-tabs">
|
||||
<button type="button" class="on" data-sort="latest">最新</button>
|
||||
<button type="button" data-sort="hot">最热</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hc-rail-list" id="railList">
|
||||
<div class="rail-set" data-set="latest">
|
||||
{latestSet.map((r) => (
|
||||
<article class="rail-card">
|
||||
<div class="rail-text">
|
||||
<span class="rail-eyebrow">{railEyebrow(r)}</span>
|
||||
<h3 class="rail-brand">{r.brand}</h3>
|
||||
<p class="rail-tag">{railTagline(r)}</p>
|
||||
<a class="rail-bar" href={`/article?id=${r.id}`}>
|
||||
<span>Brand Analysis</span>
|
||||
<span class="rail-bar-plus">+</span>
|
||||
</a>
|
||||
<div class="rail-desc">{r.brand}:{railDesc(r).seasonTxt || "近期新作"} <span class="rail-dot">·</span> {r.count} 张</div>
|
||||
</div>
|
||||
<a class="rail-thumb" href={`/article?id=${r.id}`} aria-label={r.brand}>
|
||||
<img src={r.gallery[0] || r.img} alt={r.brand} loading="lazy" />
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div class="rail-set" data-set="hot" style="display:none">
|
||||
{hotSet.map((r) => (
|
||||
<article class="rail-card">
|
||||
<div class="rail-text">
|
||||
<span class="rail-eyebrow">{railEyebrow(r)}</span>
|
||||
<h3 class="rail-brand">{r.brand}</h3>
|
||||
<p class="rail-tag">{railTagline(r)}</p>
|
||||
<a class="rail-bar" href={`/article?id=${r.id}`}>
|
||||
<span>Brand Analysis</span>
|
||||
<span class="rail-bar-plus">+</span>
|
||||
</a>
|
||||
<div class="rail-desc">{r.brand}:{railDesc(r).seasonTxt || "近期新作"} <span class="rail-dot">·</span> {r.count} 张</div>
|
||||
</div>
|
||||
<a class="rail-thumb" href={`/article?id=${r.id}`} aria-label={r.brand}>
|
||||
<img src={r.gallery[0] || r.img} alt={r.brand} loading="lazy" />
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- 卡片网格(轮播之外的剩余 shows) -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6" id="showgrid">
|
||||
{shows.slice(5).map((s) => (
|
||||
<article class="prod-card group" data-article-id={s.id} data-ct={s.collection_type || "rtw"}>
|
||||
<div class="prod-main">
|
||||
<img src={s.gallery[0] || s.img} alt={s.brand} class="prod-main-img" loading="lazy" />
|
||||
<span class="prod-tag tl">Runway</span>
|
||||
{s.count > 1 && <span class="prod-tag tr">{s.count} Pics</span>}
|
||||
<a href={`/article?id=${s.id}`} class="prod-viewall">查看全部 {s.count} 张 →</a>
|
||||
</div>
|
||||
<div class="prod-thumbs">
|
||||
{( s.gallery.length ? s.gallery : [s.img] ).map((g, gi) => (
|
||||
<button class="prod-thumb" type="button" aria-label={`查看第 ${gi + 1} 张`}>
|
||||
<img src={g} alt="" loading="lazy" />
|
||||
</button>
|
||||
))}
|
||||
{s.count > (s.gallery.length || 1) && (
|
||||
<a href={`/article?id=${s.id}`} class="prod-more">+{s.count - (s.gallery.length || 1)}</a>
|
||||
)}
|
||||
</div>
|
||||
<div class="prod-info">
|
||||
<div class="prod-name">{s.brand}</div>
|
||||
<div class="prod-meta">
|
||||
<span class="prod-chip">{seriesLabel(s.collection_type)}</span>
|
||||
{s.season_code && <span class="prod-chip">{s.season_code}</span>}
|
||||
</div>
|
||||
<div class="prod-sub">{s.title}</div>
|
||||
<div class="prod-foot">
|
||||
<a href={`/article?id=${s.id}`} class="prod-cta">查看 {s.count} 张 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div class="showmore-wrap">
|
||||
<button id="showmore" class="showmore" type="button">查看更多走秀 ↓</button>
|
||||
</div>
|
||||
<div class="empty" id="showempty" hidden>该系列暂无可展示的走秀</div>
|
||||
</div>
|
||||
|
||||
<!-- 灵感 面板(暂未上线) -->
|
||||
<div class="panel" data-panel="shows" data-show="false" data-loaded="true">
|
||||
<ComingSoon module="灵感" moduleZh="Inspiration" note="灵感板块正在筹备中,敬请期待。你可以先浏览已上线的走秀档案。" />
|
||||
</div>
|
||||
|
||||
<!-- 社媒 面板(暂未上线) -->
|
||||
<div class="panel" data-panel="events" data-show="false" data-loaded="true">
|
||||
<ComingSoon module="社媒" moduleZh="Social" note="社媒板块正在筹备中,敬请期待。你可以先浏览已上线的走秀档案。" />
|
||||
</div>
|
||||
|
||||
<!-- 街拍 面板(暂未上线) -->
|
||||
<div class="panel" data-panel="street" data-show="false" data-loaded="true">
|
||||
<ComingSoon module="街拍" moduleZh="Street" note="街拍板块正在筹备中,敬请期待。你可以先浏览已上线的走秀档案。" />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 底部/两侧切换按钮(保持你原来的) -->
|
||||
<div class="flex justify-between items-center text-[13px] font-bold mt-12">
|
||||
<div class="cursor-pointer hover:opacity-60 transition-opacity duration-200">Previous</div>
|
||||
<div class="cursor-pointer hover:opacity-60 transition-opacity duration-200">Next</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style is:global>
|
||||
@reference "tailwindcss";
|
||||
/* 卡片及缩略图样式改为全局(is:global):默认 Shows 走 SSR 由 Astro 作用域正常生效,
|
||||
但其余 tab 的卡片由前端 JS 在运行时注入 DOM,缺 data-astro-cid 属性,
|
||||
原组件作用域样式无法命中。改为全局后所有 tab 的卡片展示完全一致(3:4)。 */
|
||||
|
||||
/* 标签配色:沿用你原来的 #bbb / #666 hover / 黑 激活 */
|
||||
.tab { @apply text-[#bbb] transition-colors duration-200 no-underline; }
|
||||
.tab:hover { @apply text-[#666]; }
|
||||
.tab[data-active="true"] { @apply text-black; }
|
||||
.tab[data-active="true"]:hover { @apply text-black; }
|
||||
|
||||
/* 面板:仅显示当前标签对应的内容区,局部切换 */
|
||||
.panel { @apply hidden; }
|
||||
.panel[data-show="true"] {
|
||||
@apply block;
|
||||
animation: tabfade 0.3s ease;
|
||||
}
|
||||
@keyframes tabfade {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* 卡片悬停上浮 + 阴影(恢复此前被误删的规则) */
|
||||
.prod-card:hover { @apply -translate-y-1 shadow-[0_16px_38px_rgba(0,0,0,0.13)]; }
|
||||
|
||||
/* 主图容器:固定 3:4 比例 + 骨架占位,图片未加载时尺寸恒定、显示加载动画 */
|
||||
.prod-main {
|
||||
@apply relative w-full aspect-[3/4] overflow-hidden bg-[#f0f0f0];
|
||||
background-image: linear-gradient(100deg, #f0f0f0 30%, #e6e6e6 50%, #f0f0f0 70%);
|
||||
background-size: 200% 100%;
|
||||
animation: img-shimmer 1.3s ease-in-out infinite;
|
||||
}
|
||||
.prod-main.is-loaded { animation: none; }
|
||||
/* 主图:绝对铺满容器并裁切,保证不同分辨率图展示一致、容器高度恒定 */
|
||||
.prod-main-img {
|
||||
@apply absolute inset-0 w-full h-full object-cover block origin-center;
|
||||
transition: transform .55s ease, opacity .22s ease;
|
||||
}
|
||||
/* 缩略图容器:固定 3:4 比例 + 骨架占位 */
|
||||
.prod-thumb {
|
||||
@apply bg-[#f0f0f0];
|
||||
background-image: linear-gradient(100deg, #f0f0f0 30%, #e6e6e6 50%, #f0f0f0 70%);
|
||||
background-size: 200% 100%;
|
||||
animation: img-shimmer 1.3s ease-in-out infinite;
|
||||
}
|
||||
.prod-thumb.is-loaded { animation: none; }
|
||||
@keyframes img-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
||||
|
||||
/* (.js 门控的淡入与 is-error 占位规则已移至下方 <style is:global>,避免组件作用域破坏 .js 选择器) */
|
||||
/* ===== 卡片 + 缩略图轮播 混合布局 ===== */
|
||||
:root { --brand: #cf5a44; }
|
||||
.prod-card {
|
||||
@apply flex flex-col bg-white border border-[#ececec] rounded-[6px] overflow-hidden transition-[transform,box-shadow] duration-300 will-change-transform;
|
||||
}
|
||||
.prod-card:hover .prod-main-img { @apply scale-105; }
|
||||
.prod-card:hover .prod-main { @apply shadow-[0_12px_26px_rgba(0,0,0,0.18)]; }
|
||||
.prod-tag {
|
||||
@apply absolute z-[2] font-mono text-[10px] uppercase tracking-[.18em] px-[8px] py-[4px];
|
||||
}
|
||||
.prod-tag.tl { @apply top-[10px] left-[10px] bg-white text-black; }
|
||||
.prod-tag.tr { @apply top-[10px] right-[10px] bg-black text-white flex gap-[5px] items-center; }
|
||||
.prod-tag.tr::before { content: ""; @apply w-[6px] h-[6px] rounded-full bg-white; }
|
||||
.prod-thumbs {
|
||||
@apply flex gap-2 px-[10px] pt-[10px] pb-[6px] overflow-x-auto;
|
||||
scrollbar-width: none; -ms-overflow-style: none;
|
||||
}
|
||||
.prod-thumbs::-webkit-scrollbar { display: none; }
|
||||
.prod-thumb {
|
||||
@apply flex-none w-[78px] aspect-[3/4] h-auto p-0 m-0 border-2 border-transparent bg-[#f2f2f2] cursor-pointer rounded-[4px] overflow-hidden opacity-75 transition-[border-color,opacity,transform] duration-200;
|
||||
}
|
||||
.prod-thumb img { @apply w-full h-full object-cover block; }
|
||||
.prod-thumb:hover { @apply opacity-100; }
|
||||
.prod-thumb.active { @apply border-[var(--brand)] opacity-100; }
|
||||
.prod-info { @apply px-[12px] pt-[8px] pb-[14px] flex flex-col gap-[6px] flex-1; }
|
||||
.prod-name { @apply text-[16px] font-extrabold leading-[1.2] text-[#111]; }
|
||||
.prod-sub { @apply text-[11px] uppercase tracking-[.14em] text-[#999] font-mono; }
|
||||
.prod-tags { @apply flex gap-[6px] flex-wrap; }
|
||||
.prod-badge {
|
||||
@apply text-[10px] uppercase tracking-[.08em] px-[8px] py-[3px] border border-[#ddd] rounded-full text-[#666] bg-white;
|
||||
}
|
||||
.prod-badge.new { @apply bg-[#111] text-white border-[#111]; }
|
||||
.prod-badge.hot { @apply bg-[var(--brand)] text-white border-[var(--brand)]; }
|
||||
.prod-foot { @apply flex items-center justify-between mt-auto pt-[4px]; }
|
||||
.prod-price { @apply text-[15px] font-extrabold tracking-[.02em] text-[#111]; }
|
||||
.prod-cta { @apply text-[11px] uppercase tracking-[.14em] font-bold no-underline; color: var(--brand); }
|
||||
.prod-card:hover .prod-cta { @apply underline; }
|
||||
|
||||
/* 缩略图条末尾的"更多"入口:提示还有更多图片,点击进入详情看全部 */
|
||||
.prod-more {
|
||||
@apply flex-none w-[78px] aspect-[3/4] h-auto flex items-center justify-center bg-[var(--brand)] text-white no-underline rounded-[4px] text-[13px] font-extrabold tracking-[.02em] transition-[filter,transform] duration-200;
|
||||
}
|
||||
.prod-more:hover { @apply brightness-[1.08] -translate-y-[2px]; }
|
||||
|
||||
/* 主图悬停时浮现"查看全部 N 张"浮层(桌面 hover 生效,移动端不拦截 pinch) */
|
||||
.prod-viewall {
|
||||
@apply absolute inset-0 z-[3] flex items-end justify-center pb-[16px] text-white text-[12px] font-bold tracking-[.14em] uppercase no-underline opacity-0 pointer-events-none transition-opacity duration-[250ms] ease-[ease];
|
||||
background: linear-gradient(to top, rgba(0,0,0,.6), rgba(0,0,0,0) 55%);
|
||||
}
|
||||
.prod-card:hover .prod-viewall { @apply opacity-100 pointer-events-auto; }
|
||||
|
||||
/* 标签页激活态:品牌色下划线动画 */
|
||||
.tab { @apply relative pb-[6px]; }
|
||||
.tab::after {
|
||||
content: "";
|
||||
@apply absolute left-0 right-0 bottom-0 h-[2px] bg-[var(--brand)] scale-x-0 origin-left transition-transform duration-[250ms] ease-[ease];
|
||||
}
|
||||
.tab[data-active="true"]::after { @apply scale-x-100; }
|
||||
|
||||
/* 懒加载占位骨架卡 */
|
||||
.skel-card { @apply border border-[#ececec] rounded-[6px] overflow-hidden bg-white; }
|
||||
.skel-main {
|
||||
@apply aspect-[3/4] bg-[#f0f0f0];
|
||||
background-image: linear-gradient(100deg, #f0f0f0 30%, #e6e6e6 50%, #f0f0f0 70%);
|
||||
background-size: 200% 100%; animation: img-shimmer 1.3s ease-in-out infinite;
|
||||
}
|
||||
.skel-line {
|
||||
@apply h-[12px] mt-[10px] mx-[12px] rounded-[3px] bg-[#f0f0f0];
|
||||
background-image: linear-gradient(100deg, #f0f0f0 30%, #e6e6e6 50%, #f0f0f0 70%);
|
||||
background-size: 200% 100%; animation: img-shimmer 1.3s ease-in-out infinite;
|
||||
}
|
||||
.skel-line.short { @apply w-1/2; }
|
||||
|
||||
/* 区块标题 */
|
||||
.sec-head { @apply flex items-end justify-between mt-2 mb-6 pb-4 border-b border-[#ececec]; }
|
||||
.sec-title { @apply font-serif text-[28px] leading-none font-extrabold text-[#111]; }
|
||||
.sec-sub { @apply mt-2 text-[12px] uppercase tracking-[.14em] text-[#999] font-mono; }
|
||||
.sec-more { @apply text-[12px] uppercase tracking-[.14em] font-bold no-underline text-[#111] transition-opacity duration-200; }
|
||||
.sec-more:hover { @apply opacity-60; }
|
||||
|
||||
/* 系列快速筛选 */
|
||||
.seriesbar { @apply flex items-center gap-2 overflow-x-auto mb-7 pb-1; scrollbar-width: none; -ms-overflow-style: none; }
|
||||
.seriesbar::-webkit-scrollbar { display: none; }
|
||||
.schip {
|
||||
@apply flex-none font-mono text-[11px] uppercase tracking-[.12em] px-[14px] py-[6px] rounded-full border border-[#ddd] bg-white text-[#666] cursor-pointer transition-colors duration-200;
|
||||
}
|
||||
.schip:hover { @apply text-[#111] border-[#111]; }
|
||||
.schip.on { @apply bg-[#111] text-white border-[#111]; }
|
||||
|
||||
/* 精选 feat group:橄榄绿 editorial 重排(保留左大图轮播 + 右品牌栏) */
|
||||
.hero-carousel { @apply relative mb-9; }
|
||||
.hc-stage { @apply relative flex flex-col; }
|
||||
@media (min-width: 768px) { .hc-stage { flex-direction: row; gap: 32px; align-items: stretch; } }
|
||||
.hc-rotator { display: flex; flex-direction: column; min-height: 0; }
|
||||
@media (min-width: 768px) { .hc-rotator { flex: 1 1 0%; min-height: 560px; } }
|
||||
.hc-slide { display: flex; flex-direction: column; flex: 1 1 0%; min-height: 0; gap: 20px; }
|
||||
.hc-media {
|
||||
position: relative; flex: 1 1 0%; min-height: 320px; width: 100%; overflow: hidden;
|
||||
border-radius: 2px; background: #f0f0f0; display: block; text-decoration: none;
|
||||
}
|
||||
@media (min-width: 768px) { .hc-media { min-height: 480px; } }
|
||||
.hc-img { @apply absolute inset-0 w-full h-full object-cover block origin-center transition-transform duration-[600ms] ease-out; }
|
||||
.hc-media:hover .hc-img { @apply scale-[1.03]; }
|
||||
.hc-text { @apply flex flex-col gap-[18px] flex-shrink-0; }
|
||||
.hc-info { @apply flex flex-col gap-3; }
|
||||
.hc-eyebrow { @apply block font-mono text-[11px] uppercase tracking-[.24em] text-[#8a8455]; }
|
||||
.hc-head { @apply flex items-baseline gap-4 flex-wrap; }
|
||||
.hc-brand { @apply font-serif font-extrabold text-[#14110d] m-0 tracking-[-.02em] leading-[1.0]; font-size: 34px; }
|
||||
@media (min-width: 768px) { .hc-brand { font-size: 46px; } }
|
||||
.hc-tagline { @apply font-mono text-[11px] uppercase tracking-[.18em] text-[#8a8455] m-0; }
|
||||
.hc-bar-row { @apply flex items-center gap-4 pt-[14px] border-t border-[#e6e2d8]; }
|
||||
.hc-bar {
|
||||
@apply inline-flex items-center justify-between gap-4 px-4 py-[10px] text-white font-mono text-[10px] uppercase tracking-[.2em] font-bold no-underline border transition-colors duration-200;
|
||||
background: #8a8455; border-color: #8a8455;
|
||||
}
|
||||
.hc-bar:hover { @apply no-underline; background: #fff; color: #8a8455; }
|
||||
.hc-bar-plus { @apply text-[16px] leading-none font-light; }
|
||||
.hc-meta-count { @apply font-mono text-[11px] uppercase tracking-[.16em] text-[#9a937f]; }
|
||||
.hc-controls { @apply flex items-center justify-between pt-[14px] border-t border-[#e6e2d8]; }
|
||||
.hc-index { @apply font-mono text-[12px] tracking-[.2em] text-[#9a937f]; }
|
||||
.hc-index .hc-cur { @apply font-bold; color: #8a8455; }
|
||||
.hc-nav { @apply flex items-center gap-4; }
|
||||
.hc-arrow {
|
||||
@apply w-9 h-9 rounded-full flex items-center justify-center cursor-pointer bg-white no-underline select-none transition-colors duration-200;
|
||||
border: 1px solid #8a8455; color: #8a8455;
|
||||
}
|
||||
.hc-arrow:hover { background: #8a8455; color: #fff; }
|
||||
.hc-dots { @apply flex gap-2 items-center; }
|
||||
.hc-dot { @apply w-2 h-2 rounded-full border-0 p-0 cursor-pointer transition-colors duration-200; background: #dcd8cc; }
|
||||
.hc-dot.on { background: #8a8455; }
|
||||
|
||||
/* 右侧品牌栏(横版卡片,图左文右,×2 等高) */
|
||||
.hc-rail { @apply flex flex-col min-h-0; }
|
||||
@media (min-width: 768px) { .hc-rail { width: 34%; flex-shrink: 0; border-left: 1px solid #e6e2d8; padding-left: 24px; } }
|
||||
.hc-rail-head { @apply mb-4; }
|
||||
.hc-rail-tabs { @apply flex gap-3; }
|
||||
.hc-rail-tabs button { @apply font-mono text-[11px] uppercase tracking-[.14em] bg-none border-0 p-0 cursor-pointer transition-colors duration-200; color: #b8b2a2; }
|
||||
.hc-rail-tabs button.on { @apply font-bold pb-[2px] border-b; color: #8a8455; border-color: #8a8455; }
|
||||
.hc-rail-list { @apply flex flex-col flex-1 overflow-y-auto min-h-0; scrollbar-width: thin; scrollbar-color: #dcd8cc #fff; }
|
||||
.hc-rail-list::-webkit-scrollbar { @apply w-[6px]; }
|
||||
.hc-rail-list::-webkit-scrollbar-thumb { @apply bg-[#dcd8cc] rounded-full; }
|
||||
.rail-set { @apply list-none m-0 p-0 flex flex-col flex-1 min-h-0 gap-4; }
|
||||
.rail-card { @apply flex flex-row-reverse flex-1 min-h-0 gap-3 items-stretch; }
|
||||
.rail-text { @apply w-[42%] flex flex-col justify-between min-w-0 py-[2px]; }
|
||||
.rail-eyebrow { @apply block font-mono text-[10px] uppercase tracking-[.16em] text-[#8a8455]; }
|
||||
.rail-brand { @apply font-serif font-extrabold text-[#14110d] m-0 mt-[2px] leading-[1.02] tracking-[-.01em]; font-size: 22px; }
|
||||
.rail-tag { @apply font-mono text-[10px] uppercase tracking-[.12em] m-0 mt-[2px]; color: #9a937f; }
|
||||
.rail-bar {
|
||||
@apply flex items-center justify-between w-full mt-3 px-3 py-[8px] text-white font-mono text-[10px] uppercase tracking-[.16em] font-bold no-underline border transition-colors duration-200;
|
||||
background: #8a8455; border-color: #8a8455;
|
||||
}
|
||||
.rail-bar:hover { @apply no-underline; background: #fff; color: #8a8455; }
|
||||
.rail-bar-plus { @apply text-[14px] leading-none font-light; }
|
||||
.rail-desc { @apply font-mono text-[10px] mt-2 leading-[1.5] tracking-[.02em]; color: #9a937f; }
|
||||
.rail-dot { @apply mx-[3px]; color: #ccc; }
|
||||
.rail-thumb {
|
||||
position: relative; flex: 1; min-height: 160px; border-radius: 2px; overflow: hidden;
|
||||
background: #f0f0f0; display: block; text-decoration: none;
|
||||
}
|
||||
@media (min-width: 768px) { .rail-thumb { min-height: 0; } }
|
||||
.rail-thumb img { @apply absolute inset-0 w-full h-full object-cover transition-transform duration-[600ms] ease-out; }
|
||||
.rail-thumb:hover img { @apply scale-[1.04]; }
|
||||
|
||||
/* 卡片上的系列 / 季节标签 */
|
||||
.prod-meta { @apply flex gap-2 flex-wrap mt-1; }
|
||||
.prod-chip {
|
||||
@apply font-mono text-[10px] uppercase tracking-[.1em] px-[8px] py-[3px] rounded-full border border-[#ddd] text-[#666] bg-white;
|
||||
}
|
||||
.empty { @apply text-center text-[13px] text-[#999] py-16 font-mono uppercase tracking-[.12em]; }
|
||||
|
||||
/* 查看更多(首页走秀栏内联分页) */
|
||||
.showmore-wrap { @apply flex justify-center mt-10; }
|
||||
.showmore {
|
||||
@apply font-mono uppercase tracking-[.16em] text-[12px] px-[28px] py-[14px] border border-[#111] bg-white text-[#111] rounded-full cursor-pointer transition-colors duration-200;
|
||||
}
|
||||
.showmore:hover { @apply bg-[#111] text-white; }
|
||||
.showmore[disabled] { @apply opacity-40 cursor-default; }
|
||||
|
||||
/* (图片堆方案已移除,改由卡片内缩略图条展示多图) */
|
||||
|
||||
</style>
|
||||
|
||||
<style is:global>
|
||||
/* JS 启用时主图/缩略图先透明,加载完成淡入;失败保留占位骨架(is:global 避免组件作用域误伤 .js 选择器) */
|
||||
.js .prod-main-img { @apply opacity-0; }
|
||||
.js .prod-main-img.is-loaded { @apply opacity-100; }
|
||||
.js .prod-thumb img { @apply opacity-0; }
|
||||
.js .prod-thumb img.is-loaded { @apply opacity-100; }
|
||||
.prod-main-img.is-error, .prod-thumb img.is-error { opacity: 0 !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 标签页交互:默认 Shows 为 SSR;点击其它标签时按需请求接口渲染(懒加载)
|
||||
const API_BASE = (import.meta.env.PUBLIC_API_BASE || "http://localhost:8090").replace(/\/$/, "");
|
||||
const tabs = Array.from(document.querySelectorAll<HTMLElement>("#tabbar .tab"));
|
||||
const panels = Array.from(document.querySelectorAll<HTMLElement>(".panel"));
|
||||
|
||||
function toAbs(u: string): string {
|
||||
if (!u) return "";
|
||||
return /^https?:\/\//i.test(u) ? u : API_BASE + u;
|
||||
}
|
||||
|
||||
// 面板数据区段(与后端真实列表对齐):各面板取不同区段,互不重复
|
||||
const PANEL_SLICE: Record<string, [number, number]> = {
|
||||
backstage: [8, 16],
|
||||
events: [16, 25],
|
||||
street: [24, 32],
|
||||
};
|
||||
const LABEL: Record<string, string> = { shows: "Show", backstage: "Backstage", events: "Trend", street: "Street" };
|
||||
const SERIES_LABELS: Record<string, string> = { rtw: "成衣 RTW", menswear: "男装", couture: "高定", resort: "度假", pre_fall: "早秋" };
|
||||
|
||||
let panelCache: any[] | null = null;
|
||||
async function ensurePanelData(): Promise<any[]> {
|
||||
if (panelCache) return panelCache;
|
||||
const res = await fetch(`${API_BASE}/api/public/articles?page=1&size=32&with_images=5`);
|
||||
const json = await res.json();
|
||||
const raw: any[] = json?.data ?? [];
|
||||
panelCache = raw.map((r) => ({
|
||||
id: r.id,
|
||||
brand: r.brand_name || "Unknown",
|
||||
title: r.title || "",
|
||||
season: r.year ? "AW" + String(r.year).slice(-2) : "",
|
||||
season_code: r.season_code || "",
|
||||
collection_type: r.collection_type || "",
|
||||
year: r.year,
|
||||
img: toAbs(r.cover || ""),
|
||||
count: r.image_count || 0,
|
||||
gallery: (r.images || []).map((im: any) => toAbs(im.image || "")).filter(Boolean).slice(0, 5),
|
||||
}));
|
||||
return panelCache;
|
||||
}
|
||||
|
||||
function skeletonHTML(n = 8): string {
|
||||
let s = "";
|
||||
for (let i = 0; i < n; i++) {
|
||||
s += `<div class="skel-card"><div class="skel-main"></div><div class="skel-line"></div><div class="skel-line short"></div></div>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function cardHTML(it: any, i: number, label: string): string {
|
||||
const g = it.gallery && it.gallery.length ? it.gallery : [it.img];
|
||||
const thumbs = g.map((src: string, gi: number) =>
|
||||
`<button class="prod-thumb" type="button" aria-label="查看第 ${gi + 1} 张"><img src="${src}" alt="" loading="lazy" /></button>`
|
||||
).join("");
|
||||
const more = it.count > g.length
|
||||
? `<a href="/article?id=${it.id}" class="prod-more">+${it.count - g.length}</a>`
|
||||
: "";
|
||||
const badges =
|
||||
(i === 0 ? `<span class="prod-badge new">新品</span>` : "") +
|
||||
(it.count > 3 ? `<span class="prod-badge hot">热门</span>` : "");
|
||||
return `
|
||||
<article class="prod-card group" data-article-id="${it.id}">
|
||||
<div class="prod-main">
|
||||
<img src="${g[0]}" alt="${it.brand}" class="prod-main-img" loading="lazy" />
|
||||
<span class="prod-tag tl">${label}</span>
|
||||
${it.count > 1 ? `<span class="prod-tag tr">${it.count} Pics</span>` : ""}
|
||||
<a href="/article?id=${it.id}" class="prod-viewall">查看全部 ${it.count} 张 →</a>
|
||||
</div>
|
||||
<div class="prod-thumbs">${thumbs}${more}</div>
|
||||
<div class="prod-info">
|
||||
<div class="prod-name">${it.brand}</div>
|
||||
<div class="prod-meta">
|
||||
<span class="prod-chip">${SERIES_LABELS[it.collection_type] || "成衣 RTW"}</span>
|
||||
${it.season_code ? `<span class="prod-chip">${it.season_code}</span>` : ""}
|
||||
</div>
|
||||
<div class="prod-sub">${it.title}</div>
|
||||
<div class="prod-tags">${badges}</div>
|
||||
<div class="prod-foot">
|
||||
<span class="prod-price">${it.season}</span>
|
||||
<a href="/article?id=${it.id}" class="prod-cta">查看 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function initCardInteractions(root: ParentNode) {
|
||||
root.querySelectorAll<HTMLElement>(".prod-card").forEach((card) => {
|
||||
const main = card.querySelector<HTMLImageElement>(".prod-main-img");
|
||||
const thumbs = Array.from(card.querySelectorAll<HTMLElement>(".prod-thumb"));
|
||||
if (!main || !thumbs.length) return;
|
||||
thumbs[0].classList.add("active");
|
||||
thumbs.forEach((t) => {
|
||||
t.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const img = t.querySelector("img");
|
||||
const src = img?.getAttribute("src") || "";
|
||||
if (!src || main.getAttribute("src") === src) return;
|
||||
main.classList.remove("is-loaded");
|
||||
main.parentElement?.classList.remove("is-loaded");
|
||||
main.style.opacity = "0";
|
||||
window.setTimeout(() => {
|
||||
main.setAttribute("src", src);
|
||||
main.style.opacity = "1";
|
||||
}, 180);
|
||||
thumbs.forEach((x) => x.classList.remove("active"));
|
||||
t.classList.add("active");
|
||||
});
|
||||
});
|
||||
main.addEventListener("load", () => {
|
||||
main.classList.add("is-loaded");
|
||||
main.parentElement?.classList.add("is-loaded");
|
||||
});
|
||||
enablePinch(card.querySelector<HTMLElement>(".prod-main"));
|
||||
});
|
||||
root.querySelectorAll<HTMLImageElement>(".prod-main-img, .prod-thumb img").forEach(watchImg);
|
||||
}
|
||||
|
||||
async function loadPanel(name: string) {
|
||||
const panel = panels.find((p) => p.getAttribute("data-panel") === name);
|
||||
if (!panel || panel.getAttribute("data-loaded") === "true" || panel.getAttribute("data-loading") === "true") return;
|
||||
panel.setAttribute("data-loading", "true");
|
||||
const grid = panel.querySelector<HTMLElement>(".grid");
|
||||
if (grid) grid.innerHTML = skeletonHTML(8);
|
||||
try {
|
||||
const all = await ensurePanelData();
|
||||
const [a, b] = PANEL_SLICE[name] || [0, 8];
|
||||
const items = all.slice(a, b);
|
||||
if (grid) {
|
||||
grid.innerHTML = items.map((it, i) => cardHTML(it, i, LABEL[name] || "Show")).join("");
|
||||
initCardInteractions(grid);
|
||||
}
|
||||
panel.setAttribute("data-loaded", "true");
|
||||
} catch {
|
||||
if (grid) grid.innerHTML = `<div class="text-sm text-gray-400 p-6">内容加载失败,请重试</div>`;
|
||||
}
|
||||
panel.setAttribute("data-loading", "false");
|
||||
}
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
// Brands 是导航型标签(<a href="/brands">):点击直接跳转品牌档案页,不切换面板
|
||||
if (tab.tagName.toLowerCase() === "a") return;
|
||||
tab.addEventListener("click", () => {
|
||||
const target = tab.getAttribute("data-tab");
|
||||
if (!target) return;
|
||||
tabs.forEach((t) => t.setAttribute("data-active", String(t === tab)));
|
||||
panels.forEach((p) => {
|
||||
p.setAttribute("data-show", String(p.getAttribute("data-panel") === target));
|
||||
});
|
||||
// 暂未上线标签:仅切换面板显示占位内容,不请求数据
|
||||
if (tab.getAttribute("data-soon") === "true") return;
|
||||
if (target !== "shows") loadPanel(target);
|
||||
});
|
||||
});
|
||||
|
||||
// 首页发布会面板:系列快速筛选(前端本地过滤,按 data-ct 显示/隐藏)
|
||||
let homeFType = ""
|
||||
const seriesApply = (function () {
|
||||
const bar = document.getElementById("seriesbar");
|
||||
if (!bar) return () => {};
|
||||
const grid = document.getElementById("showgrid");
|
||||
const empty = document.getElementById("showempty");
|
||||
const chips = Array.from(bar.querySelectorAll<HTMLButtonElement>(".schip"));
|
||||
function apply(ct: string) {
|
||||
homeFType = ct;
|
||||
const ok = (el: Element | null) => !!el && (!ct || (el.getAttribute("data-ct") || "") === ct);
|
||||
let vis = 0;
|
||||
if (grid) grid.querySelectorAll<HTMLElement>(".prod-card").forEach((c) => {
|
||||
const m = ok(c); c.style.display = m ? "" : "none"; if (m) vis++;
|
||||
});
|
||||
// 精选轮播(Featured)始终展示,不参与系列筛选隐藏
|
||||
if (empty) empty.hidden = vis > 0;
|
||||
}
|
||||
chips.forEach((b) => b.addEventListener("click", () => {
|
||||
chips.forEach((x) => x.classList.toggle("on", x === b));
|
||||
apply(b.getAttribute("data-ct") || "");
|
||||
}));
|
||||
return apply;
|
||||
})();
|
||||
|
||||
// 首页发布会面板:查看更多(前端分页追加,带系列筛选参数)
|
||||
(function () {
|
||||
let homePage = 2;
|
||||
let homeBusy = false;
|
||||
const btn = document.getElementById("showmore") as HTMLButtonElement | null;
|
||||
if (!btn) return;
|
||||
btn.addEventListener("click", async () => {
|
||||
if (homeBusy) return;
|
||||
homeBusy = true;
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = "加载中…";
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const p = new URLSearchParams({ page: String(homePage), size: "8", with_images: "5" });
|
||||
if (homeFType) p.set("collection_type", homeFType);
|
||||
const res = await fetch(`${API_BASE}/api/public/articles?${p.toString()}`);
|
||||
const json = await res.json();
|
||||
const rows = json?.data ?? [];
|
||||
const grid = document.getElementById("showgrid");
|
||||
if (grid && rows.length) {
|
||||
const html = rows.map((r: any) => {
|
||||
const g = (r.images || []).map((im: any) => toAbs(im.image || "")).filter(Boolean).slice(0, 5);
|
||||
const it = {
|
||||
id: r.id, brand: r.brand_name || "Unknown", title: r.title || "",
|
||||
season: r.year ? "AW" + String(r.year).slice(-2) : "",
|
||||
season_code: r.season_code || "", collection_type: r.collection_type || "",
|
||||
year: r.year, img: toAbs(r.cover || ""), count: r.image_count || 0, gallery: g,
|
||||
};
|
||||
return cardHTML(it, 99, "Runway");
|
||||
}).join("");
|
||||
grid.insertAdjacentHTML("beforeend", html);
|
||||
initCardInteractions(grid);
|
||||
seriesApply(homeFType);
|
||||
homePage++;
|
||||
if (rows.length < 8) { btn.textContent = "没有更多了"; return; }
|
||||
} else {
|
||||
btn.textContent = "没有更多了"; return;
|
||||
}
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
} catch {
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
} finally {
|
||||
homeBusy = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// 启用 JS 标记:用于渐进增强的占位符淡入(无 JS 时图片直接显示,不影响可用性)
|
||||
document.documentElement.classList.add("js");
|
||||
|
||||
// 图片占位符:加载完成淡入并停止骨架动画;失败保留占位骨架
|
||||
function watchImg(img: HTMLImageElement) {
|
||||
const done = () => {
|
||||
img.classList.add("is-loaded");
|
||||
img.parentElement?.classList.add("is-loaded");
|
||||
};
|
||||
const fail = () => {
|
||||
img.classList.add("is-error");
|
||||
img.removeAttribute("src");
|
||||
};
|
||||
if (img.complete) {
|
||||
if (img.naturalWidth > 0) done();
|
||||
else fail();
|
||||
} else {
|
||||
img.addEventListener("load", done, { once: true });
|
||||
img.addEventListener("error", fail, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 SSR 的 Shows 面板卡片交互(缩略图切换 / 占位淡入 / 双指缩放)
|
||||
initCardInteractions(document);
|
||||
|
||||
// 移动端双指缩放主图(pinch-to-zoom)+ 单指平移
|
||||
function enablePinch(box: HTMLElement | null) {
|
||||
if (!box) return;
|
||||
const img = box.querySelector<HTMLImageElement>(".prod-main-img");
|
||||
if (!img) return;
|
||||
let scale = 1, startDist = 0, startScale = 1, panX = 0, panY = 0, sx = 0, sy = 0;
|
||||
const dist = (a: Touch, b: Touch) => Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
||||
box.addEventListener("touchstart", (e: TouchEvent) => {
|
||||
if (e.touches.length === 2) {
|
||||
startDist = dist(e.touches[0], e.touches[1]);
|
||||
startScale = scale;
|
||||
img.style.transition = "none";
|
||||
} else if (e.touches.length === 1 && scale > 1) {
|
||||
sx = e.touches[0].clientX - panX;
|
||||
sy = e.touches[0].clientY - panY;
|
||||
}
|
||||
}, { passive: true });
|
||||
box.addEventListener("touchmove", (e: TouchEvent) => {
|
||||
if (e.touches.length === 2 && startDist) {
|
||||
e.preventDefault();
|
||||
scale = Math.min(4, Math.max(1, startScale * dist(e.touches[0], e.touches[1]) / startDist));
|
||||
panX = 0; panY = 0;
|
||||
} else if (e.touches.length === 1 && scale > 1) {
|
||||
e.preventDefault();
|
||||
panX = e.touches[0].clientX - sx;
|
||||
panY = e.touches[0].clientY - sy;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
img.style.transform = `scale(${scale}) translate(${panX}px, ${panY}px)`;
|
||||
}, { passive: false });
|
||||
box.addEventListener("touchend", () => {
|
||||
if (scale <= 1) {
|
||||
scale = 1; panX = 0; panY = 0;
|
||||
img.style.transition = "";
|
||||
img.style.transform = "";
|
||||
} else {
|
||||
img.style.transition = "transform .2s ease";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 首页精选轮播(左图右文卡片式)
|
||||
(function () {
|
||||
const root = document.getElementById("heroCarousel");
|
||||
if (!root) return;
|
||||
const slides = Array.from(root.querySelectorAll<HTMLElement>(".hc-slide"));
|
||||
const total = slides.length;
|
||||
if (total <= 1) {
|
||||
root.querySelectorAll<HTMLElement>(".hc-controls").forEach((c) => (c.style.display = "none"));
|
||||
return;
|
||||
}
|
||||
let idx = 0;
|
||||
let timer: number | undefined;
|
||||
const dots = Array.from(root.querySelectorAll<HTMLButtonElement>(".hc-dot"));
|
||||
function go(n: number) {
|
||||
idx = (n + total) % total;
|
||||
slides.forEach((s, i) => { s.style.display = i === idx ? "" : "none"; });
|
||||
// 每屏各自带一套 controls,只高亮当前可见 slide 内的圆点
|
||||
const visDots = slides[idx].querySelectorAll<HTMLElement>(".hc-dot");
|
||||
visDots.forEach((d, i) => d.classList.toggle("on", i === idx));
|
||||
}
|
||||
function start() { stop(); timer = window.setInterval(() => go(idx + 1), 5000); }
|
||||
function stop() { if (timer) window.clearInterval(timer); }
|
||||
root.querySelectorAll<HTMLButtonElement>(".hc-prev").forEach((b) =>
|
||||
b.addEventListener("click", () => { go(idx - 1); start(); })
|
||||
);
|
||||
root.querySelectorAll<HTMLButtonElement>(".hc-next").forEach((b) =>
|
||||
b.addEventListener("click", () => { go(idx + 1); start(); })
|
||||
);
|
||||
dots.forEach((d) =>
|
||||
d.addEventListener("click", () => { go(Number(d.dataset.i)); start(); })
|
||||
);
|
||||
root.addEventListener("mouseenter", stop);
|
||||
root.addEventListener("mouseleave", start);
|
||||
start();
|
||||
})();
|
||||
|
||||
// 右侧品牌栏:最新 / 最热 切换(事件委托,稳健绑定)
|
||||
(function () {
|
||||
const wrap = document.querySelector(".hc-rail-tabs");
|
||||
const railList = document.getElementById("railList");
|
||||
if (!wrap || !railList) return;
|
||||
const sets = railList.querySelectorAll<HTMLElement>(".rail-set");
|
||||
wrap.addEventListener("click", (e) => {
|
||||
const btn = (e.target as HTMLElement).closest("button");
|
||||
if (!btn) return;
|
||||
const sort = btn.dataset.sort;
|
||||
if (!sort) return;
|
||||
wrap.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
|
||||
sets.forEach((s) => { s.style.display = s.dataset.set === sort ? "" : "none"; });
|
||||
});
|
||||
})();
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user