1008 lines
40 KiB
Plaintext
1008 lines
40 KiB
Plaintext
---
|
||
import Layout from "../layouts/Layout.astro"
|
||
|
||
// 全部秀场档案库:HBX 风左筛选 + 右 4 列商品卡。
|
||
// 筛选交互(设计师多选 / 系列类型 / 季节 / 年份 / 排序)全部通过 querystring 同步到后端,
|
||
// 真正由 `/api/public/articles` 的 brand_ids / collection_type / season / year / sort 参数承担。
|
||
|
||
// 侧边栏品牌列表:featured=1 取 ~198 个有走秀档案的代表品牌,含 article_count 作为计数。
|
||
// dev 期(import.meta.env.DEV)自动兜底到本地后端,避免每次手敲环境变量;生产走 API_SSR 构建参数。
|
||
import { getBrands, getYears, type BrandEntry } from "../lib/api"
|
||
|
||
let sidebarBrands: BrandEntry[] = []
|
||
let yearChips: number[] = []
|
||
|
||
// SSR 期从后端拉精选品牌(有走秀档案的代表品牌,含计数),运行期前端用同一封装再拉。
|
||
try {
|
||
sidebarBrands = (await getBrands({ size: 300, only_with_articles: 1, featured: 1 }))
|
||
.filter((b) => b.id && b.name)
|
||
.sort((a, b) => a.name.localeCompare(b.name))
|
||
} catch {}
|
||
|
||
// 年份 chips:从 back-end 拉最近 30 条秀场,提取独特年份(按降序去重),构造最近 6 年。
|
||
// 为 SSR 提供稳健的初始值,运行期会按筛选再补;不可达时给一组 fallback 区间。
|
||
let yearSeed = [2026, 2025, 2024, 2023, 2022, 2021]
|
||
try {
|
||
const ys = await getYears(30)
|
||
if (ys.length) yearChips = ys
|
||
} catch {}
|
||
if (!yearChips.length) yearChips = yearSeed
|
||
|
||
// URL 链接:保留当前路径以便 client-side filter 同步到地址栏
|
||
const pathname = "/shows"
|
||
---
|
||
|
||
<Layout title="All Shows — Portfolio Studio">
|
||
<!-- 首屏绘制前同步侧边栏折叠状态,杜绝刷新时的宽度闪动(无闪烁) -->
|
||
<script is:inline>
|
||
try {
|
||
if (localStorage.getItem("showsaside-collapsed") === "1") {
|
||
document.documentElement.classList.add("showsaside-collapsed")
|
||
}
|
||
} catch (e) {}
|
||
</script>
|
||
<div class="showsroot">
|
||
|
||
<!-- 顶部:极简眉头 -->
|
||
<header class="showshead">
|
||
<div class="showshead-l">
|
||
<span class="showskicker">Runway Archive · 2026 Edition</span>
|
||
<h1 class="showsh1">All Shows</h1>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 已激活筛选 pills -->
|
||
<div id="showspills" class="showspills" hidden></div>
|
||
|
||
<!-- 主体:左 sidebar + 右 4 列商品 grid -->
|
||
<div class="showsbody">
|
||
|
||
<!-- LEFT SIDEBAR -->
|
||
<aside class="showsaside" id="showsaside">
|
||
|
||
<div class="showsaside-head">
|
||
<h2 class="showsaside-h">REFINE</h2>
|
||
<button id="showstoggle" class="showsaside-toggle" type="button" aria-label="折叠/展开筛选">
|
||
<svg viewBox="0 0 12 12" width="11" height="11" aria-hidden="true">
|
||
<path d="M7.5 3l-4 3 4 3" stroke="currentColor" fill="none" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="showsaside-body">
|
||
|
||
<!-- Designer 多选 -->
|
||
<section class="showsg">
|
||
<header class="showsg-h" data-group="designers" role="button" tabindex="0" aria-expanded="true">
|
||
<span>DESIGNERS</span>
|
||
<span class="showsg-right">
|
||
<span class="showsg-count" id="brand-count">0</span>
|
||
<span class="showsg-chev" aria-hidden="true"><svg viewBox="0 0 12 12" width="11" height="11"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
|
||
</span>
|
||
</header>
|
||
<div class="showsg-b">
|
||
<input id="brand-search" class="showssearch" placeholder="Search designer…" autocomplete="off" />
|
||
<ul id="brand-list" class="showschecks">
|
||
<!-- 由 JS 把 sidebarBrands 渲染进来 -->
|
||
</ul>
|
||
<a id="brand-showmore" class="showsmore">Show all 198 designers ▾</a>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Collection type - 多选复选框 -->
|
||
<section class="showsg">
|
||
<header class="showsg-h" data-group="collection" role="button" tabindex="0" aria-expanded="true"><span>COLLECTION</span><span class="showsg-chev" aria-hidden="true"><svg viewBox="0 0 12 12" width="11" height="11"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span></header>
|
||
<div class="showsg-b">
|
||
<ul id="ct-checks" class="showschecks showschecks--flat">
|
||
<li><label><input type="checkbox" value="rtw" /> <span>Ready-to-Wear</span></label></li>
|
||
<li><label><input type="checkbox" value="menswear" /> <span>Menswear</span></label></li>
|
||
<li><label><input type="checkbox" value="couture" /> <span>Couture</span></label></li>
|
||
<li><label><input type="checkbox" value="resort" /> <span>Resort</span></label></li>
|
||
<li><label><input type="checkbox" value="pre_fall" /> <span>Pre-Fall</span></label></li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Season - 多选复选框 -->
|
||
<section class="showsg">
|
||
<header class="showsg-h" data-group="season" role="button" tabindex="0" aria-expanded="true"><span>SEASON</span><span class="showsg-chev" aria-hidden="true"><svg viewBox="0 0 12 12" width="11" height="11"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span></header>
|
||
<div class="showsg-b">
|
||
<ul id="season-checks" class="showschecks showschecks--flat">
|
||
<li><label><input type="checkbox" value="spring" /> <span>Spring / Summer</span></label></li>
|
||
<li><label><input type="checkbox" value="fall" /> <span>Fall / Winter</span></label></li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Year - 多选复选框 -->
|
||
<section class="showsg">
|
||
<header class="showsg-h" data-group="year" role="button" tabindex="0" aria-expanded="true"><span>YEAR</span><span class="showsg-chev" aria-hidden="true"><svg viewBox="0 0 12 12" width="11" height="11"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span></header>
|
||
<div class="showsg-b">
|
||
<ul id="year-checks" class="showschecks showschecks--flat">
|
||
{yearChips.map((y) => (
|
||
<li><label><input type="checkbox" value={String(y)} /> <span>{y}</span></label></li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<button id="showsclear" class="showsclear" type="button">Clear all ×</button>
|
||
|
||
</div><!-- /.showsaside-body -->
|
||
</aside>
|
||
|
||
<!-- MAIN grid -->
|
||
<main class="showsmain">
|
||
<div id="showsgrid" class="showsgrid">
|
||
<!-- Skeletons 占位:避免 dev SSR 时空白闪烁 -->
|
||
{Array.from({ length: 12 }, () => (
|
||
<div class="showscard showscard--skel">
|
||
<div class="showscard-left">
|
||
<div class="showscard-img"></div>
|
||
</div>
|
||
<div class="showscard-right">
|
||
<div class="showscard-info">
|
||
<div class="showsline" style="height:40px;margin:0"></div>
|
||
<div class="showsline short" style="height:14px;margin:12px 0 0"></div>
|
||
<div class="showsline" style="height:34px;margin:14px 0 0"></div>
|
||
</div>
|
||
<div class="showscard-strip">
|
||
<div class="showscard-thumb"></div>
|
||
<div class="showscard-thumb"></div>
|
||
<div class="showscard-thumb"></div>
|
||
<div class="showscard-thumb"></div>
|
||
<div class="showscard-thumb"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div class="showsloadmore-wrap">
|
||
<button id="showsloadmore" class="showsloadmore" type="button">Load More ↓</button>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
</Layout>
|
||
|
||
<!-- 注入 SSR 期品牌数据:避免 client-side 再发一次请求 -->
|
||
<script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script>
|
||
|
||
<style is:global>
|
||
@reference "tailwindcss";
|
||
|
||
.showsroot {
|
||
max-width: 1480px;
|
||
/* margin: auto; */
|
||
padding-top: 28px;
|
||
/* font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); */
|
||
color: #0a0a0a;
|
||
}
|
||
|
||
/* ---------- 顶部眉头 ---------- */
|
||
.showshead {
|
||
display: flex; justify-content: space-between; align-items: flex-end;
|
||
gap: 24px; padding: 22px 0 26px; border-bottom: 2px solid #000;
|
||
}
|
||
.showskicker {
|
||
display: inline-block;
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 11px; letter-spacing: .22em; text-transform: uppercase;
|
||
color: #000; margin-bottom: 10px;
|
||
}
|
||
.showsh1 {
|
||
font-family: var(--font-serif, ui-serif, Georgia, serif);
|
||
font-size: clamp(40px, 6vw, 72px); line-height: .9; font-weight: 900;
|
||
letter-spacing: -.02em; margin: 0 0 10px;
|
||
}
|
||
.showscount-label {
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
|
||
color: #000; font-weight: 700; margin-right: 4px;
|
||
}
|
||
.showssortwrap {
|
||
display: inline-flex; align-items: center; gap: 10px;
|
||
font-family: var(--font-mono, ui-monospace, monospace);
|
||
font-size: 11px; text-transform: uppercase; letter-spacing: .16em; color: #000;
|
||
}
|
||
.showssort {
|
||
appearance: none; -webkit-appearance: none;
|
||
border: 1.5px solid #000; background: #fff; color: #000;
|
||
font-family: inherit; font-size: 11px; letter-spacing: .14em;
|
||
text-transform: uppercase; padding: 10px 32px 10px 14px;
|
||
cursor: pointer; border-radius: 0;
|
||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2 4l4 4 4-4' stroke='%23000' fill='none' stroke-width='1.4'/></svg>");
|
||
background-repeat: no-repeat; background-position: right 10px center;
|
||
}
|
||
|
||
/* ---------- 已激活筛选 pills ---------- */
|
||
.showspills {
|
||
display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 0 0;
|
||
}
|
||
.showspill {
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
border: 1.5px solid #000; padding: 6px 10px;
|
||
font-family: var(--font-mono, monospace); font-size: 10.5px;
|
||
text-transform: uppercase; letter-spacing: .14em; color: #000; background: #fff;
|
||
}
|
||
.showspill .x {
|
||
display: inline-flex; width: 14px; height: 14px; align-items: center; justify-content: center;
|
||
border: none; background: #000; color: #fff; cursor: pointer; font-family: inherit; font-size: 12px; line-height: 1;
|
||
}
|
||
|
||
/* ---------- 主体:sidebar + grid ---------- */
|
||
.showsbody {
|
||
display: flex; gap: 36px;
|
||
align-items: flex-start;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.showsbody { flex-direction: column; gap: 18px; }
|
||
.showsaside { position: static !important; max-height: none !important; width: 100% !important; }
|
||
}
|
||
|
||
/* ---------- LEFT SIDEBAR(可折叠) ---------- */
|
||
.showsaside {
|
||
flex: 0 0 240px;
|
||
/* border: 1.5px solid #000; */
|
||
background: #fff;
|
||
position: sticky; top: 20px;
|
||
max-height: calc(100vh - 40px);
|
||
border-radius: 0;
|
||
transition: flex-basis .25s ease;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
html.showsaside-collapsed .showsaside {
|
||
flex: 0 0 44px;
|
||
}
|
||
html.showsaside-collapsed .showsaside-body { display: none; }
|
||
|
||
.showsaside-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 14px 14px 12px;
|
||
border-bottom: 1px solid #000;
|
||
flex-shrink: 0;
|
||
}
|
||
.showsaside-h {
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 11px; letter-spacing: .26em; text-transform: uppercase;
|
||
font-weight: 700; margin: 0; line-height: 1;
|
||
}
|
||
html.showsaside-collapsed .showsaside-h {
|
||
opacity: 0; pointer-events: none;
|
||
width: 0; overflow: hidden;
|
||
}
|
||
.showsaside-toggle {
|
||
appearance: none; -webkit-appearance: none;
|
||
background: #fff; border: 1.5px solid #000;
|
||
width: 22px; height: 22px;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
cursor: pointer; border-radius: 0;
|
||
color: #000; padding: 0;
|
||
transition: transform .25s ease;
|
||
}
|
||
.showsaside-toggle:hover { background: #000; color: #fff; }
|
||
html.showsaside-collapsed .showsaside-toggle {
|
||
margin-left: auto;
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.showsaside-body {
|
||
padding: 14px 14px 18px;
|
||
overflow-y: auto;
|
||
scrollbar-width: thin; scrollbar-color: #ccc #fff;
|
||
flex: 1; min-height: 0;
|
||
}
|
||
.showsaside-body::-webkit-scrollbar { width: 6px; }
|
||
.showsaside-body::-webkit-scrollbar-thumb { background: #ccc; }
|
||
|
||
.showsg { border-top: 1px solid #e6e6e6; padding: 12px 0 6px; }
|
||
.showsg:first-of-type { border-top: none; padding-top: 4px; }
|
||
.showsg-h {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 11px; letter-spacing: .22em; text-transform: uppercase;
|
||
font-weight: 700; color: #000; margin-bottom: 8px;
|
||
cursor: pointer; user-select: none;
|
||
}
|
||
.showsg-h:hover { color: #000; }
|
||
.showsg-right { display: inline-flex; align-items: center; gap: 10px; }
|
||
.showsg-chev {
|
||
display: inline-block; font-size: 9px; line-height: 1; color: #999;
|
||
transition: transform .2s ease; margin-left: 4px;
|
||
}
|
||
.showsg.is-collapsed .showsg-chev { transform: rotate(-90deg); }
|
||
.showsg.is-collapsed .showsg-b { display: none; }
|
||
.showsg-count {
|
||
color: #999; font-weight: 400;
|
||
font-family: var(--font-mono, monospace); font-size: 10px; letter-spacing: .14em;
|
||
}
|
||
|
||
.showssearch {
|
||
width: 100%; border: 1px solid #d4d4d4; padding: 7px 10px;
|
||
font-family: var(--font-mono, monospace); font-size: 11px;
|
||
text-transform: uppercase; letter-spacing: .08em; color: #000;
|
||
border-radius: 0; outline: none; background: #fafafa;
|
||
}
|
||
.showssearch:focus { border-color: #000; background: #fff; }
|
||
|
||
.showschecks { list-style: none; margin: 10px 0 0; padding: 0; max-height: 280px; overflow-y: auto; scrollbar-width: thin; }
|
||
.showschecks::-webkit-scrollbar { width: 4px; }
|
||
.showschecks::-webkit-scrollbar-thumb { background: #ddd; }
|
||
|
||
.showschecks li {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 5px 0;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;
|
||
color: #000; cursor: pointer; user-select: none;
|
||
}
|
||
.showschecks li:hover { background: #f6f6f6; }
|
||
.showschecks li[data-letter] {
|
||
padding: 8px 0 3px; font-size: 10px; letter-spacing: .22em;
|
||
color: #8a8a8a; cursor: default; pointer-events: none;
|
||
border-top: 1px dotted #e6e6e6;
|
||
}
|
||
.showschecks li[data-letter]:first-child { border-top: none; padding-top: 4px; }
|
||
.showschecks input[type="checkbox"] {
|
||
appearance: none; -webkit-appearance: none;
|
||
width: 12px; height: 12px; border: 1.5px solid #000;
|
||
background: #fff; cursor: pointer; flex-shrink: 0;
|
||
border-radius: 0; display: inline-grid; place-content: center;
|
||
}
|
||
.showschecks input[type="checkbox"]:checked { background: #000; }
|
||
.showschecks input[type="checkbox"]:checked::after {
|
||
content: ""; width: 6px; height: 6px; background: #fff;
|
||
}
|
||
.showschecks .ct { color: #999; font-size: 10px; margin-left: auto; }
|
||
|
||
/* 扁平复选框组(COLLECTION / SEASON / YEAR):不按字母分组,更紧凑 */
|
||
.showschecks--flat li { padding: 4px 0; }
|
||
.showschecks--flat li label {
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
cursor: pointer; width: 100%;
|
||
}
|
||
.showschecks--flat li:hover { background: #f6f6f6; }
|
||
.showschecks--flat li input[type="checkbox"] {
|
||
width: 13px; height: 13px;
|
||
}
|
||
|
||
.showsmore {
|
||
display: inline-block; margin-top: 10px;
|
||
font-family: var(--font-mono, monospace); font-size: 10.5px;
|
||
text-transform: uppercase; letter-spacing: .16em; color: #555;
|
||
cursor: pointer;
|
||
}
|
||
.showsmore:hover { color: #000; }
|
||
|
||
.showsclear {
|
||
margin-top: 18px; width: 100%;
|
||
border: 1.5px solid #000; background: #fff; color: #000;
|
||
padding: 11px 12px; font-family: var(--font-mono, monospace);
|
||
font-size: 11px; text-transform: uppercase; letter-spacing: .22em;
|
||
font-weight: 700; cursor: pointer; border-radius: 0;
|
||
transition: all .15s;
|
||
}
|
||
.showsclear:hover { background: #000; color: #fff; }
|
||
|
||
/* ---------- MAIN GRID ---------- */
|
||
.showsmain { flex: 1; min-width: 0; }
|
||
.showsgrid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 22px;
|
||
position: relative;
|
||
}
|
||
/* 切换筛选时的顶部加载进度条 */
|
||
.showsgrid.is-loading::before {
|
||
content: ""; position: absolute; top: -4px; left: 0; height: 2px; width: 38%;
|
||
background: #000; z-index: 5; animation: hbxProg 1s ease-in-out infinite;
|
||
}
|
||
@keyframes hbxProg {
|
||
0% { left: -38%; } 100% { left: 100%; }
|
||
}
|
||
@media (max-width: 560px) { .showsgrid { grid-template-columns: repeat(1, 1fr); } }
|
||
|
||
.showscard {
|
||
position: relative; display: flex; gap: 14px;
|
||
text-decoration: none; color: inherit;
|
||
background: #fff;
|
||
/* border: 1px solid #e6e6e6; */
|
||
padding: 12px;
|
||
transition: border-color .2s;
|
||
}
|
||
/* .showscard:hover { border-color: #000; } */
|
||
.showscard:hover .showscard-img img { transform: scale(1.04); }
|
||
|
||
/* 左:主图(窄,占 ~30%) */
|
||
.showscard-left {
|
||
flex: 0 0 30%;
|
||
min-width: 110px;
|
||
}
|
||
.showscard-img {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 3 / 4;
|
||
background: #f1f1f1;
|
||
overflow: hidden;
|
||
border-radius: 0;
|
||
}
|
||
.showscard-img img {
|
||
width: 100%; height: 100%; object-fit: cover;
|
||
transition: transform .6s cubic-bezier(.2,.8,.2,1), opacity .45s ease;
|
||
opacity: 0; /* 固定灰底占位:图片未加载时显示容器灰底,加载完成后淡入 */
|
||
}
|
||
.showscard-img img.is-loaded { opacity: 1; }
|
||
|
||
/* 图片加载中的小 spinner:灰底之上居中显示,加载完成/失败淡出 */
|
||
.showsspin {
|
||
position: absolute; inset: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
color: #bdbdbd; opacity: 1;
|
||
transition: opacity .3s ease; pointer-events: none; z-index: 2;
|
||
}
|
||
.showscard-img.is-loaded .showsspin,
|
||
.showscard-thumb.is-loaded .showsspin { opacity: 0; }
|
||
.showsspin svg { width: 22px; height: 22px; display: block; }
|
||
.showscard-thumb .showsspin svg { width: 15px; height: 15px; }
|
||
.showsspin .ring { transform-origin: 12px 12px; animation: hbxSpin .8s linear infinite; }
|
||
@keyframes hbxSpin { to { transform: rotate(360deg); } }
|
||
|
||
.showsbadge {
|
||
position: absolute; top: 6px; right: 6px;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
|
||
font-weight: 700; color: #fff; background: #000; padding: 3px 6px;
|
||
}
|
||
.showsflag-featured {
|
||
position: absolute; top: 6px; left: 6px;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
|
||
font-weight: 700; color: #000; background: #fff; padding: 3px 6px;
|
||
border: 1px solid #000;
|
||
}
|
||
|
||
/* 右列:上信息 + 下缩略图(缩略图位于主图右侧) */
|
||
.showscard-right {
|
||
flex: 1; min-width: 0;
|
||
display: flex; flex-direction: column;justify-content: space-between;
|
||
}
|
||
.showscard-info {
|
||
flex: 0 0 auto; min-width: 0;
|
||
display: flex; flex-direction: column;
|
||
padding: 2px 2px 0;
|
||
}
|
||
.showscard-title {
|
||
font-family: var(--font-serif, serif);
|
||
font-size: 15.5px; font-weight: 700; line-height: 1.32;
|
||
margin: 0 0 10px; color: #111;
|
||
letter-spacing: -.01em;
|
||
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||
}
|
||
.showscard-meta {
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 9.5px; color: #888;
|
||
margin: 0 0 14px; line-height: 1.55;
|
||
letter-spacing: .04em; text-transform: uppercase;
|
||
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||
}
|
||
.showscard-meta-foot {
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
}
|
||
.showscard-meta-line {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
|
||
color: #444;
|
||
}
|
||
.showscard-meta-line svg {
|
||
flex-shrink: 0; color: #888;
|
||
}
|
||
|
||
/* 右列底部:5 列缩略图条、最后一张叠加 N+ 角标(位于主图右侧,与样图一致) */
|
||
.showscard-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 4px;
|
||
margin-top: 10px;
|
||
}
|
||
.showscard-thumb {
|
||
position: relative;
|
||
aspect-ratio: 3 / 4;
|
||
background: #f1f1f1;
|
||
overflow: hidden;
|
||
}
|
||
.showscard-thumb img {
|
||
width: 100%; height: 100%; object-fit: cover;
|
||
opacity: 0;
|
||
transition: opacity .45s ease;
|
||
}
|
||
.showscard-thumb img.is-loaded { opacity: 1; }
|
||
.showscard-thumb.placeholder {
|
||
background: #f6f6f6;
|
||
}
|
||
.showscard-thumb-more {
|
||
position: absolute; inset: 0;
|
||
background: rgba(0, 0, 0, 0.55);
|
||
color: #fff;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 11px; font-weight: 700;
|
||
letter-spacing: .04em;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.showscard { flex-direction: column; gap: 10px; }
|
||
.showscard-left { flex: none; width: 100%; }
|
||
.showscard-right { width: 100%; }
|
||
}
|
||
|
||
/* skeleton */
|
||
.showscard--skel .showscard-img,
|
||
.showscard--skel .showscard-thumb,
|
||
.showscard--skel .showsline {
|
||
background: #efefef;
|
||
animation: hbxShim 1.6s ease infinite;
|
||
}
|
||
.showscard--skel .showscard-img { background: #efefef; }
|
||
.showscard--skel .showsline {
|
||
height: 10px; margin: 8px 0 0;
|
||
}
|
||
.showscard--skel .showsline.short { width: 60%; }
|
||
@keyframes hbxShim {
|
||
0% { opacity: .85; }
|
||
50% { opacity: .55; }
|
||
100% { opacity: .85; }
|
||
}
|
||
|
||
/* 加载更多 */
|
||
.showsloadmore-wrap { display: flex; justify-content: center; margin-top: 38px; }
|
||
.showsloadmore {
|
||
border: 1.5px solid #000; background: #fff; color: #000;
|
||
padding: 13px 36px;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 11px; text-transform: uppercase; letter-spacing: .22em; font-weight: 700;
|
||
cursor: pointer; border-radius: 0; transition: all .2s;
|
||
}
|
||
.showsloadmore:hover { background: #000; color: #fff; }
|
||
.showsloadmore[disabled] { opacity: .4; cursor: default; }
|
||
|
||
.showsempty {
|
||
grid-column: 1 / -1;
|
||
padding: 80px 20px; text-align: center;
|
||
font-family: var(--font-mono, monospace);
|
||
font-size: 12px; letter-spacing: .22em; text-transform: uppercase;
|
||
color: #999;
|
||
border: 1px dashed #ccc;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
import { getArticles, toAbs, type ArticleItem, type BrandEntry } from "../lib/api"
|
||
|
||
type Item = ArticleItem
|
||
|
||
const root = document.querySelector<HTMLElement>(".showsroot")
|
||
const grid = document.getElementById("showsgrid") as HTMLElement | null
|
||
const totalEl = document.getElementById("showstotal")
|
||
const brandCountEl = document.getElementById("brand-count")
|
||
const brandListEl = document.getElementById("brand-list")
|
||
const brandSearchEl = document.getElementById("brand-search") as HTMLInputElement | null
|
||
const brandShowMore = document.getElementById("brand-showmore") as HTMLElement | null
|
||
const pillsEl = document.getElementById("showspills")
|
||
const sortEl = document.getElementById("showssort") as HTMLSelectElement | null
|
||
const loadBtn = document.getElementById("showsloadmore") as HTMLButtonElement | null
|
||
const brandnEl = document.getElementById("showsbrandn")
|
||
|
||
// SSR 期注入的品牌清单(包含 article_count)
|
||
const brandsData: BrandEntry[] = (() => {
|
||
const node = document.getElementById("showsbrands-data")
|
||
if (!node?.textContent) return []
|
||
try { return JSON.parse(node.textContent) } catch { return [] }
|
||
})()
|
||
if (brandnEl) brandnEl.textContent = String(brandsData.length || 198)
|
||
|
||
// 筛选状态(COLLECTION / SEASON / YEAR 用 Set 实现多选)
|
||
const state = {
|
||
brand_ids: new Set<number>(),
|
||
ct: new Set<string>(),
|
||
season: new Set<string>(),
|
||
year: new Set<string>(),
|
||
sort: "newest",
|
||
page: 1,
|
||
size: 24,
|
||
total: 0,
|
||
brandsExpanded: false,
|
||
keyword: "",
|
||
}
|
||
|
||
// ============ 侧边栏:DESIGNERS 按字母分组 ============
|
||
const grouped = (() => {
|
||
const map = new Map<string, BrandEntry[]>()
|
||
for (const b of brandsData) {
|
||
const letter = (b.name[0] || "?").toUpperCase()
|
||
if (!map.has(letter)) map.set(letter, [])
|
||
map.get(letter)!.push(b)
|
||
}
|
||
return [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
||
})()
|
||
|
||
const renderBrands = () => {
|
||
if (!brandListEl) return
|
||
brandListEl.innerHTML = ""
|
||
const frag = document.createDocumentFragment()
|
||
const kw = state.keyword
|
||
let shown = 0
|
||
const MAX_INITIAL = 60
|
||
let truncated = false
|
||
|
||
const appendBrand = (b: BrandEntry) => {
|
||
const li = document.createElement("li")
|
||
const cb = document.createElement("input")
|
||
cb.type = "checkbox"; cb.value = String(b.id)
|
||
cb.checked = state.brand_ids.has(b.id)
|
||
cb.addEventListener("change", () => {
|
||
cb.checked ? state.brand_ids.add(b.id) : state.brand_ids.delete(b.id)
|
||
syncPills()
|
||
loadPage(1)
|
||
})
|
||
const name = document.createElement("span"); name.textContent = b.name
|
||
const ct = document.createElement("span"); ct.className = "ct"; ct.textContent = b.article_count ? String(b.article_count) : ""
|
||
li.appendChild(cb); li.appendChild(name); li.appendChild(ct)
|
||
frag.appendChild(li)
|
||
shown++
|
||
}
|
||
|
||
if (kw) {
|
||
// 搜索模式:跨字母平铺所有匹配项(忽略 60 上限 / 字母分组)
|
||
for (const [, list] of grouped) {
|
||
for (const b of list) {
|
||
if (b.name.toLowerCase().includes(kw)) appendBrand(b)
|
||
}
|
||
}
|
||
} else {
|
||
for (const [letter, list] of grouped) {
|
||
if (!state.brandsExpanded && shown >= MAX_INITIAL) { truncated = true; break }
|
||
const h = document.createElement("li")
|
||
h.dataset.letter = letter
|
||
h.textContent = letter
|
||
frag.appendChild(h)
|
||
for (const b of list) {
|
||
if (!state.brandsExpanded && shown >= MAX_INITIAL) { truncated = true; break }
|
||
appendBrand(b)
|
||
}
|
||
}
|
||
}
|
||
brandListEl.appendChild(frag)
|
||
if (brandShowMore) brandShowMore.style.display = (truncated && !kw) ? "" : "none"
|
||
if (brandCountEl) brandCountEl.textContent = String(state.brand_ids.size)
|
||
}
|
||
|
||
if (brandShowMore) {
|
||
brandShowMore.addEventListener("click", () => {
|
||
state.brandsExpanded = !state.brandsExpanded
|
||
brandShowMore.textContent = state.brandsExpanded ? "Show fewer ▴" : `Show all ${brandsData.length} designers ▾`
|
||
renderBrands()
|
||
})
|
||
brandShowMore.textContent = `Show all ${brandsData.length} designers ▾`
|
||
}
|
||
|
||
if (brandSearchEl) {
|
||
let timer: number | undefined
|
||
brandSearchEl.addEventListener("input", () => {
|
||
window.clearTimeout(timer)
|
||
timer = window.setTimeout(() => {
|
||
state.keyword = brandSearchEl.value.trim().toLowerCase()
|
||
renderBrands()
|
||
}, 120)
|
||
})
|
||
}
|
||
|
||
// ============ 侧边栏整体折叠(localStorage 记住状态) ============
|
||
// 初始折叠状态已由页面顶部 <script is:inline> 同步到 <html> 根类,
|
||
// 在首屏绘制前生效,避免「先展开后折叠」的宽度闪动。此处只绑定切换。
|
||
const asideToggle = document.getElementById("showstoggle")
|
||
const lsKey = "showsaside-collapsed"
|
||
if (asideToggle) {
|
||
asideToggle.addEventListener("click", () => {
|
||
const collapsed = document.documentElement.classList.toggle("showsaside-collapsed")
|
||
try { localStorage.setItem(lsKey, collapsed ? "1" : "") } catch (e) {}
|
||
})
|
||
}
|
||
|
||
// ============ 每个筛选分组可上下折叠(accordion,localStorage 记忆) ============
|
||
document.querySelectorAll<HTMLElement>(".showsg-h[data-group]").forEach((h) => {
|
||
const key = "showsg-" + (h.dataset.group || "")
|
||
const g = h.closest(".showsg")
|
||
try { if (localStorage.getItem(key) === "1" && g) { g.classList.add("is-collapsed"); h.setAttribute("aria-expanded", "false") } } catch (e) {}
|
||
const toggle = () => {
|
||
if (!g) return
|
||
const collapsed = g.classList.toggle("is-collapsed")
|
||
h.setAttribute("aria-expanded", collapsed ? "false" : "true")
|
||
try { localStorage.setItem(key, collapsed ? "1" : "") } catch (e) {}
|
||
}
|
||
h.addEventListener("click", toggle)
|
||
h.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle() }
|
||
})
|
||
})
|
||
|
||
// ============ 侧边栏多选复选框组:COLLECTION / SEASON / YEAR ============
|
||
// 用一组扁平复选框代替旧 chip。每个 input.change 写入对应的 Set,立即刷新数据。
|
||
const wireCheckboxGroup = (
|
||
set: Set<string>,
|
||
containerId: string,
|
||
onChange: () => void
|
||
) => {
|
||
const container = document.getElementById(containerId)
|
||
if (!container) return
|
||
container.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((cb) => {
|
||
cb.addEventListener("change", () => {
|
||
cb.checked ? set.add(cb.value) : set.delete(cb.value)
|
||
onChange()
|
||
})
|
||
})
|
||
}
|
||
wireCheckboxGroup(state.ct, "ct-checks", refreshAfterFilter)
|
||
wireCheckboxGroup(state.season, "season-checks", refreshAfterFilter)
|
||
wireCheckboxGroup(state.year, "year-checks", refreshAfterFilter)
|
||
|
||
function refreshAfterFilter() {
|
||
syncPills()
|
||
loadPage(1)
|
||
}
|
||
|
||
// ============ 排序 ============
|
||
if (sortEl) {
|
||
sortEl.addEventListener("change", () => {
|
||
state.sort = sortEl.value
|
||
syncPills()
|
||
loadPage(1)
|
||
})
|
||
}
|
||
|
||
// ============ Pills:已激活筛选清单 ============
|
||
const syncPills = () => {
|
||
if (!pillsEl) return
|
||
const list: { label: string; clear: () => void }[] = []
|
||
if (state.brand_ids.size) {
|
||
const idArr = [...state.brand_ids]
|
||
const labels = idArr.map((id) => brandsData.find((b) => b.id === id)?.name ?? `#${id}`).slice(0, 4)
|
||
const more = idArr.length > 4 ? ` +${idArr.length - 4}` : ""
|
||
list.push({
|
||
label: "DESIGNER · " + labels.join(", ") + more,
|
||
clear: () => { state.brand_ids.clear(); renderBrands(); }
|
||
})
|
||
}
|
||
if (state.ct.size) list.push({
|
||
label: "COLLECTION · " + [...state.ct].join("+").toUpperCase(),
|
||
clear: () => { state.ct.clear(); syncCheckboxGroup("ct-checks"); }
|
||
})
|
||
if (state.season.size) list.push({
|
||
label: "SEASON · " + [...state.season].join("+").toUpperCase(),
|
||
clear: () => { state.season.clear(); syncCheckboxGroup("season-checks"); }
|
||
})
|
||
if (state.year.size) list.push({
|
||
label: "YEAR · " + [...state.year].sort().join("+"),
|
||
clear: () => { state.year.clear(); syncCheckboxGroup("year-checks"); }
|
||
})
|
||
if (state.sort !== "newest") list.push({
|
||
label: "SORT · " + state.sort.replace("_", " ").toUpperCase(),
|
||
clear: () => { state.sort = "newest"; if (sortEl) sortEl.value = "newest" }
|
||
})
|
||
|
||
if (!list.length) { pillsEl.hidden = true; pillsEl.innerHTML = ""; return }
|
||
pillsEl.hidden = false
|
||
pillsEl.innerHTML = list.map((p, i) => `<span class="showspill" data-i="${i}">${p.label}<button class="x" type="button">×</button></span>`).join("")
|
||
pillsEl.querySelectorAll<HTMLElement>(".showspill").forEach((pill) => {
|
||
pill.querySelector(".x")?.addEventListener("click", () => {
|
||
const i = Number(pill.dataset.i)
|
||
list[i]?.clear()
|
||
syncPills()
|
||
loadPage(1)
|
||
})
|
||
})
|
||
if (brandCountEl) brandCountEl.textContent = String(state.brand_ids.size)
|
||
}
|
||
const syncCheckboxGroup = (containerId: string) => {
|
||
document.getElementById(containerId)?.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((cb) => { cb.checked = false })
|
||
}
|
||
|
||
// Clear all
|
||
document.getElementById("showsclear")?.addEventListener("click", () => {
|
||
state.brand_ids.clear()
|
||
state.ct.clear(); state.season.clear(); state.year.clear()
|
||
state.sort = "newest"
|
||
state.keyword = ""
|
||
if (brandSearchEl) brandSearchEl.value = ""
|
||
syncCheckboxGroup("ct-checks")
|
||
syncCheckboxGroup("season-checks")
|
||
syncCheckboxGroup("year-checks")
|
||
if (sortEl) sortEl.value = "newest"
|
||
renderBrands()
|
||
syncPills()
|
||
loadPage(1)
|
||
})
|
||
|
||
// ============ 卡片渲染(左大图 + 右上文字 + 下 5 张缩略图条) ============
|
||
const ICON_USER = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><circle cx="6" cy="4" r="2" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 10c0-2 1.8-3 4-3s4 1 4 3" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
|
||
const ICON_DATE = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><rect x="2" y="3" width="8" height="7" stroke="currentColor" fill="none" stroke-width="1"/><path d="M2 6h8M4 2v2M8 2v2" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
|
||
const ICON_CAM = `<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true"><rect x="1.5" y="3.5" width="9" height="6" stroke="currentColor" fill="none" stroke-width="1"/><circle cx="6" cy="6.5" r="1.8" stroke="currentColor" fill="none" stroke-width="1"/><path d="M4 3.5l1-1.2h2l1 1.2" stroke="currentColor" fill="none" stroke-width="1" stroke-linecap="round"/></svg>`
|
||
// 加载中 spinner:细线旋转环 + 中心静态相机小图标(灰调,HBX 编辑风)
|
||
const SPIN_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<g class="ring" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="40 22">
|
||
<circle cx="12" cy="12" r="9"></circle>
|
||
</g>
|
||
<g opacity="0.55" fill="none" stroke="currentColor" stroke-width="1">
|
||
<rect x="8" y="9.6" width="8" height="5.4"></rect>
|
||
<circle cx="12" cy="12.3" r="1.6"></circle>
|
||
</g>
|
||
</svg>`
|
||
const fmtDate = (ts: number) => {
|
||
if (!ts) return ""
|
||
const d = new Date(ts * 1000)
|
||
const mm = String(d.getMonth() + 1).padStart(2, "0")
|
||
const dd = String(d.getDate()).padStart(2, "0")
|
||
return `${d.getFullYear()}-${mm}-${dd}`
|
||
}
|
||
const ctLabel = (ct?: string) => {
|
||
const map: Record<string, string> = {
|
||
rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture",
|
||
resort: "Resort", pre_fall: "Pre-Fall",
|
||
}
|
||
return ct && map[ct] ? map[ct] : (ct || "")
|
||
}
|
||
|
||
const cardHtml = (it: Item) => {
|
||
const code = it.season_code || String(it.year || "")
|
||
const gallery = it.gallery || []
|
||
const totalCount = it.image_count || 0
|
||
// 角标:总数 > 30 一律 30+,否则展示 N+(即使 N<=5 也展示,保持与样图 5 槽位+末张叠加的视觉一致)
|
||
const moreLabel = totalCount > 30 ? "30+" : `${totalCount || 0}+`
|
||
|
||
// 永远渲染 5 个缩略图槽位(HBX 视觉一致性,不够补 .placeholder)
|
||
const SLOTS = 5
|
||
const slots: string[] = []
|
||
for (let i = 0; i < SLOTS; i++) {
|
||
const src = gallery[i]
|
||
const isLast = i === SLOTS - 1
|
||
if (src) {
|
||
slots.push(`<div class="showscard-thumb"><span class="showsspin" aria-hidden="true">${SPIN_SVG}</span><img src="${toAbs(src)}" alt="" loading="lazy" />${isLast && totalCount > 0 ? `<span class="showscard-thumb-more">${moreLabel}</span>` : ""}</div>`)
|
||
} else {
|
||
slots.push(`<div class="showscard-thumb placeholder">${isLast && totalCount > 0 ? `<span class="showscard-thumb-more">${moreLabel}</span>` : ""}</div>`)
|
||
}
|
||
}
|
||
|
||
const isNew = (Date.now() / 1000 - Number(it.published_at || 0) < 60 * 60 * 24 * 14)
|
||
const isFeatured = it.image_count > 70
|
||
|
||
return `
|
||
<a class="showscard" href="/article?id=${it.id}" aria-label="${it.brand_name} ${it.title}">
|
||
<div class="showscard-left">
|
||
<div class="showscard-img">
|
||
${isFeatured ? '<span class="showsflag-featured">Featured</span>' : ""}
|
||
<span class="showsspin" aria-hidden="true">${SPIN_SVG}</span>
|
||
<img src="${toAbs(it.cover)}" alt="${it.title}" loading="lazy" />
|
||
${isNew ? '<span class="showsbadge">New Added</span>' : ""}
|
||
</div>
|
||
</div>
|
||
<div class="showscard-right">
|
||
<div class="showscard-info">
|
||
<h3 class="showscard-title">${it.title}</h3>
|
||
<p class="showscard-meta">${code} · ${ctLabel(it.collection_type)} · ${it.brand_name}</p>
|
||
<div class="showscard-meta-foot">
|
||
<span class="showscard-meta-line">${ICON_USER}${it.brand_name}</span>
|
||
<span class="showscard-meta-line">${ICON_DATE}${fmtDate(it.published_at)}</span>
|
||
</div>
|
||
</div>
|
||
<div class="showscard-strip">${slots.join("")}</div>
|
||
</div>
|
||
</a>`
|
||
}
|
||
|
||
// 图片淡入 + spinner:网络图加载完成前显示容器灰底占位与居中 spinner,
|
||
// 加载完淡入图片并淡出 spinner,消除硬切换跳变
|
||
const revealImages = (root: ParentNode = document) => {
|
||
const imgs = root.querySelectorAll<HTMLImageElement>(".showscard-img img, .showscard-thumb img")
|
||
imgs.forEach((img) => {
|
||
if (img.classList.contains("is-loaded")) return
|
||
const wrap = img.closest(".showscard-img, .showscard-thumb")
|
||
const done = () => { img.classList.add("is-loaded"); wrap?.classList.add("is-loaded") }
|
||
if (img.complete && img.naturalWidth > 0) {
|
||
done()
|
||
} else {
|
||
img.addEventListener("load", done, { once: true })
|
||
// 加载失败:隐藏 img,保留容器灰底占位,并淡出 spinner
|
||
img.addEventListener("error", () => { img.style.display = "none"; wrap?.classList.add("is-loaded") }, { once: true })
|
||
}
|
||
})
|
||
}
|
||
|
||
const renderItems = (items: Item[], append: boolean) => {
|
||
if (!grid) return
|
||
if (!append) {
|
||
grid.innerHTML = ""
|
||
}
|
||
if (!items.length && !append) {
|
||
grid.innerHTML = `<div class="showsempty">No shows match the current filters · 暂无符合条件的秀场</div>`
|
||
return
|
||
}
|
||
grid.insertAdjacentHTML("beforeend", items.map(cardHtml).join(""))
|
||
revealImages(grid)
|
||
}
|
||
|
||
// ============ 拉数(统一走 src/lib/api 的 getArticles) ============
|
||
|
||
// 筛选/翻页时的骨架占位
|
||
const skeletonHtml = (n: number) => Array.from({ length: n }, () => `
|
||
<div class="showscard showscard--skel">
|
||
<div class="showscard-left">
|
||
<div class="showscard-img"></div>
|
||
</div>
|
||
<div class="showscard-right">
|
||
<div class="showscard-info">
|
||
<div class="showsline" style="height:40px;margin:0"></div>
|
||
<div class="showsline short" style="height:14px;margin:12px 0 0"></div>
|
||
<div class="showsline" style="height:34px;margin:14px 0 0"></div>
|
||
</div>
|
||
<div class="showscard-strip">
|
||
<div class="showscard-thumb"></div><div class="showscard-thumb"></div><div class="showscard-thumb"></div><div class="showscard-thumb"></div><div class="showscard-thumb"></div>
|
||
</div>
|
||
</div>
|
||
</div>`).join("")
|
||
|
||
// 请求令牌:避免快速切换筛选时旧响应覆盖新结果
|
||
let reqToken = 0
|
||
|
||
const loadPage = async (page: number, append = false) => {
|
||
const myToken = ++reqToken
|
||
try {
|
||
if (loadBtn && append) { loadBtn.textContent = "Loading…"; loadBtn.disabled = true }
|
||
// 切换筛选(非追加)时立即进入 loading 态:显示骨架 + 顶部进度条
|
||
if (!append && grid) {
|
||
grid.innerHTML = skeletonHtml(12)
|
||
grid.classList.add("is-loading")
|
||
}
|
||
const json = await getArticles({
|
||
page,
|
||
size: state.size,
|
||
sort: state.sort,
|
||
brand_ids: [...state.brand_ids],
|
||
collection_type: [...state.ct],
|
||
season: [...state.season],
|
||
year: [...state.year],
|
||
with_images: 6,
|
||
})
|
||
if (myToken !== reqToken) return // 已有更新的请求,丢弃本次结果
|
||
const rows: Item[] = json.data
|
||
state.total = json.total
|
||
state.page = page
|
||
grid?.classList.remove("is-loading")
|
||
renderItems(rows, append)
|
||
if (totalEl) totalEl.textContent = `${state.total || (grid?.querySelectorAll(".showscard").length ?? rows.length)} Shows`
|
||
const last = json.last_page
|
||
if (page >= last || rows.length < state.size) {
|
||
if (loadBtn) { loadBtn.textContent = "— End of Archive —"; loadBtn.disabled = true }
|
||
} else if (loadBtn) {
|
||
loadBtn.textContent = "Load More ↓"
|
||
loadBtn.disabled = false
|
||
}
|
||
} catch {
|
||
if (myToken !== reqToken) return
|
||
grid?.classList.remove("is-loading")
|
||
renderItems([], false)
|
||
if (totalEl) totalEl.textContent = "—"
|
||
if (loadBtn) { loadBtn.textContent = "Retry"; loadBtn.disabled = false }
|
||
}
|
||
}
|
||
|
||
if (loadBtn) loadBtn.addEventListener("click", () => loadPage(state.page + 1, true))
|
||
|
||
// 初次渲染
|
||
renderBrands()
|
||
revealImages(document) // 首屏 SSR 已渲染的卡片图片同样淡入
|
||
loadPage(1, false)
|
||
</script>
|
||
</content>
|