update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-08-15 01:07:33 +08:00
parent 8d65eb8850
commit 58f9f95f8e
8 changed files with 1383 additions and 600 deletions

View File

@ -6,68 +6,20 @@ 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} 发布` : "") }
// 横向滚动走秀画廊:扁平化所有 show 的图片(cover + gallery 去重),
// 每张卡片 = 一个文章(show)的封面,不展开 gallery,避免同一文章重复出现。
type GalleryItem = { src: string; brand: string; articleId: number; year: number; title: string; collection_type?: string }
const galleryItems: GalleryItem[] = []
for (const s of shows) {
if (!s.img) continue
galleryItems.push({
src: s.img,
brand: s.brand,
articleId: s.id,
year: s.year,
title: s.title,
collection_type: s.collection_type,
})
}
---
@ -104,150 +56,39 @@ function railDesc(r: typeof shows[number]) {
<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>
))}
<!-- 横向滚动画廊:左右滑动浏览;图片加载前骨架占位防偏移 -->
{galleryItems.length > 0 && (
<section class="hs-section">
<div class="hs-stage">
<div class="swiper hs-swiper" id="hsSwiper">
<div class="swiper-wrapper">
{galleryItems.map((g, i) => (
<div class="swiper-slide hs-slide">
<a class="hs-item" href={`/article?id=${g.articleId}`} data-idx={i}>
<div class="hs-img-box">
<div class="hs-skel" aria-hidden="true"></div>
<span class="hs-spin" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-dasharray="40 22" class="hs-ring" /></svg>
</span>
<img src={g.src} alt={g.brand} loading={i < 4 ? "eager" : "lazy"} decoding="async" />
<span class="hs-caption">
<span class="hs-cap-brand">{g.brand}</span>
<span class="hs-cap-meta">{g.year ? g.year : ""}{g.collection_type ? " · " + seriesLabel(g.collection_type) : ""}</span>
</span>
</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>
</div>
</aside>
)}
<div class="hs-controls flex justify-between items-center text-[13px] font-bold">
<div class="hs-arrow hs-prev cursor-pointer hover:opacity-60 transition-opacity duration-200" role="button" tabindex="0">Previous</div>
<div class="hs-arrow hs-next cursor-pointer hover:opacity-60 transition-opacity duration-200" role="button" tabindex="0">Next</div>
</div>
</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>
<!-- 灵感 面板(暂未上线) -->
@ -265,12 +106,6 @@ function railDesc(r: typeof shows[number]) {
<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>
@ -296,83 +131,8 @@ function railDesc(r: typeof shows[number]) {
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]; }
@ -403,88 +163,144 @@ function railDesc(r: typeof shows[number]) {
.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;
/* 横向滚动画廊(Runway Gallery):水平 flex 容器,每张图按统一卡片尺寸展示、
左右滑动浏览。图片用 object-fit:cover 适配卡片框(不再因原图宽高比过宽);
加载前骨架 shimmer 占位,加载完图片淡入、骨架淡出。 */
.hs-section { @apply mb-9; }
.hs-head {
@apply flex items-end justify-between gap-4 pb-4 mb-4 border-b border-[#ececec];
}
.schip:hover { @apply text-[#111] border-[#111]; }
.schip.on { @apply bg-[#111] text-white border-[#111]; }
.hs-title { @apply font-serif text-[28px] leading-none font-extrabold text-[#111] m-0 tracking-[-.01em]; }
.hs-sub { @apply font-mono text-[12px] uppercase tracking-[.14em] text-[#999] mt-2 mb-0; }
.hs-stage { @apply relative; }
.hs-controls {
position: absolute;
top: 50%;
left: 0;
right: 0;
transform: translateY(-50%);
z-index: 6;
padding: 0 8px;
pointer-events: none; /* 容器不挡拖拽,仅按钮可点 */
mix-blend-mode: difference; /* 随背景反相,始终可见 */
color: #fff; /* 白色源色经 difference 后随底图反相 */
}
.hs-controls .hs-arrow { pointer-events: auto; }
.hs-arrow { @apply select-none; } /* 仅保留 Swiper 绑定所需的类,视觉完全由内联类名控制 */
.hs-arrow.swiper-button-disabled { pointer-events: none; } /* 禁用时保持原色,仅不可点击 */
/* 精选 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;
/* Swiper 容器:高度恒定(内容区固定);宽度由 slide 自适应撑开 */
.hs-swiper {
width: 100%;
height: clamp(500px, 80vh, 900px); /* 固定高度,不随图片尺寸变化(已调高) */
padding: 0 4px;
}
@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; }
.hs-swiper .swiper-wrapper { align-items: center; } /* 卡片在固定高度的容器内垂直居中 */
/* 右侧品牌栏(横版卡片,图左文右,×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;
/* 每张 slide:尺寸完全由内部图片决定(slidesPerView:"auto" 取自身宽) */
.hs-swiper .hs-slide {
width: auto;
height: auto;
}
.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;
.hs-item {
display: block;
height: auto; /* 高度交给图片框,按图片比例自适应 */
width: max-content; /* 宽度收缩到图片自身宽度 */
text-decoration: none;
color: inherit;
}
.hs-img-box {
position: relative;
display: block;
/* 加载前默认占位:统一高度的 3:2 横向占位,避免塌缩;加载后由 JS 按真实比例写入精确 px */
height: clamp(500px, 80vh, 900px);
/* aspect-ratio: 3 / 2; */
background: #f0f0f0;
overflow: hidden;
}
/* 图片:填满图片框;图片框的比例由 JS 按真实宽高比设定,故完整显示、不裁切、不变形 */
.hs-img-box > img {
width: 100%;
height: 100%;
display: block;
position: relative;
z-index: 1;
opacity: 0;
transition: opacity 0.4s ease;
}
.hs-img-box.is-loaded > img { opacity: 1; }
/* 骨架占位:绝对覆盖 img-box,shimmer 动画;加载完淡出(与图片淡入同步) */
.hs-skel {
position: absolute;
inset: 0;
z-index: 0;
background-image: linear-gradient(100deg, #f0f0f0 30%, #e6e6e6 50%, #f0f0f0 70%);
background-size: 200% 100%;
animation: hs-shimmer 1.3s ease-in-out infinite;
transition: opacity 0.35s ease;
}
.hs-img-box.is-loaded .hs-skel { opacity: 0; }
/* 加载中旋转环(细线灰调) */
.hs-spin {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #bdbdbd;
pointer-events: none;
z-index: 2;
transition: opacity 0.3s ease;
}
.hs-img-box.is-loaded .hs-spin { opacity: 0; }
.hs-spin svg { display: block; }
.hs-ring { transform-origin: 12px 12px; animation: hs-rotate 0.8s linear infinite; }
@keyframes hs-rotate { to { transform: rotate(360deg); } }
@keyframes hs-shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
/* 悬停浮现品牌标签 */
.hs-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 28px 16px 12px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: white;
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 12px;
opacity: 0;
transition: opacity 0.25s ease;
z-index: 3;
font-family: monospace;
pointer-events: none;
}
.hs-item:hover .hs-caption { opacity: 1; }
.hs-cap-brand {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.hs-cap-meta {
font-size: 11px;
opacity: 0.85;
letter-spacing: 0.12em;
}
@media (max-width: 768px) {
.hs-swiper { height: clamp(340px, 56vh, 540px); }
.hs-img-box { height: clamp(340px, 56vh, 540px); }
.hs-head { flex-direction: column; align-items: flex-start; gap: 12px; }
}
@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; }
@ -493,28 +309,24 @@ function railDesc(r: typeof shows[number]) {
}
.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; }
/* JS 启用时画廊图片先透明,加载完成淡入;失败保留占位骨架(is:global 避免组件作用域误伤 .js 选择器) */
.js .hs-img-box > img { opacity: 0; }
.js .hs-img-box.is-loaded > img { opacity: 1; }
.hs-img-box > img.is-error { opacity: 0 !important; }
</style>
<script>
import Swiper from "swiper";
import { Navigation, FreeMode, Keyboard } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/free-mode";
// 标签页交互:默认 Shows 为 SSR;点击其它标签时按需请求接口渲染(懒加载)
const API_BASE = (import.meta.env.PUBLIC_API_BASE || "http://localhost:8090").replace(/\/$/, "");
const tabs = Array.from(document.querySelectorAll<HTMLElement>("#tabbar .tab"));
@ -599,59 +411,59 @@ function railDesc(r: typeof shows[number]) {
</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);
}
// 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");
}
// 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">):点击直接跳转品牌档案页,不切换面板
@ -669,80 +481,6 @@ function railDesc(r: typeof shows[number]) {
});
});
// 首页发布会面板:系列快速筛选(前端本地过滤,按 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");
@ -766,7 +504,7 @@ function railDesc(r: typeof shows[number]) {
}
// 初始化 SSR 的 Shows 面板卡片交互(缩略图切换 / 占位淡入 / 双指缩放)
initCardInteractions(document);
// initCardInteractions(document);
// 移动端双指缩放主图(pinch-to-zoom)+ 单指平移
function enablePinch(box: HTMLElement | null) {
@ -810,55 +548,51 @@ function railDesc(r: typeof shows[number]) {
});
}
// 首页精选轮播(左图右文卡片式)
// 横向滚动画廊(Swiper):容器高度固定,按每张图片原始宽高比自适应该卡尺寸
(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();
})();
const el = document.getElementById("hsSwiper");
if (!el) return;
// 右侧品牌栏:最新 / 最热 切换(事件委托,稳健绑定)
(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"; });
let swiper: any = null;
let updateScheduled = false;
// 图片尺寸随加载/视口变化,需在下一帧通知 Swiper 重新测量布局(防偏移)
const scheduleUpdate = () => {
if (updateScheduled) return;
updateScheduled = true;
requestAnimationFrame(() => { updateScheduled = false; if (swiper) swiper.update(); });
};
// 图片加载:按真实比例设定尺寸 + 完成淡入;失败保留灰底占位
const imgs = el.querySelectorAll<HTMLImageElement>(".hs-img-box img");
imgs.forEach((img) => {
const box = img.closest(".hs-img-box") as HTMLElement | null;
const done = () => {img.classList.add("is-loaded"); box?.classList.add("is-loaded"); scheduleUpdate(); };
const fail = () => {
if (box) { box.style.width = "360px"; box.style.height = (el.clientHeight || 600) + "px"; }
img.style.display = "none";
box?.classList.add("is-loaded");
scheduleUpdate();
};
if (img.complete) { if (img.naturalWidth > 0) done(); else fail(); }
else { img.addEventListener("load", done, { once: true }); img.addEventListener("error", fail, { once: true }); }
});
// 箭头按钮位于容器外的 header 内,直接传元素以确保正确绑定
const prevBtn = el.parentElement?.querySelector<HTMLElement>(".hs-prev") || document.querySelector(".hs-prev");
const nextBtn = el.parentElement?.querySelector<HTMLElement>(".hs-next") || document.querySelector(".hs-next");
swiper = new Swiper(el, {
modules: [Navigation, FreeMode, Keyboard],
slidesPerView: "auto", // 每张 slide 取自身宽度(由图片宽高比决定)
spaceBetween: 4, // slide 间距
grabCursor: true, // 可拖拽光标
speed: 220, // 箭头/吸附过渡时长(ms),调小让拖拽松手后更快吸附
freeMode: { enabled: true, sticky: true, momentumBounce: false, momentumRatio: 0.5 }, // 自由拖拽 + 松手粘性吸附(momentumRatio 调小减短滑行)
keyboard: { enabled: true },
navigation: { prevEl: prevBtn, nextEl: nextBtn, disabledClass: "swiper-button-disabled" },
updateOnImagesReady: true, // 图片就绪后自动 update
observer: true,
observeSlideChildren: true,
});
})();