36
src/components/LanguageDetector.astro
Normal file
36
src/components/LanguageDetector.astro
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import { pathHasLocale, getRelativeLocaleUrl } from 'astro:i18n';
|
||||||
|
|
||||||
|
// 使用 pathHasLocale 安全判断当前路径是否已包含任何语言前缀(返回 boolean)
|
||||||
|
const hasLocale = pathHasLocale(Astro.url.pathname);
|
||||||
|
|
||||||
|
// 自动生成中文根路径
|
||||||
|
const cnRootUrl = getRelativeLocaleUrl('cn', '');
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ hasLocale, cnRootUrl }}>
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
// 当前路径已带有语言前缀(如 /cn/ 或 /en/),直接跳过
|
||||||
|
if (hasLocale) return;
|
||||||
|
|
||||||
|
// 避免重复跳转
|
||||||
|
if (sessionStorage.getItem('lang-redirect')) return;
|
||||||
|
|
||||||
|
// 判定浏览器偏好语言
|
||||||
|
const langs = navigator.languages || [navigator.language || 'en'];
|
||||||
|
const isZh = langs.some((l) => l.toLowerCase().startsWith('zh'));
|
||||||
|
|
||||||
|
if (!isZh) return;
|
||||||
|
|
||||||
|
sessionStorage.setItem('lang-redirect', '1');
|
||||||
|
|
||||||
|
// 拼接真实跳转路径
|
||||||
|
const { pathname, search, hash } = location;
|
||||||
|
const cleanPath = pathname === '/' ? '' : pathname;
|
||||||
|
const targetUrl = `${cnRootUrl}${cleanPath.replace(/^\//, '')}${search}${hash}`;
|
||||||
|
|
||||||
|
location.replace(targetUrl);
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -1,53 +1,45 @@
|
|||||||
---
|
<!-- Alpine.js 防止闪烁样式 -->
|
||||||
|
<style>
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
/* 隐藏缩略图侧边栏滚动条 */
|
||||||
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||||
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||||
|
</style>
|
||||||
|
|
||||||
// 示例数据:字段严格对齐后端 BrandRunway 模型 + 图片集接口
|
<div x-data="articleDetail()" class="min-h-screen bg-white text-black font-sans selection:bg-black selection:text-white">
|
||||||
const article = {
|
|
||||||
id: 1,
|
|
||||||
title: "Maison Lumen — Paper Light",
|
|
||||||
description:
|
|
||||||
"巴黎,2026 秋冬。Maison Lumen 以“Paper Light(纸光)”为题,将薄纱、宣纸与压褶工艺叠合成一种近乎透明的体积感。\n\n系列延续了工作室对“轻”的执念:肩线被刻意放低,廓形在行走中随风摆动,像被光穿透的纸。色彩仅保留骨白、炭灰与一抹雾蓝。\n\n这不是关于廓形的宣言,而是关于 material 的克制——用最少的手段,制造最大的呼吸感。",
|
|
||||||
created_at: 1711948800,
|
|
||||||
updated_at: 1711948800,
|
|
||||||
year: 2026,
|
|
||||||
source_url: "https://example.com/runway/maison-lumen-paper-light",
|
|
||||||
brand_id: 12,
|
|
||||||
brand_name: "Maison Lumen",
|
|
||||||
images: [
|
|
||||||
{ id: 1, image: "https://picsum.photos/seed/art1/1200/1500", name: "Look 01", sort_order: 1 },
|
|
||||||
{ id: 2, image: "https://picsum.photos/seed/art2/1200/1500", name: "Look 02", sort_order: 2 },
|
|
||||||
{ id: 3, image: "https://picsum.photos/seed/art3/1200/1500", name: "Look 03", sort_order: 3 },
|
|
||||||
{ id: 4, image: "https://picsum.photos/seed/art4/1200/1500", name: "Look 04", sort_order: 4 },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const fmt = (ts: number) => {
|
<!-- 1. 加载中状态 (Loading) -->
|
||||||
const d = new Date(ts * 1000)
|
<div x-show="loading" class="flex flex-col items-center justify-center min-h-[60vh] text-[12px] font-mono uppercase tracking-[0.2em] text-[#888]">
|
||||||
const y = d.getFullYear()
|
<div class="w-7 h-7 border-2 border-black border-t-transparent rounded-full animate-spin mb-4"></div>
|
||||||
const m = String(d.getMonth() + 1).padStart(2, "0")
|
<span>Loading Article...</span>
|
||||||
const day = String(d.getDate()).padStart(2, "0")
|
</div>
|
||||||
return `${y}.${m}.${day}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = article.description.split("\n\n")
|
<!-- 2. 错误提示状态 (Error) -->
|
||||||
---
|
<div x-show="error && !loading" x-cloak class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
|
||||||
|
<p class="font-mono text-sm text-red-500 uppercase tracking-widest mb-4" x-text="error"></p>
|
||||||
|
<a href="/" class="text-[12px] font-mono uppercase tracking-widest border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors">
|
||||||
|
← Back to Index
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<article class="mx-auto max-w-[1100px] px-5 md:px-10 py-12 md:py-16">
|
<!-- 3. 文章主内容 -->
|
||||||
|
<article x-show="!loading && article" x-cloak class="mx-auto max-w-[1100px] px-5 md:px-10 py-12 md:py-16">
|
||||||
<!-- 面包屑 / 品牌 -->
|
<!-- 面包屑 / 品牌 -->
|
||||||
<div class="flex items-center justify-between text-[11px] font-mono uppercase tracking-[0.25em] text-black border-b border-black pb-4">
|
<div class="flex items-center justify-between text-[11px] font-mono uppercase tracking-[0.25em] text-black border-b border-black pb-4">
|
||||||
<a href="/" class="hover:opacity-60 transition-opacity">← Index</a>
|
<a href="/" class="hover:opacity-60 transition-opacity">← Index</a>
|
||||||
<span id="art-brand">Runway · {article.brand_name}</span>
|
<span>Runway · <span x-text="article?.brand_name || 'Brand'"></span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标题 -->
|
<!-- 标题 -->
|
||||||
<h1 class="font-serif font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
|
<h1 class="font-serif font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8" x-text="article?.title"></h1>
|
||||||
{article.title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- 元信息 -->
|
<!-- 元信息 -->
|
||||||
<div class="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-[12px] font-mono uppercase tracking-widest text-[#666]">
|
<div class="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-[12px] font-mono uppercase tracking-widest text-[#666]">
|
||||||
<span id="art-date">{fmt(article.created_at)}</span>
|
<span x-text="formatDate(article?.published_at || article?.created_at)"></span>
|
||||||
<span id="art-year" class="text-black">{article.year}</span>
|
<span class="text-black" x-text="article?.year"></span>
|
||||||
<a href={article.source_url} target="_blank" rel="noopener" class="hover:text-black underline underline-offset-4">Source ↗</a>
|
<template x-if="article?.source_url">
|
||||||
|
<a :href="article.source_url" target="_blank" rel="noopener" class="hover:text-black underline underline-offset-4">Source ↗</a>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 正文 -->
|
<!-- 正文 -->
|
||||||
@ -55,28 +47,36 @@ const body = article.description.split("\n\n")
|
|||||||
<div class="md:col-span-2 text-[11px] font-mono uppercase tracking-widest text-[#999] pt-1">
|
<div class="md:col-span-2 text-[11px] font-mono uppercase tracking-widest text-[#999] pt-1">
|
||||||
Report
|
Report
|
||||||
</div>
|
</div>
|
||||||
<div id="art-body" class="md:col-span-10 max-w-[68ch] space-y-5 text-[16px] leading-[1.9] text-[#222]">
|
<div class="md:col-span-10 max-w-[68ch] space-y-5 text-[16px] leading-[1.9] text-[#222]">
|
||||||
{body.map((p) => <p>{p}</p>)}
|
<template x-for="(paragraph, idx) in bodyParagraphs" :key="idx">
|
||||||
|
<p x-text="paragraph"></p>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片集 -->
|
<!-- 图片集 -->
|
||||||
<section class="mt-16">
|
<section class="mt-16" x-show="article?.images && article.images.length > 0">
|
||||||
<div class="flex items-end justify-between border-b border-[#eee] pb-3 mb-6">
|
<div class="flex items-end justify-between border-b border-[#eee] pb-3 mb-6">
|
||||||
<div class="text-[11px] font-mono uppercase tracking-[0.25em] text-[#999]">
|
<div class="text-[11px] font-mono uppercase tracking-[0.25em] text-[#999]">
|
||||||
Gallery <span id="art-gallery-count" class="ml-2 text-black"></span>
|
Gallery <span class="ml-2 text-black" x-text="`· 共 ${article?.images?.length || 0} 张`"></span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="art-viewall" class="text-[11px] font-mono uppercase tracking-widest border border-black px-3 py-1.5 hover:bg-black hover:text-white transition-colors">⤢ 全屏查看</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openLb(0)"
|
||||||
|
class="text-[11px] font-mono uppercase tracking-widest border border-black px-3 py-1.5 hover:bg-black hover:text-white transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
⤢ 全屏查看
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片网格 -->
|
<!-- 图片网格 -->
|
||||||
<div id="art-gallery" class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{article.images.map((im) => (
|
<template x-for="(im, i) in article?.images" :key="im.id || i">
|
||||||
<figure class="g-item bg-[#f2f2f2] cursor-zoom-in">
|
<figure @click="openLb(i)" class="bg-[#f2f2f2] cursor-zoom-in group">
|
||||||
<img src={im.image} alt={im.name} loading="lazy" class="w-full aspect-[3/4] object-cover" />
|
<img :src="im.imageUrl" :alt="im.name" loading="lazy" class="w-full aspect-[3/4] object-cover group-hover:opacity-90 transition-opacity" />
|
||||||
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2">{im.name}</figcaption>
|
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2" x-text="im.name || ''"></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -85,376 +85,230 @@ const body = article.description.split("\n\n")
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<!-- 灯箱:左侧竖排缩略图条 + 中央大图(点击放大→宽度最大+纵向滚动) -->
|
<!-- 4. 全屏灯箱组件 (Lightbox) -->
|
||||||
<div id="lb" class="lb" aria-hidden="true">
|
<div
|
||||||
<div class="lb-counter" id="lb-counter"></div>
|
x-show="lbOpen"
|
||||||
<button class="lb-btn lb-close" id="lb-close" aria-label="关闭">✕</button>
|
x-cloak
|
||||||
<div class="lb-stage" id="lb-stage">
|
@keydown.window.escape="closeLb()"
|
||||||
<div class="lb-thumbs" id="lb-thumbs"></div>
|
@keydown.window.arrow-right="nextLb()"
|
||||||
<button class="lb-tbtn lb-tup" id="lb-tup" type="button" aria-label="上一张">
|
@keydown.window.arrow-left="prevLb()"
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 14l6-6 6 6"/></svg>
|
class="fixed inset-0 z-[100] bg-white flex flex-col select-none"
|
||||||
|
>
|
||||||
|
<!-- 计数器 -->
|
||||||
|
<div class="absolute top-5 left-1/2 -translate-x-1/2 z-30 font-mono text-[12px] font-semibold tracking-[0.14em] text-[#111] bg-white/75 border border-black/10 px-3 py-1.5 rounded-[6px] backdrop-blur-md" x-text="`${lbIndex + 1} / ${article?.images?.length || 0}`"></div>
|
||||||
|
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<button @click="closeLb()" class="absolute top-[18px] right-[18px] z-30 w-10 h-10 rounded-full border border-black/15 bg-white/75 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black/5 hover:border-black/30 transition-all cursor-pointer" aria-label="关闭">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 主舞台 -->
|
||||||
|
<div class="flex-1 min-h-0 flex items-stretch overflow-hidden relative">
|
||||||
|
<!-- 左侧缩略图纵向列表 -->
|
||||||
|
<div x-ref="lbThumbs" class="w-[76px] sm:w-[118px] flex-none overflow-y-auto overflow-x-hidden relative scroll-smooth p-[32px_8px] sm:p-[38px_18px_38px_20px] border-r border-black/10 bg-gradient-to-b from-black/5 to-transparent no-scrollbar">
|
||||||
|
<template x-for="(im, i) in article?.images" :key="im.id || i">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="showLb(i)"
|
||||||
|
class="relative block w-full aspect-[3/4] mb-2 rounded-[2px] overflow-hidden cursor-pointer transition-all duration-200"
|
||||||
|
:class="lbIndex === i ? 'opacity-100 scale-[1.03] ring-2 ring-[#111]' : 'opacity-40 hover:opacity-85'"
|
||||||
|
>
|
||||||
|
<img :src="im.imageUrl" alt="" class="w-full h-full object-cover block" />
|
||||||
|
<span
|
||||||
|
class="absolute left-1 top-1 z-10 font-mono text-[8px] sm:text-[9px] font-semibold tracking-widest text-white px-1 py-0.5 rounded-[3px] bg-black/55 backdrop-blur-sm"
|
||||||
|
:class="lbIndex === i ? '!bg-[#111]' : ''"
|
||||||
|
x-text="String(i + 1).padStart(2, '0')"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左侧上下翻页辅助按键 -->
|
||||||
|
<button @click.stop="prevLb()" type="button" class="absolute left-3 sm:left-5 top-2 z-20 w-[52px] sm:w-[72px] h-[30px] sm:h-[34px] border border-black/15 bg-white/80 rounded-[6px] backdrop-blur-md flex items-center justify-center text-[#111] hover:bg-black/5 transition-all cursor-pointer">
|
||||||
|
<svg class="w-4 h-4 stroke-current fill-none stroke-2" viewBox="0 0 24 24"><path d="M6 14l6-6 6 6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="lb-tbtn lb-tdown" id="lb-tdown" type="button" aria-label="下一张">
|
<button @click.stop="nextLb()" type="button" class="absolute left-3 sm:left-5 bottom-2 z-20 w-[52px] sm:w-[72px] h-[30px] sm:h-[34px] border border-black/15 bg-white/80 rounded-[6px] backdrop-blur-md flex items-center justify-center text-[#111] hover:bg-black/5 transition-all cursor-pointer">
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 10l6 6 6-6"/></svg>
|
<svg class="w-4 h-4 stroke-current fill-none stroke-2" viewBox="0 0 24 24"><path d="M6 10l6 6 6-6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="lb-btn lb-prev" id="lb-prev" type="button" aria-label="上一张">
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>
|
<!-- 左右主切换按钮 -->
|
||||||
|
<button @click.stop="prevLb()" type="button" class="absolute left-[82px] sm:left-[130px] top-1/2 -translate-y-1/2 z-20 w-10 h-10 rounded-full border border-black/15 bg-white/75 text-[#111] flex items-center justify-center backdrop-blur-md hover:scale-105 hover:border-black/30 transition-all cursor-pointer">
|
||||||
|
<svg class="w-5 h-5 stroke-current fill-none stroke-2" viewBox="0 0 24 24"><path d="M15 6l-6 6 6 6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="lb-btn lb-next" id="lb-next" type="button" aria-label="下一张">
|
<button @click.stop="nextLb()" type="button" class="absolute right-2 sm:right-5 top-1/2 -translate-y-1/2 z-20 w-10 h-10 rounded-full border border-black/15 bg-white/75 text-[#111] flex items-center justify-center backdrop-blur-md hover:scale-105 hover:border-black/30 transition-all cursor-pointer">
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
|
<svg class="w-5 h-5 stroke-current fill-none stroke-2" viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="lb-main" id="lb-main">
|
|
||||||
<img id="lb-img" class="lb-img" alt="" />
|
<!-- 大图显示区域(支持点击切换放大/缩小状态) -->
|
||||||
|
<div
|
||||||
|
x-ref="lbMain"
|
||||||
|
@click.self="closeLb()"
|
||||||
|
@touchstart="handleTouchStart($event)"
|
||||||
|
@touchend="handleTouchEnd($event)"
|
||||||
|
class="flex-1 min-w-0 min-h-0 relative flex justify-center px-4 sm:px-18"
|
||||||
|
:class="lbZoomed ? 'block overflow-y-auto overflow-x-hidden items-start' : 'items-center'"
|
||||||
|
>
|
||||||
|
<template x-if="article?.images && article.images[lbIndex]">
|
||||||
|
<img
|
||||||
|
:src="article.images[lbIndex].imageUrl"
|
||||||
|
:alt="article.images[lbIndex].name || ''"
|
||||||
|
@click="toggleZoom()"
|
||||||
|
class="block select-none transition-opacity duration-200"
|
||||||
|
:class="lbZoomed ? 'w-full max-w-full h-auto max-h-none cursor-zoom-out' : 'max-w-full max-h-full object-contain cursor-zoom-in'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lb-caption" id="lb-caption"></div>
|
|
||||||
<div class="lb-hint" id="lb-hint">点击图片放大 · 上下滚动查看 · Esc 关闭</div>
|
<!-- 图说标题 -->
|
||||||
|
<div class="absolute left-0 right-0 bottom-11 z-20 text-center text-black/60 font-mono text-[11px] uppercase tracking-[0.18em] pointer-events-none px-6" x-text="article?.images[lbIndex]?.name || ''"></div>
|
||||||
|
|
||||||
|
<!-- 提示文本 -->
|
||||||
|
<div x-show="!lbZoomed" class="absolute left-0 right-0 bottom-4 z-20 text-center text-black/45 font-mono text-[11px] tracking-[0.14em] pointer-events-none px-6 transition-opacity duration-200">
|
||||||
|
点击图片放大 · 上下滚动查看 · Esc 关闭
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style is:global>
|
</div>
|
||||||
@reference "tailwindcss";
|
|
||||||
|
|
||||||
/* 灯箱:左侧竖排缩略图条 + 中央大图;点击放大→宽度最大+纵向滚动 */
|
|
||||||
.lb {
|
|
||||||
position: fixed; inset: 0; z-index: 100;
|
|
||||||
@apply bg-white;
|
|
||||||
display: none; flex-direction: column;
|
|
||||||
opacity: 0; transition: opacity .2s ease;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
.lb.is-open { display: flex; opacity: 1; }
|
|
||||||
|
|
||||||
.lb-stage {
|
|
||||||
flex: 1 1 auto; min-height: 0;
|
|
||||||
display: flex; align-items: stretch; justify-content: stretch;
|
|
||||||
overflow: hidden; position: relative;
|
|
||||||
animation: lbIn .2s ease both;
|
|
||||||
}
|
|
||||||
@keyframes lbIn { from { opacity: 0; } to { opacity: 1; } }
|
|
||||||
|
|
||||||
.lb-thumbs {
|
|
||||||
flex: 0 0 118px; overflow-y: auto; overflow-x: hidden; position: relative; scroll-behavior: smooth;
|
|
||||||
padding: 38px 18px 38px 20px;
|
|
||||||
border-right: 1px solid rgba(0,0,0,.08);
|
|
||||||
background: linear-gradient(180deg, rgba(0,0,0,.02), rgba(0,0,0,0));
|
|
||||||
scrollbar-width: none; -ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
.lb-thumbs::-webkit-scrollbar { width: 0; height: 0; }
|
|
||||||
.lb-thumb {
|
|
||||||
position: relative; display: block; width: 100%; aspect-ratio: 3 / 4;
|
|
||||||
padding: 0; margin: 0 0 8px; border: 0;
|
|
||||||
@apply bg-[#ebebeb] rounded-[2px];
|
|
||||||
overflow: hidden; cursor: pointer;
|
|
||||||
opacity: .42; transition: opacity .2s ease, transform .2s ease;
|
|
||||||
}
|
|
||||||
.lb-thumb:last-child { margin-bottom: 0; }
|
|
||||||
.lb-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
||||||
.lb-thumb::after {
|
|
||||||
content: ""; position: absolute; inset: 0; border-radius: 2px;
|
|
||||||
box-shadow: inset 0 0 0 0 transparent; pointer-events: none;
|
|
||||||
transition: box-shadow .2s ease;
|
|
||||||
}
|
|
||||||
.lb-thumb:hover { opacity: .85; }
|
|
||||||
.lb-thumb.is-active { opacity: 1; transform: scale(1.03); }
|
|
||||||
.lb-thumb.is-active::after { box-shadow: inset 0 0 0 2px #111; }
|
|
||||||
.lb-thumb-idx {
|
|
||||||
position: absolute; left: 6px; top: 6px; z-index: 1;
|
|
||||||
font: 600 9px/1 ui-monospace, SFMono-Regular, monospace;
|
|
||||||
@apply tracking-[.12em] text-white px-[5px] py-[3px] rounded-[3px];
|
|
||||||
background: rgba(0,0,0,.55);
|
|
||||||
backdrop-filter: blur(4px); pointer-events: none;
|
|
||||||
}
|
|
||||||
.lb-thumb.is-active .lb-thumb-idx { @apply bg-[#111] text-white; }
|
|
||||||
|
|
||||||
.lb-main {
|
|
||||||
flex: 1 1 auto; min-width: 0; min-height: 0;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
position: relative; padding: 0 72px;
|
|
||||||
}
|
|
||||||
.lb-img {
|
|
||||||
max-width: 100%; max-height: 100%; display: block;
|
|
||||||
object-fit: contain; user-select: none; -webkit-user-drag: none;
|
|
||||||
transition: opacity .2s ease; cursor: zoom-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 放大态:宽度最大化 + 纵向滚动(无滚轮缩放、无拖拽、无特效) */
|
|
||||||
.lb-main.is-zoomed { align-items: stretch; display: block; overflow-y: auto; overflow-x: hidden; }
|
|
||||||
.lb-main.is-zoomed .lb-img { width: 100%; max-width: 100%; height: auto; max-height: none; cursor: zoom-out; }
|
|
||||||
|
|
||||||
.lb-btn {
|
|
||||||
position: absolute; z-index: 3;
|
|
||||||
width: 42px; height: 42px;
|
|
||||||
@apply rounded-full;
|
|
||||||
border: 1px solid rgba(0,0,0,.14); background: rgba(255,255,255,.72);
|
|
||||||
@apply text-[#111];
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
transition: background .15s, transform .15s, border-color .15s; backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
.lb-btn svg, .lb-tbtn svg {
|
|
||||||
display: block; fill: none; stroke: currentColor;
|
|
||||||
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
.lb-btn svg { width: 20px; height: 20px; }
|
|
||||||
.lb-tbtn svg { width: 16px; height: 16px; }
|
|
||||||
.lb-btn:hover { background: rgba(0,0,0,.06); border-color: rgba(0,0,0,.24); }
|
|
||||||
.lb-close { top: 18px; right: 18px; }
|
|
||||||
.lb-prev { left: 114px; top: 50%; transform: translateY(-50%); }
|
|
||||||
.lb-next { right: 18px; top: 50%; transform: translateY(-50%); }
|
|
||||||
.lb-prev:hover, .lb-next:hover { transform: translateY(-50%) scale(1.08); }
|
|
||||||
|
|
||||||
/* 左栏上下翻页按钮(浮在缩略图条上、禁手动滚动时的翻页入口) */
|
|
||||||
.lb-tbtn {
|
|
||||||
position: absolute; z-index: 4;
|
|
||||||
left: 20px; width: 72px; height: 34px;
|
|
||||||
border: 1px solid rgba(0,0,0,.12); background: rgba(255,255,255,.82);
|
|
||||||
@apply rounded-[6px];
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
@apply text-[#111];
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
transition: background .15s, transform .15s, border-color .15s;
|
|
||||||
}
|
|
||||||
.lb-tbtn svg { width: 16px; height: 16px; display: block; }
|
|
||||||
.lb-tbtn:hover { background: rgba(0,0,0,.06); border-color: rgba(0,0,0,.22); }
|
|
||||||
.lb-tup { top: 8px; }
|
|
||||||
.lb-tdown { bottom: 8px; }
|
|
||||||
|
|
||||||
.lb-counter {
|
|
||||||
position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 3;
|
|
||||||
font: 600 12px/1 ui-monospace, monospace;
|
|
||||||
@apply tracking-[.14em] text-[#111];
|
|
||||||
background: rgba(255,255,255,.75); border: 1px solid rgba(0,0,0,.1);
|
|
||||||
@apply px-[11px] py-[7px] rounded-[6px];
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
}
|
|
||||||
.lb-caption {
|
|
||||||
position: absolute; left: 0; right: 0; bottom: 44px; z-index: 3;
|
|
||||||
text-align: center; color: rgba(0,0,0,.62); pointer-events: none;
|
|
||||||
font: 11px/1.4 ui-monospace, monospace;
|
|
||||||
@apply tracking-[.18em] uppercase;
|
|
||||||
padding: 0 24px; text-shadow: 0 1px 3px rgba(255,255,255,.7);
|
|
||||||
}
|
|
||||||
.lb-hint {
|
|
||||||
position: absolute; left: 0; right: 0; bottom: 16px; z-index: 3;
|
|
||||||
text-align: center; color: rgba(0,0,0,.45); pointer-events: none;
|
|
||||||
font: 11px/1 ui-monospace, monospace;
|
|
||||||
@apply tracking-[.14em];
|
|
||||||
padding: 0 24px; transition: opacity .2s ease; text-shadow: 0 1px 3px rgba(255,255,255,.7);
|
|
||||||
}
|
|
||||||
.lb-hint.is-hidden { opacity: 0; }
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.lb-thumbs { flex: 0 0 76px; padding: 32px 8px 32px 12px; }
|
|
||||||
.lb-thumb { margin-bottom: 8px; }
|
|
||||||
.lb-thumb-idx { font-size: 8px; left: 4px; top: 4px; padding: 2px 4px; }
|
|
||||||
.lb-main { padding: 0 18px; }
|
|
||||||
.lb-btn { width: 38px; height: 38px; }
|
|
||||||
.lb-prev { left: 82px; } .lb-next { right: 8px; }
|
|
||||||
.lb-tbtn { left: 12px; width: 52px; height: 30px; }
|
|
||||||
.lb-caption { bottom: 40px; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
<!-- Alpine.js 业务逻辑代码 -->
|
||||||
<script>
|
<script>
|
||||||
// 真实数据接入:按 ?id= 拉取后端接口,失败时保留上方示例布局
|
document.addEventListener('alpine:init', () => {
|
||||||
const API = import.meta.env.PUBLIC_API_BASE || ""
|
Alpine.data('articleDetail', () => ({
|
||||||
const toAbs = (url: string) =>
|
loading: true,
|
||||||
!url ? "" : /^https?:\/\//i.test(url) ? url : API + url
|
error: null,
|
||||||
const id = new URLSearchParams(location.search).get("id")
|
article: null,
|
||||||
if (id) {
|
bodyParagraphs: [],
|
||||||
;(async () => {
|
|
||||||
try {
|
// 灯箱状态
|
||||||
const res = await fetch(`${API}/api/public/articles/${id}`)
|
lbOpen: false,
|
||||||
if (!res.ok) return
|
lbIndex: 0,
|
||||||
const d = (await res.json()).data
|
lbZoomed: false,
|
||||||
if (!d) return
|
|
||||||
const titleEl = document.querySelector("h1")
|
// 触摸滑动记录
|
||||||
if (titleEl && d.title) titleEl.textContent = d.title
|
touchStartX: 0,
|
||||||
const bodyEl = document.getElementById("art-body")
|
touchStartY: 0,
|
||||||
if (bodyEl && d.description) {
|
|
||||||
bodyEl.innerHTML = d.description
|
async init() {
|
||||||
.split("\n\n")
|
const api = import.meta.env.PUBLIC_API_BASE || "";
|
||||||
.map((p: string) => `<p>${p}</p>`)
|
const id = new URLSearchParams(window.location.search).get("id");
|
||||||
.join("")
|
|
||||||
|
if (!id) {
|
||||||
|
this.error = "未指定文章 ID (Missing Article ID)";
|
||||||
|
this.loading = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const brandEl = document.getElementById("art-brand")
|
|
||||||
if (brandEl && d.brand_name) brandEl.textContent = `Runway · ${d.brand_name}`
|
try {
|
||||||
const yearEl = document.getElementById("art-year")
|
const res = await fetch(`${api}/api/public/articles/${id}`);
|
||||||
if (yearEl && d.year) yearEl.textContent = String(d.year)
|
if (!res.ok) throw new Error("网络请求失败");
|
||||||
const dateEl = document.getElementById("art-date")
|
const resData = await res.json();
|
||||||
if (dateEl && d.published_at) {
|
if (!resData || !resData.data) throw new Error("未获取到有效文章数据");
|
||||||
const dt = new Date(Number(d.published_at) * 1000)
|
|
||||||
dateEl.textContent = `${dt.getFullYear()}.${String(dt.getMonth() + 1).padStart(2, "0")}.${String(dt.getDate()).padStart(2, "0")}`
|
const data = resData.data;
|
||||||
}
|
|
||||||
const imgs = d.images
|
// 补全图片相对路径
|
||||||
if (imgs && Array.isArray(imgs)) {
|
if (Array.isArray(data.images)) {
|
||||||
const gal = document.getElementById("art-gallery")
|
data.images = data.images.map(im => ({
|
||||||
if (gal) {
|
...im,
|
||||||
gal.innerHTML = imgs
|
imageUrl: im.image ? (/^https?:\/\//i.test(im.image) ? im.image : api + im.image) : ""
|
||||||
.map(
|
}));
|
||||||
(im: any) =>
|
|
||||||
`<figure class="bg-[#f2f2f2]"><img src="${toAbs(im.image)}" alt="${im.name || ""}" loading="lazy" class="w-full aspect-[3/4] object-cover" /><figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2">${im.name || ""}</figcaption></figure>`
|
|
||||||
)
|
|
||||||
.join("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.article = data;
|
||||||
|
|
||||||
|
// 处理正文段落
|
||||||
|
if (data.description) {
|
||||||
|
this.bodyParagraphs = data.description.split("\n\n").filter(Boolean);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fetch Article Error:", err);
|
||||||
|
this.error = "加载文章失败,请刷新或稍后再试";
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
} catch {
|
},
|
||||||
/* 后端不可达时,使用示例数据 */
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
// 时间格式化辅助函数
|
||||||
(function () {
|
formatDate(ts) {
|
||||||
const gallery = document.getElementById("art-gallery");
|
if (!ts) return "";
|
||||||
const lb = document.getElementById("lb");
|
const dt = new Date(Number(ts) * 1000);
|
||||||
const lbStage = document.getElementById("lb-stage");
|
if (isNaN(dt.getTime())) return "";
|
||||||
const lbMain = document.getElementById("lb-main");
|
const y = dt.getFullYear();
|
||||||
const lbImg = document.getElementById("lb-img");
|
const m = String(dt.getMonth() + 1).padStart(2, "0");
|
||||||
const lbCounter = document.getElementById("lb-counter");
|
const day = String(dt.getDate()).padStart(2, "0");
|
||||||
const lbCaption = document.getElementById("lb-caption");
|
return `${y}.${m}.${day}`;
|
||||||
const lbThumbs = document.getElementById("lb-thumbs");
|
},
|
||||||
const lbHint = document.getElementById("lb-hint");
|
|
||||||
const btnClose = document.getElementById("lb-close");
|
|
||||||
const btnPrev = document.getElementById("lb-prev");
|
|
||||||
const btnNext = document.getElementById("lb-next");
|
|
||||||
const btnTup = document.getElementById("lb-tup");
|
|
||||||
const btnTdown = document.getElementById("lb-tdown");
|
|
||||||
const viewall = document.getElementById("art-viewall");
|
|
||||||
|
|
||||||
let items = [];
|
// 打开灯箱
|
||||||
let index = 0;
|
openLb(index = 0) {
|
||||||
let zoomed = false;
|
if (!this.article?.images?.length) return;
|
||||||
|
this.lbIndex = index;
|
||||||
|
this.lbZoomed = false;
|
||||||
|
this.lbOpen = true;
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
this.scrollThumbIntoView();
|
||||||
|
},
|
||||||
|
|
||||||
/* 放大态:宽度最大化 + 纵向滚动(无滚轮缩放 / 无拖拽 / 无特效) */
|
// 关闭灯箱
|
||||||
function zoomIn() {
|
closeLb() {
|
||||||
zoomed = true;
|
this.lbOpen = false;
|
||||||
if (lbMain) { lbMain.classList.add("is-zoomed"); lbMain.scrollTop = 0; }
|
this.lbZoomed = false;
|
||||||
if (lbHint) lbHint.classList.add("is-hidden");
|
document.body.style.overflow = "";
|
||||||
}
|
},
|
||||||
function resetZoom() {
|
|
||||||
zoomed = false;
|
|
||||||
if (lbMain) { lbMain.classList.remove("is-zoomed"); lbMain.scrollTop = 0; }
|
|
||||||
if (lbHint) lbHint.classList.remove("is-hidden");
|
|
||||||
}
|
|
||||||
function toggleZoom() { zoomed ? resetZoom() : zoomIn(); }
|
|
||||||
|
|
||||||
/* 收集画廊图片 */
|
// 切换视图
|
||||||
function collect() {
|
showLb(index) {
|
||||||
const list = [];
|
const total = this.article?.images?.length || 0;
|
||||||
if (gallery) gallery.querySelectorAll("img").forEach((img) => {
|
if (total === 0) return;
|
||||||
const fig = img.closest("figure");
|
this.lbIndex = (index + total) % total;
|
||||||
const cap = fig && fig.querySelector("figcaption");
|
this.resetZoom();
|
||||||
list.push({ src: img.currentSrc || img.src, name: (cap && cap.textContent || "").trim() });
|
this.scrollThumbIntoView();
|
||||||
});
|
},
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 渲染左侧竖排缩略图条 */
|
nextLb() { this.showLb(this.lbIndex + 1); },
|
||||||
function renderStrip() {
|
prevLb() { this.showLb(this.lbIndex - 1); },
|
||||||
if (!lbThumbs) return;
|
|
||||||
lbThumbs.innerHTML = items
|
toggleZoom() {
|
||||||
.map((it, i) => {
|
this.lbZoomed = !this.lbZoomed;
|
||||||
const n = String(i + 1).padStart(2, "0");
|
if (this.$refs.lbMain) this.$refs.lbMain.scrollTop = 0;
|
||||||
return '<button type="button" class="lb-thumb' + (i === index ? ' is-active' : '') + '" data-i="' + i + '">' +
|
},
|
||||||
'<img src="' + it.src + '" alt="" />' +
|
|
||||||
'<span class="lb-thumb-idx">' + n + '</span>' +
|
resetZoom() {
|
||||||
'</button>';
|
this.lbZoomed = false;
|
||||||
})
|
if (this.$refs.lbMain) this.$refs.lbMain.scrollTop = 0;
|
||||||
.join("");
|
},
|
||||||
}
|
|
||||||
function show(i) {
|
// 左侧缩略图自动居中滚到视野中央
|
||||||
if (!items.length) return;
|
scrollThumbIntoView() {
|
||||||
index = (i + items.length) % items.length;
|
this.$nextTick(() => {
|
||||||
const it = items[index];
|
const thumbsEl = this.$refs.lbThumbs;
|
||||||
resetZoom();
|
if (!thumbsEl) return;
|
||||||
lbImg.style.opacity = "0";
|
const activeThumb = thumbsEl.children[this.lbIndex];
|
||||||
const loader = new Image();
|
if (activeThumb) {
|
||||||
loader.onload = function () { lbImg.src = it.src; lbImg.style.opacity = "1"; };
|
const target = activeThumb.offsetTop - (thumbsEl.clientHeight - activeThumb.clientHeight) / 2;
|
||||||
loader.src = it.src;
|
thumbsEl.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
|
||||||
if (lbCounter) lbCounter.textContent = (index + 1) + " / " + items.length;
|
}
|
||||||
if (lbCaption) lbCaption.textContent = it.name || "";
|
});
|
||||||
if (lbThumbs) lbThumbs.querySelectorAll(".lb-thumb").forEach((t, k) => {
|
},
|
||||||
t.classList.toggle("is-active", k === index);
|
|
||||||
if (k === index) {
|
// 手势处理
|
||||||
// 自动居中当前张(左栏禁用手动滚动,改用程序化 scrollTo)
|
handleTouchStart(e) {
|
||||||
const target = t.offsetTop - (lbThumbs.clientHeight - t.clientHeight) / 2;
|
if (e.touches.length === 1) {
|
||||||
lbThumbs.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
|
this.touchStartX = e.touches[0].clientX;
|
||||||
|
this.touchStartY = e.touches[0].clientY;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
|
||||||
function open(i) {
|
|
||||||
items = collect();
|
|
||||||
if (!items.length) return;
|
|
||||||
if (lb) { lb.classList.add("is-open"); lb.setAttribute("aria-hidden", "false"); }
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
renderStrip();
|
|
||||||
show(i || 0);
|
|
||||||
}
|
|
||||||
function close() {
|
|
||||||
if (lb) { lb.classList.remove("is-open"); lb.setAttribute("aria-hidden", "true"); }
|
|
||||||
document.body.style.overflow = "";
|
|
||||||
}
|
|
||||||
function next() { show(index + 1); }
|
|
||||||
function prev() { show(index - 1); }
|
|
||||||
|
|
||||||
/* 事件绑定 */
|
handleTouchEnd(e) {
|
||||||
if (gallery) gallery.addEventListener("click", (e) => {
|
if (this.lbZoomed) return;
|
||||||
const fig = e.target.closest("figure");
|
|
||||||
if (!fig) return;
|
|
||||||
const imgs = Array.prototype.slice.call(gallery.querySelectorAll("img"));
|
|
||||||
open(Math.max(0, imgs.indexOf(fig.querySelector("img"))));
|
|
||||||
});
|
|
||||||
if (btnClose) btnClose.addEventListener("click", close);
|
|
||||||
if (btnNext) btnNext.addEventListener("click", (e) => { e.stopPropagation(); next(); });
|
|
||||||
if (btnPrev) btnPrev.addEventListener("click", (e) => { e.stopPropagation(); prev(); });
|
|
||||||
if (btnTup) btnTup.addEventListener("click", (e) => { e.stopPropagation(); prev(); });
|
|
||||||
if (btnTdown) btnTdown.addEventListener("click", (e) => { e.stopPropagation(); next(); });
|
|
||||||
if (lb) lb.addEventListener("click", (e) => {
|
|
||||||
const t = e.target;
|
|
||||||
if (t === lb || (t.classList && t.classList.contains("lb-stage"))) close();
|
|
||||||
});
|
|
||||||
if (lbThumbs) lbThumbs.addEventListener("click", (e) => {
|
|
||||||
const t = e.target.closest(".lb-thumb");
|
|
||||||
if (t) show(Number(t.dataset.i));
|
|
||||||
});
|
|
||||||
if (lbImg) lbImg.addEventListener("click", toggleZoom);
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (!lb || !lb.classList.contains("is-open")) return;
|
|
||||||
if (e.key === "Escape") close();
|
|
||||||
else if (e.key === "ArrowRight") next();
|
|
||||||
else if (e.key === "ArrowLeft") prev();
|
|
||||||
});
|
|
||||||
if (viewall) viewall.addEventListener("click", () => open(0));
|
|
||||||
|
|
||||||
/* 移动端单指滑动切换(仅在未放大时,放大态交给纵向滚动) */
|
|
||||||
let tx0 = 0, ty0 = 0, two = false;
|
|
||||||
if (lbStage) {
|
|
||||||
lbStage.addEventListener("touchstart", (e) => {
|
|
||||||
if (e.touches.length === 1) { tx0 = e.touches[0].clientX; ty0 = e.touches[0].clientY; }
|
|
||||||
else if (e.touches.length === 2) two = true;
|
|
||||||
}, { passive: true });
|
|
||||||
lbStage.addEventListener("touchend", (e) => {
|
|
||||||
if (two) { two = false; return; }
|
|
||||||
if (zoomed) return;
|
|
||||||
const t = e.changedTouches[0];
|
const t = e.changedTouches[0];
|
||||||
const dx = t.clientX - tx0, dy = t.clientY - ty0;
|
const dx = t.clientX - this.touchStartX;
|
||||||
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) (dx < 0 ? next() : prev());
|
const dy = t.clientY - this.touchStartY;
|
||||||
}, { passive: true });
|
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
|
||||||
}
|
dx < 0 ? this.nextLb() : this.prevLb();
|
||||||
|
}
|
||||||
/* 计数 + 画廊重渲染后同步 */
|
}
|
||||||
function syncCount() {
|
}));
|
||||||
const c = document.getElementById("art-gallery-count");
|
});
|
||||||
const n = gallery ? gallery.querySelectorAll("img").length : 0;
|
|
||||||
if (c) c.textContent = n ? "· 共 " + n + " 张" : "";
|
|
||||||
}
|
|
||||||
syncCount();
|
|
||||||
if (gallery && "MutationObserver" in window) {
|
|
||||||
new MutationObserver(syncCount).observe(gallery, { childList: true });
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
@ -3,6 +3,7 @@ import '../styles/global.css'
|
|||||||
import { getRelativeLocaleUrl } from 'astro:i18n'
|
import { getRelativeLocaleUrl } from 'astro:i18n'
|
||||||
import { useTranslations, DEFAULT_LOCALE, type Locale } from '../i18n/utils'
|
import { useTranslations, DEFAULT_LOCALE, type Locale } from '../i18n/utils'
|
||||||
import { LOCALES, LOCALE_NAMES, isLocaleSegment } from '../i18n/config'
|
import { LOCALES, LOCALE_NAMES, isLocaleSegment } from '../i18n/config'
|
||||||
|
import LanguageDetector from '../components/LanguageDetector.astro'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title = 'Portfolio Studio',
|
title = 'Portfolio Studio',
|
||||||
@ -14,43 +15,17 @@ const {
|
|||||||
const locale = (Astro.currentLocale as Locale) ?? DEFAULT_LOCALE
|
const locale = (Astro.currentLocale as Locale) ?? DEFAULT_LOCALE
|
||||||
const { t } = useTranslations(locale)
|
const { t } = useTranslations(locale)
|
||||||
|
|
||||||
|
|
||||||
// 当前路径去掉语言前缀与尾随 .html(format:'file' 产物如 cn.html / shows.html),
|
|
||||||
// 用于语言切换器拼出「同页面、另一语言」的链接(逻辑路径,不带 .html)
|
|
||||||
// 例:/cn.html → / ,/cn/shows.html → /shows ,/shows.html → /shows
|
|
||||||
let _segs = Astro.url.pathname.split('/').filter(Boolean);
|
|
||||||
if (_segs[0] && isLocaleSegment(_segs[0])) _segs = _segs.slice(1);
|
|
||||||
const _last = _segs[_segs.length - 1];
|
|
||||||
if (_last && _last.endsWith('.html')) _segs = _segs.slice(0, -1);
|
|
||||||
const currentPath = _segs.length === 0 ? '/' : '/' + _segs.join('/');
|
|
||||||
|
|
||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
||||||
<head>
|
<head>
|
||||||
|
<!-- <LanguageDetector /> -->
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<!-- 语言自动检测:无前缀且浏览器为中文环境时,跳转到 /cn 前缀 -->
|
|
||||||
<script is:inline>
|
|
||||||
(function () {
|
|
||||||
try {
|
|
||||||
var SUPPORTED = ['en', 'cn'];
|
|
||||||
var path = location.pathname;
|
|
||||||
var seg = path.split('/').filter(Boolean)[0];
|
|
||||||
if (SUPPORTED.indexOf(seg) !== -1) return; // 已有语言前缀
|
|
||||||
var lang = (navigator.language || 'en').toLowerCase();
|
|
||||||
var target = lang.indexOf('zh') === 0 ? 'cn' : 'en';
|
|
||||||
if (target === 'en') return; // 默认英文无需跳转
|
|
||||||
if (sessionStorage.getItem('lang-redirect')) return;
|
|
||||||
sessionStorage.setItem('lang-redirect', '1');
|
|
||||||
location.pathname = '/' + target + path;
|
|
||||||
} catch (e) {}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-black min-h-screen flex flex-col p-[30px_50px] font-sans">
|
<body class="bg-white text-black min-h-screen flex flex-col p-[30px_50px] font-sans">
|
||||||
|
|
||||||
@ -106,7 +81,7 @@ const currentPath = _segs.length === 0 ? '/' : '/' + _segs.join('/');
|
|||||||
{LOCALES.map((l) => (
|
{LOCALES.map((l) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={getRelativeLocaleUrl(l, currentPath)}
|
href={getRelativeLocaleUrl(l)}
|
||||||
@click="open = false"
|
@click="open = false"
|
||||||
class:list={[
|
class:list={[
|
||||||
'block px-4 py-1.5 text-[12px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
'block px-4 py-1.5 text-[12px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
||||||
|
|||||||
864
src/pages/cn/shows.astro
Normal file
864
src/pages/cn/shows.astro
Normal file
@ -0,0 +1,864 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro"
|
||||||
|
import { getBrands, getYears, type BrandEntry } from "../lib/api"
|
||||||
|
|
||||||
|
let sidebarBrands: BrandEntry[] = []
|
||||||
|
let yearChips: number[] = []
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
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
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="All Shows — Portfolio Studio">
|
||||||
|
<div class="showsroot w-full" x-data="showsPage()">
|
||||||
|
<!-- 顶部:极简眉头 -->
|
||||||
|
<header class="showshead">
|
||||||
|
<div>
|
||||||
|
<nav class="showscrumb" aria-label="Breadcrumb">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<span class="sep" aria-hidden="true">/</span>
|
||||||
|
<a href="/shows">All Shows</a>
|
||||||
|
<span class="sep" aria-hidden="true">/</span>
|
||||||
|
<span class="cur" aria-current="page" x-text="crumbYear"></span>
|
||||||
|
</nav>
|
||||||
|
<h1 class="showsh1">All Shows</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 已激活筛选 pills -->
|
||||||
|
<div class="showspills" x-show="pills.length > 0" x-cloak>
|
||||||
|
<template x-for="(p, i) in pills" :key="i">
|
||||||
|
<span class="showspill">
|
||||||
|
<span x-text="p.label"></span>
|
||||||
|
<button class="x" type="button" @click="p.clear()">×</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体:左 sidebar + 右 4 列商品 grid -->
|
||||||
|
<div class="showsbody">
|
||||||
|
<!-- LEFT SIDEBAR -->
|
||||||
|
<aside class="showsaside">
|
||||||
|
<div class="showsaside-head">
|
||||||
|
<h2 class="showsaside-h">REFINE</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="showsaside-body">
|
||||||
|
<!-- Designer 多选 -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('designers')">
|
||||||
|
<span>DESIGNERS</span>
|
||||||
|
<span class="showsg-right">
|
||||||
|
<span class="showsg-count" x-text="selectedBrands.length">0</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.designers }">
|
||||||
|
<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" x-show="!collapsedGroups.designers">
|
||||||
|
<input class="showssearch" placeholder="Search designer…" autocomplete="off" x-model.debounce.150ms="keyword" />
|
||||||
|
<ul class="showschecks">
|
||||||
|
<li>
|
||||||
|
<!-- Letter Header -->
|
||||||
|
<template x-if="item.type === 'letter'">
|
||||||
|
<div class="letter-head" x-text="item.letter"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Brand Item -->
|
||||||
|
<template x-if="item.type === 'brand'">
|
||||||
|
<label class="flex items-center gap-2 w-full cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:value="item.id"
|
||||||
|
:checked="selectedBrands.includes(item.id)"
|
||||||
|
@change="toggleBrand(item.id)"
|
||||||
|
>
|
||||||
|
<span x-text="item.name"></span>
|
||||||
|
<span class="ct" x-text="item.count ? item.count : ''"></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="showsmore" @click="openBrandModal()" x-text="'浏览全部品牌 +'"></a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Collection type -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('collection')">
|
||||||
|
<span>COLLECTION</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.collection }">
|
||||||
|
<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" x-show="!collapsedGroups.collection">
|
||||||
|
<ul class="showschecks showschecks--flat">
|
||||||
|
<li><label><input type="checkbox" value="rtw" x-model="selectedCt" @change="refreshFilter()" /> <span>Ready-to-Wear</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="menswear" x-model="selectedCt" @change="refreshFilter()" /> <span>Menswear</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="couture" x-model="selectedCt" @change="refreshFilter()" /> <span>Couture</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="resort" x-model="selectedCt" @change="refreshFilter()" /> <span>Resort</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="pre_fall" x-model="selectedCt" @change="refreshFilter()" /> <span>Pre-Fall</span></label></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Season -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('season')">
|
||||||
|
<span>SEASON</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.season }">
|
||||||
|
<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" x-show="!collapsedGroups.season">
|
||||||
|
<ul class="showschecks showschecks--flat">
|
||||||
|
<li><label><input type="checkbox" value="spring" x-model="selectedSeason" @change="refreshFilter()" /> <span>Spring / Summer</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="fall" x-model="selectedSeason" @change="refreshFilter()" /> <span>Fall / Winter</span></label></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Year -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('year')">
|
||||||
|
<span>YEAR</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.year }">
|
||||||
|
<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" x-show="!collapsedGroups.year">
|
||||||
|
<ul class="showschecks showschecks--flat">
|
||||||
|
{yearChips.map((y) => (
|
||||||
|
<li><label><input type="checkbox" value={String(y)} x-model="selectedYear" @change="refreshFilter()" /> <span>{y}</span></label></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button class="showsclear" type="button" @click="clearAll()">Clear all ×</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- MAIN GRID -->
|
||||||
|
<main class="showsmain">
|
||||||
|
<div class="showsgrid" :class="{ 'is-loading': loading }">
|
||||||
|
|
||||||
|
<!-- 骨架屏:结构与真实卡片完全一致,loading 期间占位,杜绝 加载中→数据完成 的尺寸抖动 -->
|
||||||
|
<template x-if="loading && items.length === 0">
|
||||||
|
<template x-for="i in skeletonCount" :key="'skel-'+i">
|
||||||
|
<div class="showscard showscard--skel" aria-hidden="true">
|
||||||
|
<div class="showscard-left">
|
||||||
|
<div class="showscard-img"></div>
|
||||||
|
</div>
|
||||||
|
<div class="showscard-right">
|
||||||
|
<div class="showscard-info">
|
||||||
|
<div class="showsline h-[41px] mb-[10px] rounded-[2px]"></div>
|
||||||
|
<div class="showsline h-[29px] mb-[14px] rounded-[2px]"></div>
|
||||||
|
<div class="showsline h-[38px] rounded-[2px]"></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>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<template x-if="!loading && items.length === 0">
|
||||||
|
<div class="showsempty">No shows match the current filters</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Card List -->
|
||||||
|
<template x-for="it in items" :key="it.id">
|
||||||
|
<a class="showscard" :href="`/article?id=${it.id}`" :aria-label="`${it.brand_name} ${it.title}`">
|
||||||
|
<div class="showscard-left">
|
||||||
|
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||||
|
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||||
|
<img :src="toAbsUrl(it.cover)" :alt="it.title" loading="lazy" @load="it._imgLoaded = true" @error="it._imgLoaded = true" :class="{ 'is-loaded': it._imgLoaded }" />
|
||||||
|
<!-- <span class="showsbadge" x-show="isNewAdded(it.published_at)">New</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="showscard-right">
|
||||||
|
<div class="showscard-info">
|
||||||
|
<h3 class="showscard-title" x-text="it.title"></h3>
|
||||||
|
<p class="showscard-meta" x-text="`${it.season_code || it.year || ''} · ${ctLabel(it.collection_type)} · ${it.brand_name}`"></p>
|
||||||
|
<div class="showscard-meta-foot">
|
||||||
|
<span class="showscard-meta-line"><span x-html="ICON_USER"></span><span x-text="it.brand_name"></span></span>
|
||||||
|
<span class="showscard-meta-line"><span x-html="ICON_DATE"></span><span x-text="fmtDate(it.published_at)"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="showscard-strip">
|
||||||
|
<template x-for="(slot, idx) in getCardSlots(it)" :key="idx">
|
||||||
|
<div class="showscard-thumb" :class="{ 'placeholder': !slot.src, 'is-loaded': slot.loaded }">
|
||||||
|
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG" x-show="slot.src"></span>
|
||||||
|
<img x-show="slot.src" :src="toAbsUrl(slot.src)" alt="" loading="lazy" @load="slot.loaded = true" @error="slot.loaded = true" :class="{ 'is-loaded': slot.loaded }" />
|
||||||
|
<span class="showscard-thumb-more" x-show="slot.isLast && slot.moreLabel" x-text="slot.moreLabel"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="showsloadmore-wrap" x-show="items.length > 0">
|
||||||
|
<button class="showsloadmore" type="button" :disabled="loadingMore || isEnd" @click="loadPage(page + 1, true)" x-text="loadBtnText"></button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 品牌筛选弹窗:搜索 + A-Z 跳转 + 热门推荐,按需加载,避免一次拉全量 -->
|
||||||
|
<div class="showsmodal" x-show="modalOpen" x-cloak>
|
||||||
|
<div class="showsmodal-mask" @click="closeBrandModal()"></div>
|
||||||
|
<div class="showsmodal-panel">
|
||||||
|
<header class="showsmodal-head">
|
||||||
|
<h3>全部品牌</h3>
|
||||||
|
<button class="showsmodal-x" type="button" @click="closeBrandModal()" aria-label="关闭">✕</button>
|
||||||
|
</header>
|
||||||
|
<div class="showsmodal-search">
|
||||||
|
<input
|
||||||
|
class="showsmodal-input"
|
||||||
|
type="text"
|
||||||
|
x-model="modalKeyword"
|
||||||
|
@input.debounce.300ms="modalSearch()"
|
||||||
|
placeholder="搜索品牌名…"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="showsmodal-az">
|
||||||
|
<template x-for="l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('')" :key="l">
|
||||||
|
<button class="az" type="button" :class="{ active: modalLetter === l }" @click="modalPickLetter(l)" x-text="l"></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="showsmodal-body">
|
||||||
|
<div class="showsmodal-hint" x-show="!modalKeyword && !modalLetter">热门推荐</div>
|
||||||
|
<div class="showsmodal-loading" x-show="modalLoading" x-html="SPIN_SVG"></div>
|
||||||
|
<ul class="showsmodal-list">
|
||||||
|
<template x-for="b in modalResults" :key="b.id">
|
||||||
|
<li>
|
||||||
|
<label class="showsmodal-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:value="b.id"
|
||||||
|
:checked="modalSelected.includes(b.id)"
|
||||||
|
:disabled="!modalSelected.includes(b.id) && modalMaxReached"
|
||||||
|
@change="toggleModalBrand(b.id)"
|
||||||
|
/>
|
||||||
|
<span class="showsmodal-name" x-text="b.name"></span>
|
||||||
|
<span class="showsmodal-ct" x-text="b.article_count ? b.article_count : ''"></span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li class="showsmodal-empty" x-show="!modalLoading && modalResults.length === 0">未找到匹配的品牌</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<footer class="showsmodal-foot">
|
||||||
|
<span class="showsmodal-counter" x-text="`已选 ${modalSelected.length} / 最多 5 个`"></span>
|
||||||
|
<button class="showsmodal-ok" type="button" @click="confirmBrandModal()">确定</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@reference "tailwindcss";
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
|
||||||
|
|
||||||
|
/* 使用 @apply 精简并重构样式 */
|
||||||
|
.showsroot {
|
||||||
|
@apply m-auto max-w-[1600px] pt-7 text-[#0a0a0a];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showshead {
|
||||||
|
@apply flex justify-between items-end gap-6 py-5 pb-[26px] ;
|
||||||
|
}
|
||||||
|
.showscrumb {
|
||||||
|
@apply inline-flex items-center gap-[10px] mb-[10px] text-[11px] tracking-[0.18em] uppercase text-[#999] no-underline;
|
||||||
|
}
|
||||||
|
.showscrumb a {
|
||||||
|
@apply text-black no-underline transition-colors duration-150 hover:text-[#888];
|
||||||
|
}
|
||||||
|
.showscrumb .sep {
|
||||||
|
@apply text-[#cfcfcf] select-none;
|
||||||
|
}
|
||||||
|
.showscrumb .cur {
|
||||||
|
@apply text-[#999];
|
||||||
|
}
|
||||||
|
.showsh1 {
|
||||||
|
@apply text-[clamp(40px,6vw,72px)] leading-[0.9] font-black tracking-[-0.02em] my-0 mb-[10px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showspills {
|
||||||
|
@apply flex flex-wrap gap-2 pt-3.5;
|
||||||
|
}
|
||||||
|
.showspill {
|
||||||
|
@apply inline-flex items-center gap-2 border-[1.5px] border-black p-[6px_10px] text-[10.5px] uppercase tracking-[0.14em] text-black bg-white;
|
||||||
|
}
|
||||||
|
.showspill .x {
|
||||||
|
@apply inline-flex w-3.5 h-3.5 items-center justify-center border-none bg-black text-white cursor-pointer text-xs leading-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsbody {
|
||||||
|
@apply flex gap-[36px] items-start max-md:flex-col max-md:gap-[18px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsaside {
|
||||||
|
@apply flex-none w-[240px] bg-white sticky top-5 max-h-[calc(100vh-40px)] rounded-none transition-[flex-basis] duration-250 ease-in-out flex flex-col max-md:static max-md:max-h-none max-md:w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsaside-head {
|
||||||
|
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
||||||
|
}
|
||||||
|
.showsaside-h {
|
||||||
|
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none transition-opacity duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsaside-body {
|
||||||
|
@apply p-[14px_14px_18px] overflow-y-auto flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#ccc_#fff];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsg {
|
||||||
|
@apply border-t border-[#e6e6e6] py-[12px] pb-[6px] first-of-type:border-t-0 first-of-type:pt-1;
|
||||||
|
}
|
||||||
|
.showsg-h {
|
||||||
|
@apply flex justify-between items-center text-[11px] tracking-[0.22em] uppercase font-bold text-black mb-2 cursor-pointer select-none;
|
||||||
|
}
|
||||||
|
.showsg-right {
|
||||||
|
@apply inline-flex items-center gap-2.5;
|
||||||
|
}
|
||||||
|
.showsg-chev {
|
||||||
|
@apply inline-block text-[9px] leading-none text-[#999] transition-transform duration-200 ml-1;
|
||||||
|
}
|
||||||
|
.showsg-chev.is-collapsed {
|
||||||
|
@apply -rotate-90;
|
||||||
|
}
|
||||||
|
.showsg-count {
|
||||||
|
@apply text-[#999] font-normal text-[10px] tracking-[0.14em];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showssearch {
|
||||||
|
@apply w-full border border-[#d4d4d4] p-[7px_10px] text-[11px] uppercase tracking-[0.08em] text-black rounded-none outline-none bg-[#fafafa] focus:border-black focus:bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschecks {
|
||||||
|
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin];
|
||||||
|
}
|
||||||
|
.showschecks li {
|
||||||
|
@apply flex items-center gap-2 py-[5px] text-[11.5px] uppercase tracking-[0.04em] text-black cursor-pointer select-none hover:bg-[#f6f6f6];
|
||||||
|
}
|
||||||
|
.showschecks li.letter-head {
|
||||||
|
@apply p-[8px_0_3px] text-[10px] tracking-[0.22em] text-[#8a8a8a] cursor-default pointer-events-none border-t border-dotted border-[#e6e6e6] first:border-t-0 first:pt-1;
|
||||||
|
}
|
||||||
|
.showschecks input[type="checkbox"] {
|
||||||
|
@apply appearance-none w-3 h-3 border-[1.5px] border-black bg-white cursor-pointer shrink-0 rounded-none inline-grid place-content-center checked:bg-black;
|
||||||
|
}
|
||||||
|
.showschecks input[type="checkbox"]:checked::after {
|
||||||
|
content: "";
|
||||||
|
@apply w-[6px] h-[6px] bg-white;
|
||||||
|
}
|
||||||
|
.showschecks .ct {
|
||||||
|
@apply text-[#999] text-[10px] ml-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschecks--flat li {
|
||||||
|
@apply py-[4px];
|
||||||
|
}
|
||||||
|
.showschecks--flat li label {
|
||||||
|
@apply inline-flex items-center gap-2 cursor-pointer w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsmore {
|
||||||
|
@apply inline-block mt-[10px] text-[10.5px] uppercase tracking-[0.16em] text-[#555] cursor-pointer hover:text-black;
|
||||||
|
}
|
||||||
|
.showsclear {
|
||||||
|
@apply mt-[18px] w-full border-[1.5px] border-black bg-white text-black p-[11px_12px] text-[11px] uppercase tracking-[0.22em] font-bold cursor-pointer rounded-none transition-all duration-150 hover:bg-black hover:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsmain {
|
||||||
|
@apply flex-1 min-w-0;
|
||||||
|
}
|
||||||
|
.showsgrid {
|
||||||
|
@apply grid grid-cols-2 gap-[22px] relative max-[560px]:grid-cols-1;
|
||||||
|
}
|
||||||
|
.showsgrid.is-loading::before {
|
||||||
|
content: "";
|
||||||
|
@apply absolute -top-1 left-0 h-[2px] w-[38%] bg-black z-10;
|
||||||
|
animation: hbxProg 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard {
|
||||||
|
@apply relative flex gap-[14px] no-underline text-inherit bg-white p-3 transition-colors duration-200 hover:border-black max-[700px]:flex-col max-[700px]:gap-[10px];
|
||||||
|
}
|
||||||
|
.showscard:hover .showscard-img img {
|
||||||
|
@apply scale-104;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard-left {
|
||||||
|
@apply flex-none w-[30%] min-w-[110px] max-[700px]:w-full max-[700px]:flex-none;
|
||||||
|
}
|
||||||
|
.showscard-img {
|
||||||
|
@apply relative w-full aspect-[3/4] bg-[#f1f1f1] overflow-hidden rounded-none;
|
||||||
|
}
|
||||||
|
.showscard-img img {
|
||||||
|
@apply w-full h-full object-cover transition-transform duration-600 opacity-0;
|
||||||
|
}
|
||||||
|
.showscard-img img.is-loaded {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsspin {
|
||||||
|
@apply absolute inset-0 flex items-center justify-center text-[#bdbdbd] opacity-100 transition-opacity duration-300 pointer-events-none z-10;
|
||||||
|
}
|
||||||
|
.showscard-img.is-loaded .showsspin,
|
||||||
|
.showscard-thumb.is-loaded .showsspin {
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsbadge {
|
||||||
|
@apply absolute top-[6px] right-[6px] text-[9px] tracking-[0.14em] uppercase font-bold text-white bg-black p-[3px_6px];
|
||||||
|
}
|
||||||
|
.showsflag-featured {
|
||||||
|
@apply absolute top-[6px] left-[6px] text-[9px] tracking-[0.14em] uppercase font-bold text-black bg-white p-[3px_6px] border border-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard-right {
|
||||||
|
@apply flex-1 min-w-0 flex flex-col justify-between max-[700px]:w-full;
|
||||||
|
}
|
||||||
|
.showscard-info {
|
||||||
|
@apply flex-none min-w-0 flex flex-col p-[2px_2px_0];
|
||||||
|
}
|
||||||
|
.showscard-title {
|
||||||
|
@apply text-[15.5px] font-bold leading-[1.32] my-0 mb-[10px] text-[#111] tracking-[-0.01em] line-clamp-2 h-[41px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-meta {
|
||||||
|
@apply text-[9.5px] text-[#888] my-0 mb-[14px] leading-[1.55] tracking-[0.04em] uppercase line-clamp-2 h-[29px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-meta-foot {
|
||||||
|
@apply flex flex-col gap-[6px] h-[38px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-meta-line {
|
||||||
|
@apply inline-flex items-center gap-[6px] text-[10.5px] tracking-[0.06em] uppercase text-[#444] h-[16px] leading-[16px] overflow-hidden whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard-strip {
|
||||||
|
@apply grid grid-cols-5 gap-1 mt-[10px];
|
||||||
|
}
|
||||||
|
.showscard-thumb {
|
||||||
|
@apply relative aspect-[3/4] bg-[#f1f1f1] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-thumb img {
|
||||||
|
@apply w-full h-full object-cover opacity-0 transition-opacity duration-450 ease-in-out;
|
||||||
|
}
|
||||||
|
.showscard-thumb img.is-loaded {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
.showscard-thumb.placeholder {
|
||||||
|
@apply bg-[#f6f6f6];
|
||||||
|
}
|
||||||
|
.showscard-thumb-more {
|
||||||
|
@apply absolute inset-0 bg-black/55 text-white text-[11px] font-bold tracking-[0.04em] flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard--skel .showscard-img,
|
||||||
|
.showscard--skel .showscard-thumb,
|
||||||
|
.showscard--skel .showsline {
|
||||||
|
@apply bg-[#efefef];
|
||||||
|
animation: hbxShim 1.6s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsloadmore-wrap {
|
||||||
|
@apply flex justify-center mt-[38px];
|
||||||
|
}
|
||||||
|
.showsloadmore {
|
||||||
|
@apply border-[1.5px] border-black bg-white text-black p-[13px_36px] text-[11px] uppercase tracking-[0.22em] font-bold cursor-pointer rounded-none transition-all duration-200 hover:bg-black hover:text-white disabled:opacity-40 disabled:cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsempty {
|
||||||
|
@apply col-span-full p-[80px_20px] text-center text-xs tracking-[0.22em] uppercase text-[#999] border border-dashed border-[#ccc];
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||||
|
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||||
|
/* ===== 品牌筛选弹窗 ===== */
|
||||||
|
.showsmodal {
|
||||||
|
@apply fixed inset-0 z-[60] flex items-center justify-center p-[16px];
|
||||||
|
}
|
||||||
|
.showsmodal-mask {
|
||||||
|
@apply absolute inset-0 bg-black/45;
|
||||||
|
}
|
||||||
|
.showsmodal-panel {
|
||||||
|
@apply relative w-full max-w-[680px] max-h-[86vh] bg-white flex flex-col border border-black;
|
||||||
|
}
|
||||||
|
.showsmodal-head {
|
||||||
|
@apply flex items-center justify-between px-[16px] py-[13px] border-b border-black shrink-0;
|
||||||
|
}
|
||||||
|
.showsmodal-head h3 {
|
||||||
|
@apply m-0 text-[13px] tracking-[0.2em] uppercase font-bold;
|
||||||
|
}
|
||||||
|
.showsmodal-x {
|
||||||
|
@apply appearance-none bg-white border border-black w-[24px] h-[24px] inline-flex items-center justify-center cursor-pointer text-[12px] leading-none;
|
||||||
|
}
|
||||||
|
.showsmodal-x:hover { @apply bg-black text-white; }
|
||||||
|
.showsmodal-search { @apply px-[16px] pt-[12px] pb-[8px] shrink-0; }
|
||||||
|
.showsmodal-input {
|
||||||
|
@apply w-full border border-black px-[10px] py-[8px] text-[13px] outline-none;
|
||||||
|
}
|
||||||
|
.showsmodal-input:focus { @apply ring-1 ring-black; }
|
||||||
|
.showsmodal-az {
|
||||||
|
@apply flex flex-wrap gap-[4px] px-[16px] pb-[10px] border-b border-[#e6e6e6] shrink-0;
|
||||||
|
}
|
||||||
|
.showsmodal-az .az {
|
||||||
|
@apply appearance-none bg-white border border-[#ccc] w-[22px] h-[22px] text-[11px] cursor-pointer leading-none;
|
||||||
|
}
|
||||||
|
.showsmodal-az .az.active { @apply bg-black text-white border-black; }
|
||||||
|
.showsmodal-body { @apply flex-1 min-h-0 overflow-y-auto px-[16px] py-[8px]; }
|
||||||
|
.showsmodal-hint {
|
||||||
|
@apply text-[11px] tracking-[0.2em] uppercase text-[#888] py-[6px] pb-[8px];
|
||||||
|
}
|
||||||
|
.showsmodal-loading { @apply flex justify-center py-[28px] text-[#bdbdbd]; }
|
||||||
|
.showsmodal-list { @apply list-none m-0 p-0; }
|
||||||
|
.showsmodal-row {
|
||||||
|
@apply flex items-center gap-[8px] w-full cursor-pointer py-[7px] border-b border-[#f0f0f0] text-[13px];
|
||||||
|
}
|
||||||
|
.showsmodal-name { @apply flex-1 truncate; }
|
||||||
|
.showsmodal-ct { @apply text-[#999] text-[11px] ml-auto; }
|
||||||
|
.showsmodal-empty { @apply py-[28px] text-center text-[#999] text-[13px]; }
|
||||||
|
.showsmodal-foot {
|
||||||
|
@apply flex items-center justify-between px-[16px] py-[12px] border-t border-black shrink-0 text-[12px];
|
||||||
|
}
|
||||||
|
.showsmodal-ok {
|
||||||
|
@apply appearance-none bg-black text-white border border-black px-[22px] py-[8px] cursor-pointer text-[12px] tracking-[0.1em];
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Alpine from "alpinejs"
|
||||||
|
import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "../lib/api"
|
||||||
|
|
||||||
|
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 SPIN_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true" class="w-[22px] h-[22px] block"><g class="ring origin-[12px_12px] animate-spin" 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>`
|
||||||
|
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("showsPage", () => {
|
||||||
|
const node = document.getElementById("showsbrands-data")
|
||||||
|
const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : []
|
||||||
|
|
||||||
|
return {
|
||||||
|
allBrands,
|
||||||
|
ICON_USER,
|
||||||
|
ICON_DATE,
|
||||||
|
SPIN_SVG,
|
||||||
|
collapsedGroups: {
|
||||||
|
designers: localStorage.getItem("showsg-designers") === "1",
|
||||||
|
collection: localStorage.getItem("showsg-collection") === "1",
|
||||||
|
season: localStorage.getItem("showsg-season") === "1",
|
||||||
|
year: localStorage.getItem("showsg-year") === "1",
|
||||||
|
},
|
||||||
|
selectedBrands: [] as number[],
|
||||||
|
selectedCt: [] as string[],
|
||||||
|
selectedSeason: [] as string[],
|
||||||
|
selectedYear: [] as string[],
|
||||||
|
keyword: "",
|
||||||
|
brandsExpanded: false,
|
||||||
|
|
||||||
|
// 品牌筛选弹窗状态
|
||||||
|
modalOpen: false,
|
||||||
|
modalKeyword: "",
|
||||||
|
modalLetter: "",
|
||||||
|
modalResults: [] as BrandEntry[],
|
||||||
|
modalLoading: false,
|
||||||
|
modalSelected: [] as number[],
|
||||||
|
get modalMaxReached() {
|
||||||
|
return this.modalSelected.length >= 5
|
||||||
|
},
|
||||||
|
|
||||||
|
items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
page: 1,
|
||||||
|
size: 24,
|
||||||
|
total: 0,
|
||||||
|
isEnd: false,
|
||||||
|
reqToken: 0,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.loadPage(1, false)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算属性:侧边栏设计师列表的分组渲染
|
||||||
|
get visibleBrandItems() {
|
||||||
|
const kw = this.keyword.trim().toLowerCase()
|
||||||
|
const result: any[] = []
|
||||||
|
|
||||||
|
if (kw) {
|
||||||
|
this.allBrands.forEach((b) => {
|
||||||
|
if (b.name.toLowerCase().includes(kw)) {
|
||||||
|
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const map = new Map<string, BrandEntry[]>()
|
||||||
|
for (const b of this.allBrands) {
|
||||||
|
const letter = (b.name[0] || "?").toUpperCase()
|
||||||
|
if (!map.has(letter)) map.set(letter, [])
|
||||||
|
map.get(letter)!.push(b)
|
||||||
|
}
|
||||||
|
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
|
||||||
|
let shown = 0
|
||||||
|
const MAX_INITIAL = 60
|
||||||
|
for (const [letter, list] of grouped) {
|
||||||
|
if (!this.brandsExpanded && shown >= MAX_INITIAL) break
|
||||||
|
result.push({ type: "letter", letter, key: `letter-${letter}` })
|
||||||
|
for (const b of list) {
|
||||||
|
if (!this.brandsExpanded && shown >= MAX_INITIAL) break
|
||||||
|
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
|
||||||
|
shown++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
get hasMoreBrands() {
|
||||||
|
return this.allBrands.length > 60
|
||||||
|
},
|
||||||
|
|
||||||
|
// 面包屑末级:未选年份时显示默认的「2026 Edition」,选中后显示对应年份
|
||||||
|
get crumbYear() {
|
||||||
|
if (this.selectedYear.length) {
|
||||||
|
const ys = [...this.selectedYear].sort()
|
||||||
|
return ys.length === 1 ? `${ys[0]} Edition` : `${ys.join(" / ")}`
|
||||||
|
}
|
||||||
|
return "2026 Edition"
|
||||||
|
},
|
||||||
|
|
||||||
|
// 骨架屏数量与首屏 data 数量一致,保证「加载中」与「数据完成」两阶段的网格高度相同,消除抖动
|
||||||
|
get skeletonCount() {
|
||||||
|
return this.size
|
||||||
|
},
|
||||||
|
|
||||||
|
// 已筛选选项 Pills 视图数据
|
||||||
|
get pills() {
|
||||||
|
const list: { label: string; clear: () => void }[] = []
|
||||||
|
if (this.selectedBrands.length) {
|
||||||
|
const labels = this.selectedBrands
|
||||||
|
.map((id) => this.allBrands.find((b) => b.id === id)?.name ?? `#${id}`)
|
||||||
|
.slice(0, 4)
|
||||||
|
const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : ""
|
||||||
|
list.push({
|
||||||
|
label: "DESIGNER · " + labels.join(", ") + more,
|
||||||
|
clear: () => { this.selectedBrands = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.selectedCt.length) {
|
||||||
|
list.push({
|
||||||
|
label: "COLLECTION · " + this.selectedCt.join("+").toUpperCase(),
|
||||||
|
clear: () => { this.selectedCt = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.selectedSeason.length) {
|
||||||
|
list.push({
|
||||||
|
label: "SEASON · " + this.selectedSeason.join("+").toUpperCase(),
|
||||||
|
clear: () => { this.selectedSeason = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.selectedYear.length) {
|
||||||
|
list.push({
|
||||||
|
label: "YEAR · " + [...this.selectedYear].sort().join("+"),
|
||||||
|
clear: () => { this.selectedYear = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
},
|
||||||
|
|
||||||
|
get loadBtnText() {
|
||||||
|
if (this.loadingMore) return "Loading…"
|
||||||
|
if (this.isEnd) return "— End of Archive —"
|
||||||
|
return "Load More ↓"
|
||||||
|
},
|
||||||
|
|
||||||
|
// 事件处理方法
|
||||||
|
toggleGroup(group: "designers" | "collection" | "season" | "year") {
|
||||||
|
this.collapsedGroups[group] = !this.collapsedGroups[group]
|
||||||
|
localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "")
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBrand(id: number) {
|
||||||
|
const idx = this.selectedBrands.indexOf(id)
|
||||||
|
if (idx >= 0) this.selectedBrands.splice(idx, 1)
|
||||||
|
else this.selectedBrands.push(id)
|
||||||
|
this.refreshFilter()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshFilter() {
|
||||||
|
this.loadPage(1, false)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 品牌筛选弹窗方法 =====
|
||||||
|
async openBrandModal() {
|
||||||
|
this.modalSelected = [...this.selectedBrands]
|
||||||
|
this.modalKeyword = ""
|
||||||
|
this.modalLetter = ""
|
||||||
|
this.modalOpen = true
|
||||||
|
await this.modalLoadHot()
|
||||||
|
},
|
||||||
|
closeBrandModal() {
|
||||||
|
this.modalOpen = false
|
||||||
|
},
|
||||||
|
async modalLoadHot() {
|
||||||
|
this.modalLoading = true
|
||||||
|
try {
|
||||||
|
const list = await getBrands({ featured: 1, size: 50 })
|
||||||
|
this.modalResults = list.length ? list : this.allBrands.slice(0, 50)
|
||||||
|
} catch {
|
||||||
|
this.modalResults = this.allBrands.slice(0, 50)
|
||||||
|
} finally {
|
||||||
|
this.modalLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async modalSearch() {
|
||||||
|
const kw = this.modalKeyword.trim()
|
||||||
|
if (kw) {
|
||||||
|
this.modalLetter = ""
|
||||||
|
this.modalLoading = true
|
||||||
|
try {
|
||||||
|
this.modalResults = await getBrands({ keyword: kw, size: 60 })
|
||||||
|
} catch {
|
||||||
|
this.modalResults = []
|
||||||
|
} finally {
|
||||||
|
this.modalLoading = false
|
||||||
|
}
|
||||||
|
} else if (this.modalLetter) {
|
||||||
|
await this.modalPickLetter(this.modalLetter)
|
||||||
|
} else {
|
||||||
|
await this.modalLoadHot()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async modalPickLetter(l: string) {
|
||||||
|
this.modalLetter = l
|
||||||
|
this.modalKeyword = ""
|
||||||
|
this.modalLoading = true
|
||||||
|
try {
|
||||||
|
const letter = l === "#" ? "OTHER" : l
|
||||||
|
this.modalResults = await getBrands({ letter, size: 200 })
|
||||||
|
} catch {
|
||||||
|
this.modalResults = []
|
||||||
|
} finally {
|
||||||
|
this.modalLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleModalBrand(id: number) {
|
||||||
|
const idx = this.modalSelected.indexOf(id)
|
||||||
|
if (idx >= 0) this.modalSelected.splice(idx, 1)
|
||||||
|
else {
|
||||||
|
if (this.modalSelected.length >= 5) return
|
||||||
|
this.modalSelected.push(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmBrandModal() {
|
||||||
|
this.selectedBrands = [...this.modalSelected]
|
||||||
|
this.modalOpen = false
|
||||||
|
this.refreshFilter()
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.selectedBrands = []
|
||||||
|
this.selectedCt = []
|
||||||
|
this.selectedSeason = []
|
||||||
|
this.selectedYear = []
|
||||||
|
this.keyword = ""
|
||||||
|
this.refreshFilter()
|
||||||
|
},
|
||||||
|
|
||||||
|
// API 加载
|
||||||
|
async loadPage(page: number, append = false) {
|
||||||
|
const myToken = ++this.reqToken
|
||||||
|
if (append) {
|
||||||
|
this.loadingMore = true
|
||||||
|
} else {
|
||||||
|
this.loading = true
|
||||||
|
this.items = []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = await getArticles({
|
||||||
|
page,
|
||||||
|
size: this.size,
|
||||||
|
sort: "newest",
|
||||||
|
brand_ids: this.selectedBrands,
|
||||||
|
collection_type: this.selectedCt,
|
||||||
|
season: this.selectedSeason,
|
||||||
|
year: this.selectedYear,
|
||||||
|
with_images: 6,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (myToken !== this.reqToken) return
|
||||||
|
|
||||||
|
const rows = json.data.map((item: ArticleItem) => ({ ...item, _imgLoaded: false }))
|
||||||
|
this.total = json.total
|
||||||
|
this.page = page
|
||||||
|
this.items = append ? [...this.items, ...rows] : rows
|
||||||
|
|
||||||
|
this.isEnd = page >= json.last_page || rows.length < this.size
|
||||||
|
} catch {
|
||||||
|
if (myToken !== this.reqToken) return
|
||||||
|
if (!append) this.items = []
|
||||||
|
} finally {
|
||||||
|
if (myToken === this.reqToken) {
|
||||||
|
this.loading = false
|
||||||
|
this.loadingMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 辅助辅助工具函数
|
||||||
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
|
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
||||||
|
fmtDate(ts: number) {
|
||||||
|
if (!ts) return ""
|
||||||
|
const d = new Date(ts * 1000)
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`
|
||||||
|
},
|
||||||
|
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 || "")
|
||||||
|
},
|
||||||
|
getCardSlots(it: ArticleItem) {
|
||||||
|
const gallery = it.gallery || []
|
||||||
|
const totalCount = it.image_count || 0
|
||||||
|
const moreLabel = totalCount > 30 ? "30+" : `${totalCount || 0}+`
|
||||||
|
const slots = []
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const src = gallery[i]
|
||||||
|
const isLast = i === 4
|
||||||
|
slots.push({ src, loaded: false, isLast, moreLabel: (isLast && totalCount > 0) ? moreLabel : "" })
|
||||||
|
}
|
||||||
|
return slots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
864
src/pages/en/shows.astro
Normal file
864
src/pages/en/shows.astro
Normal file
@ -0,0 +1,864 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
import { getBrands, getYears, type BrandEntry } from "../../lib/api"
|
||||||
|
|
||||||
|
let sidebarBrands: BrandEntry[] = []
|
||||||
|
let yearChips: number[] = []
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
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
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="All Shows — Portfolio Studio">
|
||||||
|
<div class="showsroot w-full" x-data="showsPage()">
|
||||||
|
<!-- 顶部:极简眉头 -->
|
||||||
|
<header class="showshead">
|
||||||
|
<div>
|
||||||
|
<nav class="showscrumb" aria-label="Breadcrumb">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<span class="sep" aria-hidden="true">/</span>
|
||||||
|
<a href="/shows">All Shows</a>
|
||||||
|
<span class="sep" aria-hidden="true">/</span>
|
||||||
|
<span class="cur" aria-current="page" x-text="crumbYear"></span>
|
||||||
|
</nav>
|
||||||
|
<h1 class="showsh1">All Shows</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 已激活筛选 pills -->
|
||||||
|
<div class="showspills" x-show="pills.length > 0" x-cloak>
|
||||||
|
<template x-for="(p, i) in pills" :key="i">
|
||||||
|
<span class="showspill">
|
||||||
|
<span x-text="p.label"></span>
|
||||||
|
<button class="x" type="button" @click="p.clear()">×</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体:左 sidebar + 右 4 列商品 grid -->
|
||||||
|
<div class="showsbody">
|
||||||
|
<!-- LEFT SIDEBAR -->
|
||||||
|
<aside class="showsaside">
|
||||||
|
<div class="showsaside-head">
|
||||||
|
<h2 class="showsaside-h">REFINE</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="showsaside-body">
|
||||||
|
<!-- Designer 多选 -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('designers')">
|
||||||
|
<span>DESIGNERS</span>
|
||||||
|
<span class="showsg-right">
|
||||||
|
<span class="showsg-count" x-text="selectedBrands.length">0</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.designers }">
|
||||||
|
<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" x-show="!collapsedGroups.designers">
|
||||||
|
<input class="showssearch" placeholder="Search designer…" autocomplete="off" x-model.debounce.150ms="keyword" />
|
||||||
|
<ul class="showschecks">
|
||||||
|
<li>
|
||||||
|
<!-- Letter Header -->
|
||||||
|
<template x-if="item.type === 'letter'">
|
||||||
|
<div class="letter-head" x-text="item.letter"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Brand Item -->
|
||||||
|
<template x-if="item.type === 'brand'">
|
||||||
|
<label class="flex items-center gap-2 w-full cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:value="item.id"
|
||||||
|
:checked="selectedBrands.includes(item.id)"
|
||||||
|
@change="toggleBrand(item.id)"
|
||||||
|
>
|
||||||
|
<span x-text="item.name"></span>
|
||||||
|
<span class="ct" x-text="item.count ? item.count : ''"></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="showsmore" @click="openBrandModal()" x-text="'浏览全部品牌 +'"></a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Collection type -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('collection')">
|
||||||
|
<span>COLLECTION</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.collection }">
|
||||||
|
<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" x-show="!collapsedGroups.collection">
|
||||||
|
<ul class="showschecks showschecks--flat">
|
||||||
|
<li><label><input type="checkbox" value="rtw" x-model="selectedCt" @change="refreshFilter()" /> <span>Ready-to-Wear</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="menswear" x-model="selectedCt" @change="refreshFilter()" /> <span>Menswear</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="couture" x-model="selectedCt" @change="refreshFilter()" /> <span>Couture</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="resort" x-model="selectedCt" @change="refreshFilter()" /> <span>Resort</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="pre_fall" x-model="selectedCt" @change="refreshFilter()" /> <span>Pre-Fall</span></label></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Season -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('season')">
|
||||||
|
<span>SEASON</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.season }">
|
||||||
|
<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" x-show="!collapsedGroups.season">
|
||||||
|
<ul class="showschecks showschecks--flat">
|
||||||
|
<li><label><input type="checkbox" value="spring" x-model="selectedSeason" @change="refreshFilter()" /> <span>Spring / Summer</span></label></li>
|
||||||
|
<li><label><input type="checkbox" value="fall" x-model="selectedSeason" @change="refreshFilter()" /> <span>Fall / Winter</span></label></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Year -->
|
||||||
|
<section class="showsg">
|
||||||
|
<header class="showsg-h" @click="toggleGroup('year')">
|
||||||
|
<span>YEAR</span>
|
||||||
|
<span class="showsg-chev" :class="{ 'is-collapsed': collapsedGroups.year }">
|
||||||
|
<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" x-show="!collapsedGroups.year">
|
||||||
|
<ul class="showschecks showschecks--flat">
|
||||||
|
{yearChips.map((y) => (
|
||||||
|
<li><label><input type="checkbox" value={String(y)} x-model="selectedYear" @change="refreshFilter()" /> <span>{y}</span></label></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button class="showsclear" type="button" @click="clearAll()">Clear all ×</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- MAIN GRID -->
|
||||||
|
<main class="showsmain">
|
||||||
|
<div class="showsgrid" :class="{ 'is-loading': loading }">
|
||||||
|
|
||||||
|
<!-- 骨架屏:结构与真实卡片完全一致,loading 期间占位,杜绝 加载中→数据完成 的尺寸抖动 -->
|
||||||
|
<template x-if="loading && items.length === 0">
|
||||||
|
<template x-for="i in skeletonCount" :key="'skel-'+i">
|
||||||
|
<div class="showscard showscard--skel" aria-hidden="true">
|
||||||
|
<div class="showscard-left">
|
||||||
|
<div class="showscard-img"></div>
|
||||||
|
</div>
|
||||||
|
<div class="showscard-right">
|
||||||
|
<div class="showscard-info">
|
||||||
|
<div class="showsline h-[41px] mb-[10px] rounded-[2px]"></div>
|
||||||
|
<div class="showsline h-[29px] mb-[14px] rounded-[2px]"></div>
|
||||||
|
<div class="showsline h-[38px] rounded-[2px]"></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>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<template x-if="!loading && items.length === 0">
|
||||||
|
<div class="showsempty">No shows match the current filters</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Card List -->
|
||||||
|
<template x-for="it in items" :key="it.id">
|
||||||
|
<a class="showscard" :href="`/article?id=${it.id}`" :aria-label="`${it.brand_name} ${it.title}`">
|
||||||
|
<div class="showscard-left">
|
||||||
|
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||||
|
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||||
|
<img :src="toAbsUrl(it.cover)" :alt="it.title" loading="lazy" @load="it._imgLoaded = true" @error="it._imgLoaded = true" :class="{ 'is-loaded': it._imgLoaded }" />
|
||||||
|
<!-- <span class="showsbadge" x-show="isNewAdded(it.published_at)">New</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="showscard-right">
|
||||||
|
<div class="showscard-info">
|
||||||
|
<h3 class="showscard-title" x-text="it.title"></h3>
|
||||||
|
<p class="showscard-meta" x-text="`${it.season_code || it.year || ''} · ${ctLabel(it.collection_type)} · ${it.brand_name}`"></p>
|
||||||
|
<div class="showscard-meta-foot">
|
||||||
|
<span class="showscard-meta-line"><span x-html="ICON_USER"></span><span x-text="it.brand_name"></span></span>
|
||||||
|
<span class="showscard-meta-line"><span x-html="ICON_DATE"></span><span x-text="fmtDate(it.published_at)"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="showscard-strip">
|
||||||
|
<template x-for="(slot, idx) in getCardSlots(it)" :key="idx">
|
||||||
|
<div class="showscard-thumb" :class="{ 'placeholder': !slot.src, 'is-loaded': slot.loaded }">
|
||||||
|
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG" x-show="slot.src"></span>
|
||||||
|
<img x-show="slot.src" :src="toAbsUrl(slot.src)" alt="" loading="lazy" @load="slot.loaded = true" @error="slot.loaded = true" :class="{ 'is-loaded': slot.loaded }" />
|
||||||
|
<span class="showscard-thumb-more" x-show="slot.isLast && slot.moreLabel" x-text="slot.moreLabel"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="showsloadmore-wrap" x-show="items.length > 0">
|
||||||
|
<button class="showsloadmore" type="button" :disabled="loadingMore || isEnd" @click="loadPage(page + 1, true)" x-text="loadBtnText"></button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 品牌筛选弹窗:搜索 + A-Z 跳转 + 热门推荐,按需加载,避免一次拉全量 -->
|
||||||
|
<div class="showsmodal" x-show="modalOpen" x-cloak>
|
||||||
|
<div class="showsmodal-mask" @click="closeBrandModal()"></div>
|
||||||
|
<div class="showsmodal-panel">
|
||||||
|
<header class="showsmodal-head">
|
||||||
|
<h3>全部品牌</h3>
|
||||||
|
<button class="showsmodal-x" type="button" @click="closeBrandModal()" aria-label="关闭">✕</button>
|
||||||
|
</header>
|
||||||
|
<div class="showsmodal-search">
|
||||||
|
<input
|
||||||
|
class="showsmodal-input"
|
||||||
|
type="text"
|
||||||
|
x-model="modalKeyword"
|
||||||
|
@input.debounce.300ms="modalSearch()"
|
||||||
|
placeholder="搜索品牌名…"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="showsmodal-az">
|
||||||
|
<template x-for="l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('')" :key="l">
|
||||||
|
<button class="az" type="button" :class="{ active: modalLetter === l }" @click="modalPickLetter(l)" x-text="l"></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="showsmodal-body">
|
||||||
|
<div class="showsmodal-hint" x-show="!modalKeyword && !modalLetter">热门推荐</div>
|
||||||
|
<div class="showsmodal-loading" x-show="modalLoading" x-html="SPIN_SVG"></div>
|
||||||
|
<ul class="showsmodal-list">
|
||||||
|
<template x-for="b in modalResults" :key="b.id">
|
||||||
|
<li>
|
||||||
|
<label class="showsmodal-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:value="b.id"
|
||||||
|
:checked="modalSelected.includes(b.id)"
|
||||||
|
:disabled="!modalSelected.includes(b.id) && modalMaxReached"
|
||||||
|
@change="toggleModalBrand(b.id)"
|
||||||
|
/>
|
||||||
|
<span class="showsmodal-name" x-text="b.name"></span>
|
||||||
|
<span class="showsmodal-ct" x-text="b.article_count ? b.article_count : ''"></span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li class="showsmodal-empty" x-show="!modalLoading && modalResults.length === 0">未找到匹配的品牌</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<footer class="showsmodal-foot">
|
||||||
|
<span class="showsmodal-counter" x-text="`已选 ${modalSelected.length} / 最多 5 个`"></span>
|
||||||
|
<button class="showsmodal-ok" type="button" @click="confirmBrandModal()">确定</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@reference "tailwindcss";
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
|
||||||
|
|
||||||
|
/* 使用 @apply 精简并重构样式 */
|
||||||
|
.showsroot {
|
||||||
|
@apply m-auto max-w-[1600px] pt-7 text-[#0a0a0a];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showshead {
|
||||||
|
@apply flex justify-between items-end gap-6 py-5 pb-[26px] ;
|
||||||
|
}
|
||||||
|
.showscrumb {
|
||||||
|
@apply inline-flex items-center gap-[10px] mb-[10px] text-[11px] tracking-[0.18em] uppercase text-[#999] no-underline;
|
||||||
|
}
|
||||||
|
.showscrumb a {
|
||||||
|
@apply text-black no-underline transition-colors duration-150 hover:text-[#888];
|
||||||
|
}
|
||||||
|
.showscrumb .sep {
|
||||||
|
@apply text-[#cfcfcf] select-none;
|
||||||
|
}
|
||||||
|
.showscrumb .cur {
|
||||||
|
@apply text-[#999];
|
||||||
|
}
|
||||||
|
.showsh1 {
|
||||||
|
@apply text-[clamp(40px,6vw,72px)] leading-[0.9] font-black tracking-[-0.02em] my-0 mb-[10px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showspills {
|
||||||
|
@apply flex flex-wrap gap-2 pt-3.5;
|
||||||
|
}
|
||||||
|
.showspill {
|
||||||
|
@apply inline-flex items-center gap-2 border-[1.5px] border-black p-[6px_10px] text-[10.5px] uppercase tracking-[0.14em] text-black bg-white;
|
||||||
|
}
|
||||||
|
.showspill .x {
|
||||||
|
@apply inline-flex w-3.5 h-3.5 items-center justify-center border-none bg-black text-white cursor-pointer text-xs leading-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsbody {
|
||||||
|
@apply flex gap-[36px] items-start max-md:flex-col max-md:gap-[18px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsaside {
|
||||||
|
@apply flex-none w-[240px] bg-white sticky top-5 max-h-[calc(100vh-40px)] rounded-none transition-[flex-basis] duration-250 ease-in-out flex flex-col max-md:static max-md:max-h-none max-md:w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsaside-head {
|
||||||
|
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
||||||
|
}
|
||||||
|
.showsaside-h {
|
||||||
|
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none transition-opacity duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsaside-body {
|
||||||
|
@apply p-[14px_14px_18px] overflow-y-auto flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#ccc_#fff];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsg {
|
||||||
|
@apply border-t border-[#e6e6e6] py-[12px] pb-[6px] first-of-type:border-t-0 first-of-type:pt-1;
|
||||||
|
}
|
||||||
|
.showsg-h {
|
||||||
|
@apply flex justify-between items-center text-[11px] tracking-[0.22em] uppercase font-bold text-black mb-2 cursor-pointer select-none;
|
||||||
|
}
|
||||||
|
.showsg-right {
|
||||||
|
@apply inline-flex items-center gap-2.5;
|
||||||
|
}
|
||||||
|
.showsg-chev {
|
||||||
|
@apply inline-block text-[9px] leading-none text-[#999] transition-transform duration-200 ml-1;
|
||||||
|
}
|
||||||
|
.showsg-chev.is-collapsed {
|
||||||
|
@apply -rotate-90;
|
||||||
|
}
|
||||||
|
.showsg-count {
|
||||||
|
@apply text-[#999] font-normal text-[10px] tracking-[0.14em];
|
||||||
|
}
|
||||||
|
|
||||||
|
.showssearch {
|
||||||
|
@apply w-full border border-[#d4d4d4] p-[7px_10px] text-[11px] uppercase tracking-[0.08em] text-black rounded-none outline-none bg-[#fafafa] focus:border-black focus:bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschecks {
|
||||||
|
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin];
|
||||||
|
}
|
||||||
|
.showschecks li {
|
||||||
|
@apply flex items-center gap-2 py-[5px] text-[11.5px] uppercase tracking-[0.04em] text-black cursor-pointer select-none hover:bg-[#f6f6f6];
|
||||||
|
}
|
||||||
|
.showschecks li.letter-head {
|
||||||
|
@apply p-[8px_0_3px] text-[10px] tracking-[0.22em] text-[#8a8a8a] cursor-default pointer-events-none border-t border-dotted border-[#e6e6e6] first:border-t-0 first:pt-1;
|
||||||
|
}
|
||||||
|
.showschecks input[type="checkbox"] {
|
||||||
|
@apply appearance-none w-3 h-3 border-[1.5px] border-black bg-white cursor-pointer shrink-0 rounded-none inline-grid place-content-center checked:bg-black;
|
||||||
|
}
|
||||||
|
.showschecks input[type="checkbox"]:checked::after {
|
||||||
|
content: "";
|
||||||
|
@apply w-[6px] h-[6px] bg-white;
|
||||||
|
}
|
||||||
|
.showschecks .ct {
|
||||||
|
@apply text-[#999] text-[10px] ml-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showschecks--flat li {
|
||||||
|
@apply py-[4px];
|
||||||
|
}
|
||||||
|
.showschecks--flat li label {
|
||||||
|
@apply inline-flex items-center gap-2 cursor-pointer w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsmore {
|
||||||
|
@apply inline-block mt-[10px] text-[10.5px] uppercase tracking-[0.16em] text-[#555] cursor-pointer hover:text-black;
|
||||||
|
}
|
||||||
|
.showsclear {
|
||||||
|
@apply mt-[18px] w-full border-[1.5px] border-black bg-white text-black p-[11px_12px] text-[11px] uppercase tracking-[0.22em] font-bold cursor-pointer rounded-none transition-all duration-150 hover:bg-black hover:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsmain {
|
||||||
|
@apply flex-1 min-w-0;
|
||||||
|
}
|
||||||
|
.showsgrid {
|
||||||
|
@apply grid grid-cols-2 gap-[22px] relative max-[560px]:grid-cols-1;
|
||||||
|
}
|
||||||
|
.showsgrid.is-loading::before {
|
||||||
|
content: "";
|
||||||
|
@apply absolute -top-1 left-0 h-[2px] w-[38%] bg-black z-10;
|
||||||
|
animation: hbxProg 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard {
|
||||||
|
@apply relative flex gap-[14px] no-underline text-inherit bg-white p-3 transition-colors duration-200 hover:border-black max-[700px]:flex-col max-[700px]:gap-[10px];
|
||||||
|
}
|
||||||
|
.showscard:hover .showscard-img img {
|
||||||
|
@apply scale-104;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard-left {
|
||||||
|
@apply flex-none w-[30%] min-w-[110px] max-[700px]:w-full max-[700px]:flex-none;
|
||||||
|
}
|
||||||
|
.showscard-img {
|
||||||
|
@apply relative w-full aspect-[3/4] bg-[#f1f1f1] overflow-hidden rounded-none;
|
||||||
|
}
|
||||||
|
.showscard-img img {
|
||||||
|
@apply w-full h-full object-cover transition-transform duration-600 opacity-0;
|
||||||
|
}
|
||||||
|
.showscard-img img.is-loaded {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsspin {
|
||||||
|
@apply absolute inset-0 flex items-center justify-center text-[#bdbdbd] opacity-100 transition-opacity duration-300 pointer-events-none z-10;
|
||||||
|
}
|
||||||
|
.showscard-img.is-loaded .showsspin,
|
||||||
|
.showscard-thumb.is-loaded .showsspin {
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsbadge {
|
||||||
|
@apply absolute top-[6px] right-[6px] text-[9px] tracking-[0.14em] uppercase font-bold text-white bg-black p-[3px_6px];
|
||||||
|
}
|
||||||
|
.showsflag-featured {
|
||||||
|
@apply absolute top-[6px] left-[6px] text-[9px] tracking-[0.14em] uppercase font-bold text-black bg-white p-[3px_6px] border border-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard-right {
|
||||||
|
@apply flex-1 min-w-0 flex flex-col justify-between max-[700px]:w-full;
|
||||||
|
}
|
||||||
|
.showscard-info {
|
||||||
|
@apply flex-none min-w-0 flex flex-col p-[2px_2px_0];
|
||||||
|
}
|
||||||
|
.showscard-title {
|
||||||
|
@apply text-[15.5px] font-bold leading-[1.32] my-0 mb-[10px] text-[#111] tracking-[-0.01em] line-clamp-2 h-[41px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-meta {
|
||||||
|
@apply text-[9.5px] text-[#888] my-0 mb-[14px] leading-[1.55] tracking-[0.04em] uppercase line-clamp-2 h-[29px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-meta-foot {
|
||||||
|
@apply flex flex-col gap-[6px] h-[38px] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-meta-line {
|
||||||
|
@apply inline-flex items-center gap-[6px] text-[10.5px] tracking-[0.06em] uppercase text-[#444] h-[16px] leading-[16px] overflow-hidden whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard-strip {
|
||||||
|
@apply grid grid-cols-5 gap-1 mt-[10px];
|
||||||
|
}
|
||||||
|
.showscard-thumb {
|
||||||
|
@apply relative aspect-[3/4] bg-[#f1f1f1] overflow-hidden;
|
||||||
|
}
|
||||||
|
.showscard-thumb img {
|
||||||
|
@apply w-full h-full object-cover opacity-0 transition-opacity duration-450 ease-in-out;
|
||||||
|
}
|
||||||
|
.showscard-thumb img.is-loaded {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
.showscard-thumb.placeholder {
|
||||||
|
@apply bg-[#f6f6f6];
|
||||||
|
}
|
||||||
|
.showscard-thumb-more {
|
||||||
|
@apply absolute inset-0 bg-black/55 text-white text-[11px] font-bold tracking-[0.04em] flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showscard--skel .showscard-img,
|
||||||
|
.showscard--skel .showscard-thumb,
|
||||||
|
.showscard--skel .showsline {
|
||||||
|
@apply bg-[#efefef];
|
||||||
|
animation: hbxShim 1.6s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsloadmore-wrap {
|
||||||
|
@apply flex justify-center mt-[38px];
|
||||||
|
}
|
||||||
|
.showsloadmore {
|
||||||
|
@apply border-[1.5px] border-black bg-white text-black p-[13px_36px] text-[11px] uppercase tracking-[0.22em] font-bold cursor-pointer rounded-none transition-all duration-200 hover:bg-black hover:text-white disabled:opacity-40 disabled:cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsempty {
|
||||||
|
@apply col-span-full p-[80px_20px] text-center text-xs tracking-[0.22em] uppercase text-[#999] border border-dashed border-[#ccc];
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||||
|
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||||
|
/* ===== 品牌筛选弹窗 ===== */
|
||||||
|
.showsmodal {
|
||||||
|
@apply fixed inset-0 z-[60] flex items-center justify-center p-[16px];
|
||||||
|
}
|
||||||
|
.showsmodal-mask {
|
||||||
|
@apply absolute inset-0 bg-black/45;
|
||||||
|
}
|
||||||
|
.showsmodal-panel {
|
||||||
|
@apply relative w-full max-w-[680px] max-h-[86vh] bg-white flex flex-col border border-black;
|
||||||
|
}
|
||||||
|
.showsmodal-head {
|
||||||
|
@apply flex items-center justify-between px-[16px] py-[13px] border-b border-black shrink-0;
|
||||||
|
}
|
||||||
|
.showsmodal-head h3 {
|
||||||
|
@apply m-0 text-[13px] tracking-[0.2em] uppercase font-bold;
|
||||||
|
}
|
||||||
|
.showsmodal-x {
|
||||||
|
@apply appearance-none bg-white border border-black w-[24px] h-[24px] inline-flex items-center justify-center cursor-pointer text-[12px] leading-none;
|
||||||
|
}
|
||||||
|
.showsmodal-x:hover { @apply bg-black text-white; }
|
||||||
|
.showsmodal-search { @apply px-[16px] pt-[12px] pb-[8px] shrink-0; }
|
||||||
|
.showsmodal-input {
|
||||||
|
@apply w-full border border-black px-[10px] py-[8px] text-[13px] outline-none;
|
||||||
|
}
|
||||||
|
.showsmodal-input:focus { @apply ring-1 ring-black; }
|
||||||
|
.showsmodal-az {
|
||||||
|
@apply flex flex-wrap gap-[4px] px-[16px] pb-[10px] border-b border-[#e6e6e6] shrink-0;
|
||||||
|
}
|
||||||
|
.showsmodal-az .az {
|
||||||
|
@apply appearance-none bg-white border border-[#ccc] w-[22px] h-[22px] text-[11px] cursor-pointer leading-none;
|
||||||
|
}
|
||||||
|
.showsmodal-az .az.active { @apply bg-black text-white border-black; }
|
||||||
|
.showsmodal-body { @apply flex-1 min-h-0 overflow-y-auto px-[16px] py-[8px]; }
|
||||||
|
.showsmodal-hint {
|
||||||
|
@apply text-[11px] tracking-[0.2em] uppercase text-[#888] py-[6px] pb-[8px];
|
||||||
|
}
|
||||||
|
.showsmodal-loading { @apply flex justify-center py-[28px] text-[#bdbdbd]; }
|
||||||
|
.showsmodal-list { @apply list-none m-0 p-0; }
|
||||||
|
.showsmodal-row {
|
||||||
|
@apply flex items-center gap-[8px] w-full cursor-pointer py-[7px] border-b border-[#f0f0f0] text-[13px];
|
||||||
|
}
|
||||||
|
.showsmodal-name { @apply flex-1 truncate; }
|
||||||
|
.showsmodal-ct { @apply text-[#999] text-[11px] ml-auto; }
|
||||||
|
.showsmodal-empty { @apply py-[28px] text-center text-[#999] text-[13px]; }
|
||||||
|
.showsmodal-foot {
|
||||||
|
@apply flex items-center justify-between px-[16px] py-[12px] border-t border-black shrink-0 text-[12px];
|
||||||
|
}
|
||||||
|
.showsmodal-ok {
|
||||||
|
@apply appearance-none bg-black text-white border border-black px-[22px] py-[8px] cursor-pointer text-[12px] tracking-[0.1em];
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Alpine from "alpinejs"
|
||||||
|
import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "../lib/api"
|
||||||
|
|
||||||
|
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 SPIN_SVG = `<svg viewBox="0 0 24 24" aria-hidden="true" class="w-[22px] h-[22px] block"><g class="ring origin-[12px_12px] animate-spin" 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>`
|
||||||
|
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("showsPage", () => {
|
||||||
|
const node = document.getElementById("showsbrands-data")
|
||||||
|
const allBrands: BrandEntry[] = node?.textContent ? JSON.parse(node.textContent) : []
|
||||||
|
|
||||||
|
return {
|
||||||
|
allBrands,
|
||||||
|
ICON_USER,
|
||||||
|
ICON_DATE,
|
||||||
|
SPIN_SVG,
|
||||||
|
collapsedGroups: {
|
||||||
|
designers: localStorage.getItem("showsg-designers") === "1",
|
||||||
|
collection: localStorage.getItem("showsg-collection") === "1",
|
||||||
|
season: localStorage.getItem("showsg-season") === "1",
|
||||||
|
year: localStorage.getItem("showsg-year") === "1",
|
||||||
|
},
|
||||||
|
selectedBrands: [] as number[],
|
||||||
|
selectedCt: [] as string[],
|
||||||
|
selectedSeason: [] as string[],
|
||||||
|
selectedYear: [] as string[],
|
||||||
|
keyword: "",
|
||||||
|
brandsExpanded: false,
|
||||||
|
|
||||||
|
// 品牌筛选弹窗状态
|
||||||
|
modalOpen: false,
|
||||||
|
modalKeyword: "",
|
||||||
|
modalLetter: "",
|
||||||
|
modalResults: [] as BrandEntry[],
|
||||||
|
modalLoading: false,
|
||||||
|
modalSelected: [] as number[],
|
||||||
|
get modalMaxReached() {
|
||||||
|
return this.modalSelected.length >= 5
|
||||||
|
},
|
||||||
|
|
||||||
|
items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
page: 1,
|
||||||
|
size: 24,
|
||||||
|
total: 0,
|
||||||
|
isEnd: false,
|
||||||
|
reqToken: 0,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.loadPage(1, false)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算属性:侧边栏设计师列表的分组渲染
|
||||||
|
get visibleBrandItems() {
|
||||||
|
const kw = this.keyword.trim().toLowerCase()
|
||||||
|
const result: any[] = []
|
||||||
|
|
||||||
|
if (kw) {
|
||||||
|
this.allBrands.forEach((b) => {
|
||||||
|
if (b.name.toLowerCase().includes(kw)) {
|
||||||
|
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const map = new Map<string, BrandEntry[]>()
|
||||||
|
for (const b of this.allBrands) {
|
||||||
|
const letter = (b.name[0] || "?").toUpperCase()
|
||||||
|
if (!map.has(letter)) map.set(letter, [])
|
||||||
|
map.get(letter)!.push(b)
|
||||||
|
}
|
||||||
|
const grouped = [...map.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
|
||||||
|
let shown = 0
|
||||||
|
const MAX_INITIAL = 60
|
||||||
|
for (const [letter, list] of grouped) {
|
||||||
|
if (!this.brandsExpanded && shown >= MAX_INITIAL) break
|
||||||
|
result.push({ type: "letter", letter, key: `letter-${letter}` })
|
||||||
|
for (const b of list) {
|
||||||
|
if (!this.brandsExpanded && shown >= MAX_INITIAL) break
|
||||||
|
result.push({ type: "brand", id: b.id, name: b.name, count: b.article_count, key: b.id })
|
||||||
|
shown++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
get hasMoreBrands() {
|
||||||
|
return this.allBrands.length > 60
|
||||||
|
},
|
||||||
|
|
||||||
|
// 面包屑末级:未选年份时显示默认的「2026 Edition」,选中后显示对应年份
|
||||||
|
get crumbYear() {
|
||||||
|
if (this.selectedYear.length) {
|
||||||
|
const ys = [...this.selectedYear].sort()
|
||||||
|
return ys.length === 1 ? `${ys[0]} Edition` : `${ys.join(" / ")}`
|
||||||
|
}
|
||||||
|
return "2026 Edition"
|
||||||
|
},
|
||||||
|
|
||||||
|
// 骨架屏数量与首屏 data 数量一致,保证「加载中」与「数据完成」两阶段的网格高度相同,消除抖动
|
||||||
|
get skeletonCount() {
|
||||||
|
return this.size
|
||||||
|
},
|
||||||
|
|
||||||
|
// 已筛选选项 Pills 视图数据
|
||||||
|
get pills() {
|
||||||
|
const list: { label: string; clear: () => void }[] = []
|
||||||
|
if (this.selectedBrands.length) {
|
||||||
|
const labels = this.selectedBrands
|
||||||
|
.map((id) => this.allBrands.find((b) => b.id === id)?.name ?? `#${id}`)
|
||||||
|
.slice(0, 4)
|
||||||
|
const more = this.selectedBrands.length > 4 ? ` +${this.selectedBrands.length - 4}` : ""
|
||||||
|
list.push({
|
||||||
|
label: "DESIGNER · " + labels.join(", ") + more,
|
||||||
|
clear: () => { this.selectedBrands = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.selectedCt.length) {
|
||||||
|
list.push({
|
||||||
|
label: "COLLECTION · " + this.selectedCt.join("+").toUpperCase(),
|
||||||
|
clear: () => { this.selectedCt = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.selectedSeason.length) {
|
||||||
|
list.push({
|
||||||
|
label: "SEASON · " + this.selectedSeason.join("+").toUpperCase(),
|
||||||
|
clear: () => { this.selectedSeason = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.selectedYear.length) {
|
||||||
|
list.push({
|
||||||
|
label: "YEAR · " + [...this.selectedYear].sort().join("+"),
|
||||||
|
clear: () => { this.selectedYear = []; this.refreshFilter(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
},
|
||||||
|
|
||||||
|
get loadBtnText() {
|
||||||
|
if (this.loadingMore) return "Loading…"
|
||||||
|
if (this.isEnd) return "— End of Archive —"
|
||||||
|
return "Load More ↓"
|
||||||
|
},
|
||||||
|
|
||||||
|
// 事件处理方法
|
||||||
|
toggleGroup(group: "designers" | "collection" | "season" | "year") {
|
||||||
|
this.collapsedGroups[group] = !this.collapsedGroups[group]
|
||||||
|
localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "")
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBrand(id: number) {
|
||||||
|
const idx = this.selectedBrands.indexOf(id)
|
||||||
|
if (idx >= 0) this.selectedBrands.splice(idx, 1)
|
||||||
|
else this.selectedBrands.push(id)
|
||||||
|
this.refreshFilter()
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshFilter() {
|
||||||
|
this.loadPage(1, false)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 品牌筛选弹窗方法 =====
|
||||||
|
async openBrandModal() {
|
||||||
|
this.modalSelected = [...this.selectedBrands]
|
||||||
|
this.modalKeyword = ""
|
||||||
|
this.modalLetter = ""
|
||||||
|
this.modalOpen = true
|
||||||
|
await this.modalLoadHot()
|
||||||
|
},
|
||||||
|
closeBrandModal() {
|
||||||
|
this.modalOpen = false
|
||||||
|
},
|
||||||
|
async modalLoadHot() {
|
||||||
|
this.modalLoading = true
|
||||||
|
try {
|
||||||
|
const list = await getBrands({ featured: 1, size: 50 })
|
||||||
|
this.modalResults = list.length ? list : this.allBrands.slice(0, 50)
|
||||||
|
} catch {
|
||||||
|
this.modalResults = this.allBrands.slice(0, 50)
|
||||||
|
} finally {
|
||||||
|
this.modalLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async modalSearch() {
|
||||||
|
const kw = this.modalKeyword.trim()
|
||||||
|
if (kw) {
|
||||||
|
this.modalLetter = ""
|
||||||
|
this.modalLoading = true
|
||||||
|
try {
|
||||||
|
this.modalResults = await getBrands({ keyword: kw, size: 60 })
|
||||||
|
} catch {
|
||||||
|
this.modalResults = []
|
||||||
|
} finally {
|
||||||
|
this.modalLoading = false
|
||||||
|
}
|
||||||
|
} else if (this.modalLetter) {
|
||||||
|
await this.modalPickLetter(this.modalLetter)
|
||||||
|
} else {
|
||||||
|
await this.modalLoadHot()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async modalPickLetter(l: string) {
|
||||||
|
this.modalLetter = l
|
||||||
|
this.modalKeyword = ""
|
||||||
|
this.modalLoading = true
|
||||||
|
try {
|
||||||
|
const letter = l === "#" ? "OTHER" : l
|
||||||
|
this.modalResults = await getBrands({ letter, size: 200 })
|
||||||
|
} catch {
|
||||||
|
this.modalResults = []
|
||||||
|
} finally {
|
||||||
|
this.modalLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleModalBrand(id: number) {
|
||||||
|
const idx = this.modalSelected.indexOf(id)
|
||||||
|
if (idx >= 0) this.modalSelected.splice(idx, 1)
|
||||||
|
else {
|
||||||
|
if (this.modalSelected.length >= 5) return
|
||||||
|
this.modalSelected.push(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmBrandModal() {
|
||||||
|
this.selectedBrands = [...this.modalSelected]
|
||||||
|
this.modalOpen = false
|
||||||
|
this.refreshFilter()
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.selectedBrands = []
|
||||||
|
this.selectedCt = []
|
||||||
|
this.selectedSeason = []
|
||||||
|
this.selectedYear = []
|
||||||
|
this.keyword = ""
|
||||||
|
this.refreshFilter()
|
||||||
|
},
|
||||||
|
|
||||||
|
// API 加载
|
||||||
|
async loadPage(page: number, append = false) {
|
||||||
|
const myToken = ++this.reqToken
|
||||||
|
if (append) {
|
||||||
|
this.loadingMore = true
|
||||||
|
} else {
|
||||||
|
this.loading = true
|
||||||
|
this.items = []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = await getArticles({
|
||||||
|
page,
|
||||||
|
size: this.size,
|
||||||
|
sort: "newest",
|
||||||
|
brand_ids: this.selectedBrands,
|
||||||
|
collection_type: this.selectedCt,
|
||||||
|
season: this.selectedSeason,
|
||||||
|
year: this.selectedYear,
|
||||||
|
with_images: 6,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (myToken !== this.reqToken) return
|
||||||
|
|
||||||
|
const rows = json.data.map((item: ArticleItem) => ({ ...item, _imgLoaded: false }))
|
||||||
|
this.total = json.total
|
||||||
|
this.page = page
|
||||||
|
this.items = append ? [...this.items, ...rows] : rows
|
||||||
|
|
||||||
|
this.isEnd = page >= json.last_page || rows.length < this.size
|
||||||
|
} catch {
|
||||||
|
if (myToken !== this.reqToken) return
|
||||||
|
if (!append) this.items = []
|
||||||
|
} finally {
|
||||||
|
if (myToken === this.reqToken) {
|
||||||
|
this.loading = false
|
||||||
|
this.loadingMore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 辅助辅助工具函数
|
||||||
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
|
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
||||||
|
fmtDate(ts: number) {
|
||||||
|
if (!ts) return ""
|
||||||
|
const d = new Date(ts * 1000)
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`
|
||||||
|
},
|
||||||
|
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 || "")
|
||||||
|
},
|
||||||
|
getCardSlots(it: ArticleItem) {
|
||||||
|
const gallery = it.gallery || []
|
||||||
|
const totalCount = it.image_count || 0
|
||||||
|
const moreLabel = totalCount > 30 ? "30+" : `${totalCount || 0}+`
|
||||||
|
const slots = []
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const src = gallery[i]
|
||||||
|
const isLast = i === 4
|
||||||
|
slots.push({ src, loaded: false, isLast, moreLabel: (isLast && totalCount > 0) ? moreLabel : "" })
|
||||||
|
}
|
||||||
|
return slots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -20,6 +20,15 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="All Shows — Portfolio Studio">
|
<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 w-full" x-data="showsPage()">
|
<div class="showsroot w-full" x-data="showsPage()">
|
||||||
<!-- 顶部:极简眉头 -->
|
<!-- 顶部:极简眉头 -->
|
||||||
<header class="showshead">
|
<header class="showshead">
|
||||||
@ -27,7 +36,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
<nav class="showscrumb" aria-label="Breadcrumb">
|
<nav class="showscrumb" aria-label="Breadcrumb">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<span class="sep" aria-hidden="true">/</span>
|
<span class="sep" aria-hidden="true">/</span>
|
||||||
<a href="/shows">All Shows</a>
|
<a href="/shows">Runway Archive</a>
|
||||||
<span class="sep" aria-hidden="true">/</span>
|
<span class="sep" aria-hidden="true">/</span>
|
||||||
<span class="cur" aria-current="page" x-text="crumbYear"></span>
|
<span class="cur" aria-current="page" x-text="crumbYear"></span>
|
||||||
</nav>
|
</nav>
|
||||||
@ -48,12 +57,17 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
<!-- 主体:左 sidebar + 右 4 列商品 grid -->
|
<!-- 主体:左 sidebar + 右 4 列商品 grid -->
|
||||||
<div class="showsbody">
|
<div class="showsbody">
|
||||||
<!-- LEFT SIDEBAR -->
|
<!-- LEFT SIDEBAR -->
|
||||||
<aside class="showsaside">
|
<aside class="showsaside" :class="{ 'is-collapsed': asideCollapsed }">
|
||||||
<div class="showsaside-head">
|
<div class="showsaside-head">
|
||||||
<h2 class="showsaside-h">REFINE</h2>
|
<h2 class="showsaside-h">REFINE</h2>
|
||||||
|
<button class="showsaside-toggle" type="button" aria-label="折叠/展开筛选" @click="toggleAside()">
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="showsaside-body">
|
<div class="showsaside-body" x-show="!asideCollapsed">
|
||||||
<!-- Designer 多选 -->
|
<!-- Designer 多选 -->
|
||||||
<section class="showsg">
|
<section class="showsg">
|
||||||
<header class="showsg-h" @click="toggleGroup('designers')">
|
<header class="showsg-h" @click="toggleGroup('designers')">
|
||||||
@ -89,7 +103,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
</template>
|
</template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a class="showsmore" @click="openBrandModal()" x-text="'浏览全部品牌 +'"></a>
|
<a class="showsmore" x-show="hasMoreBrands && !keyword" @click="brandsExpanded = !brandsExpanded" x-text="brandsExpanded ? 'Show fewer ▴' : `Show all ${allBrands.length} designers ▾`"></a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -180,7 +194,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<template x-if="!loading && items.length === 0">
|
<template x-if="!loading && items.length === 0">
|
||||||
<div class="showsempty">No shows match the current filters</div>
|
<div class="showsempty">No shows match the current filters · 暂无符合条件的秀场</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Card List -->
|
<!-- Card List -->
|
||||||
@ -221,59 +235,6 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 品牌筛选弹窗:搜索 + A-Z 跳转 + 热门推荐,按需加载,避免一次拉全量 -->
|
|
||||||
<div class="showsmodal" x-show="modalOpen" x-cloak>
|
|
||||||
<div class="showsmodal-mask" @click="closeBrandModal()"></div>
|
|
||||||
<div class="showsmodal-panel">
|
|
||||||
<header class="showsmodal-head">
|
|
||||||
<h3>全部品牌</h3>
|
|
||||||
<button class="showsmodal-x" type="button" @click="closeBrandModal()" aria-label="关闭">✕</button>
|
|
||||||
</header>
|
|
||||||
<div class="showsmodal-search">
|
|
||||||
<input
|
|
||||||
class="showsmodal-input"
|
|
||||||
type="text"
|
|
||||||
x-model="modalKeyword"
|
|
||||||
@input.debounce.300ms="modalSearch()"
|
|
||||||
placeholder="搜索品牌名…"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="showsmodal-az">
|
|
||||||
<template x-for="l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('')" :key="l">
|
|
||||||
<button class="az" type="button" :class="{ active: modalLetter === l }" @click="modalPickLetter(l)" x-text="l"></button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="showsmodal-body">
|
|
||||||
<div class="showsmodal-hint" x-show="!modalKeyword && !modalLetter">热门推荐</div>
|
|
||||||
<div class="showsmodal-loading" x-show="modalLoading" x-html="SPIN_SVG"></div>
|
|
||||||
<ul class="showsmodal-list">
|
|
||||||
<template x-for="b in modalResults" :key="b.id">
|
|
||||||
<li>
|
|
||||||
<label class="showsmodal-row">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:value="b.id"
|
|
||||||
:checked="modalSelected.includes(b.id)"
|
|
||||||
:disabled="!modalSelected.includes(b.id) && modalMaxReached"
|
|
||||||
@change="toggleModalBrand(b.id)"
|
|
||||||
/>
|
|
||||||
<span class="showsmodal-name" x-text="b.name"></span>
|
|
||||||
<span class="showsmodal-ct" x-text="b.article_count ? b.article_count : ''"></span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
<li class="showsmodal-empty" x-show="!modalLoading && modalResults.length === 0">未找到匹配的品牌</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<footer class="showsmodal-foot">
|
|
||||||
<span class="showsmodal-counter" x-text="`已选 ${modalSelected.length} / 最多 5 个`"></span>
|
|
||||||
<button class="showsmodal-ok" type="button" @click="confirmBrandModal()">确定</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
@ -325,6 +286,10 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
.showsaside {
|
.showsaside {
|
||||||
@apply flex-none w-[240px] bg-white sticky top-5 max-h-[calc(100vh-40px)] rounded-none transition-[flex-basis] duration-250 ease-in-out flex flex-col max-md:static max-md:max-h-none max-md:w-full;
|
@apply flex-none w-[240px] bg-white sticky top-5 max-h-[calc(100vh-40px)] rounded-none transition-[flex-basis] duration-250 ease-in-out flex flex-col max-md:static max-md:max-h-none max-md:w-full;
|
||||||
}
|
}
|
||||||
|
html.showsaside-collapsed .showsaside,
|
||||||
|
.showsaside.is-collapsed {
|
||||||
|
@apply flex-none w-[44px];
|
||||||
|
}
|
||||||
|
|
||||||
.showsaside-head {
|
.showsaside-head {
|
||||||
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
||||||
@ -332,10 +297,25 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
.showsaside-h {
|
.showsaside-h {
|
||||||
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none transition-opacity duration-200;
|
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none transition-opacity duration-200;
|
||||||
}
|
}
|
||||||
|
html.showsaside-collapsed .showsaside-h,
|
||||||
|
.showsaside.is-collapsed .showsaside-h {
|
||||||
|
@apply opacity-0 pointer-events-none w-0 overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showsaside-toggle {
|
||||||
|
@apply appearance-none bg-white border-[1.5px] border-black w-[22px] h-[22px] inline-flex items-center justify-center cursor-pointer rounded-none text-black p-0 transition-transform duration-250 ease-in-out hover:bg-black hover:text-white;
|
||||||
|
}
|
||||||
|
html.showsaside-collapsed .showsaside-toggle,
|
||||||
|
.showsaside.is-collapsed .showsaside-toggle {
|
||||||
|
@apply ml-auto rotate-180;
|
||||||
|
}
|
||||||
|
|
||||||
.showsaside-body {
|
.showsaside-body {
|
||||||
@apply p-[14px_14px_18px] overflow-y-auto flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#ccc_#fff];
|
@apply p-[14px_14px_18px] overflow-y-auto flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#ccc_#fff];
|
||||||
}
|
}
|
||||||
|
html.showsaside-collapsed .showsaside-body {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.showsg {
|
.showsg {
|
||||||
@apply border-t border-[#e6e6e6] py-[12px] pb-[6px] first-of-type:border-t-0 first-of-type:pt-1;
|
@apply border-t border-[#e6e6e6] py-[12px] pb-[6px] first-of-type:border-t-0 first-of-type:pt-1;
|
||||||
@ -499,62 +479,11 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||||
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||||
/* ===== 品牌筛选弹窗 ===== */
|
|
||||||
.showsmodal {
|
|
||||||
@apply fixed inset-0 z-[60] flex items-center justify-center p-[16px];
|
|
||||||
}
|
|
||||||
.showsmodal-mask {
|
|
||||||
@apply absolute inset-0 bg-black/45;
|
|
||||||
}
|
|
||||||
.showsmodal-panel {
|
|
||||||
@apply relative w-full max-w-[680px] max-h-[86vh] bg-white flex flex-col border border-black;
|
|
||||||
}
|
|
||||||
.showsmodal-head {
|
|
||||||
@apply flex items-center justify-between px-[16px] py-[13px] border-b border-black shrink-0;
|
|
||||||
}
|
|
||||||
.showsmodal-head h3 {
|
|
||||||
@apply m-0 text-[13px] tracking-[0.2em] uppercase font-bold;
|
|
||||||
}
|
|
||||||
.showsmodal-x {
|
|
||||||
@apply appearance-none bg-white border border-black w-[24px] h-[24px] inline-flex items-center justify-center cursor-pointer text-[12px] leading-none;
|
|
||||||
}
|
|
||||||
.showsmodal-x:hover { @apply bg-black text-white; }
|
|
||||||
.showsmodal-search { @apply px-[16px] pt-[12px] pb-[8px] shrink-0; }
|
|
||||||
.showsmodal-input {
|
|
||||||
@apply w-full border border-black px-[10px] py-[8px] text-[13px] outline-none;
|
|
||||||
}
|
|
||||||
.showsmodal-input:focus { @apply ring-1 ring-black; }
|
|
||||||
.showsmodal-az {
|
|
||||||
@apply flex flex-wrap gap-[4px] px-[16px] pb-[10px] border-b border-[#e6e6e6] shrink-0;
|
|
||||||
}
|
|
||||||
.showsmodal-az .az {
|
|
||||||
@apply appearance-none bg-white border border-[#ccc] w-[22px] h-[22px] text-[11px] cursor-pointer leading-none;
|
|
||||||
}
|
|
||||||
.showsmodal-az .az.active { @apply bg-black text-white border-black; }
|
|
||||||
.showsmodal-body { @apply flex-1 min-h-0 overflow-y-auto px-[16px] py-[8px]; }
|
|
||||||
.showsmodal-hint {
|
|
||||||
@apply text-[11px] tracking-[0.2em] uppercase text-[#888] py-[6px] pb-[8px];
|
|
||||||
}
|
|
||||||
.showsmodal-loading { @apply flex justify-center py-[28px] text-[#bdbdbd]; }
|
|
||||||
.showsmodal-list { @apply list-none m-0 p-0; }
|
|
||||||
.showsmodal-row {
|
|
||||||
@apply flex items-center gap-[8px] w-full cursor-pointer py-[7px] border-b border-[#f0f0f0] text-[13px];
|
|
||||||
}
|
|
||||||
.showsmodal-name { @apply flex-1 truncate; }
|
|
||||||
.showsmodal-ct { @apply text-[#999] text-[11px] ml-auto; }
|
|
||||||
.showsmodal-empty { @apply py-[28px] text-center text-[#999] text-[13px]; }
|
|
||||||
.showsmodal-foot {
|
|
||||||
@apply flex items-center justify-between px-[16px] py-[12px] border-t border-black shrink-0 text-[12px];
|
|
||||||
}
|
|
||||||
.showsmodal-ok {
|
|
||||||
@apply appearance-none bg-black text-white border border-black px-[22px] py-[8px] cursor-pointer text-[12px] tracking-[0.1em];
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Alpine from "alpinejs"
|
import Alpine from "alpinejs"
|
||||||
import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "../lib/api"
|
import { getArticles, toAbs, type ArticleItem, type BrandEntry } from "../lib/api"
|
||||||
|
|
||||||
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_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_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>`
|
||||||
@ -570,6 +499,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
ICON_USER,
|
ICON_USER,
|
||||||
ICON_DATE,
|
ICON_DATE,
|
||||||
SPIN_SVG,
|
SPIN_SVG,
|
||||||
|
asideCollapsed: localStorage.getItem("showsaside-collapsed") === "1",
|
||||||
collapsedGroups: {
|
collapsedGroups: {
|
||||||
designers: localStorage.getItem("showsg-designers") === "1",
|
designers: localStorage.getItem("showsg-designers") === "1",
|
||||||
collection: localStorage.getItem("showsg-collection") === "1",
|
collection: localStorage.getItem("showsg-collection") === "1",
|
||||||
@ -583,17 +513,6 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
keyword: "",
|
keyword: "",
|
||||||
brandsExpanded: false,
|
brandsExpanded: false,
|
||||||
|
|
||||||
// 品牌筛选弹窗状态
|
|
||||||
modalOpen: false,
|
|
||||||
modalKeyword: "",
|
|
||||||
modalLetter: "",
|
|
||||||
modalResults: [] as BrandEntry[],
|
|
||||||
modalLoading: false,
|
|
||||||
modalSelected: [] as number[],
|
|
||||||
get modalMaxReached() {
|
|
||||||
return this.modalSelected.length >= 5
|
|
||||||
},
|
|
||||||
|
|
||||||
items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
|
items: [] as (ArticleItem & { _imgLoaded?: boolean })[],
|
||||||
loading: false,
|
loading: false,
|
||||||
loadingMore: false,
|
loadingMore: false,
|
||||||
@ -701,6 +620,12 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 事件处理方法
|
// 事件处理方法
|
||||||
|
toggleAside() {
|
||||||
|
this.asideCollapsed = !this.asideCollapsed
|
||||||
|
localStorage.setItem("showsaside-collapsed", this.asideCollapsed ? "1" : "")
|
||||||
|
document.documentElement.classList.toggle("showsaside-collapsed", this.asideCollapsed)
|
||||||
|
},
|
||||||
|
|
||||||
toggleGroup(group: "designers" | "collection" | "season" | "year") {
|
toggleGroup(group: "designers" | "collection" | "season" | "year") {
|
||||||
this.collapsedGroups[group] = !this.collapsedGroups[group]
|
this.collapsedGroups[group] = !this.collapsedGroups[group]
|
||||||
localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "")
|
localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "")
|
||||||
@ -717,73 +642,6 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
this.loadPage(1, false)
|
this.loadPage(1, false)
|
||||||
},
|
},
|
||||||
|
|
||||||
// ===== 品牌筛选弹窗方法 =====
|
|
||||||
async openBrandModal() {
|
|
||||||
this.modalSelected = [...this.selectedBrands]
|
|
||||||
this.modalKeyword = ""
|
|
||||||
this.modalLetter = ""
|
|
||||||
this.modalOpen = true
|
|
||||||
await this.modalLoadHot()
|
|
||||||
},
|
|
||||||
closeBrandModal() {
|
|
||||||
this.modalOpen = false
|
|
||||||
},
|
|
||||||
async modalLoadHot() {
|
|
||||||
this.modalLoading = true
|
|
||||||
try {
|
|
||||||
const list = await getBrands({ featured: 1, size: 50 })
|
|
||||||
this.modalResults = list.length ? list : this.allBrands.slice(0, 50)
|
|
||||||
} catch {
|
|
||||||
this.modalResults = this.allBrands.slice(0, 50)
|
|
||||||
} finally {
|
|
||||||
this.modalLoading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async modalSearch() {
|
|
||||||
const kw = this.modalKeyword.trim()
|
|
||||||
if (kw) {
|
|
||||||
this.modalLetter = ""
|
|
||||||
this.modalLoading = true
|
|
||||||
try {
|
|
||||||
this.modalResults = await getBrands({ keyword: kw, size: 60 })
|
|
||||||
} catch {
|
|
||||||
this.modalResults = []
|
|
||||||
} finally {
|
|
||||||
this.modalLoading = false
|
|
||||||
}
|
|
||||||
} else if (this.modalLetter) {
|
|
||||||
await this.modalPickLetter(this.modalLetter)
|
|
||||||
} else {
|
|
||||||
await this.modalLoadHot()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async modalPickLetter(l: string) {
|
|
||||||
this.modalLetter = l
|
|
||||||
this.modalKeyword = ""
|
|
||||||
this.modalLoading = true
|
|
||||||
try {
|
|
||||||
const letter = l === "#" ? "OTHER" : l
|
|
||||||
this.modalResults = await getBrands({ letter, size: 200 })
|
|
||||||
} catch {
|
|
||||||
this.modalResults = []
|
|
||||||
} finally {
|
|
||||||
this.modalLoading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleModalBrand(id: number) {
|
|
||||||
const idx = this.modalSelected.indexOf(id)
|
|
||||||
if (idx >= 0) this.modalSelected.splice(idx, 1)
|
|
||||||
else {
|
|
||||||
if (this.modalSelected.length >= 5) return
|
|
||||||
this.modalSelected.push(id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmBrandModal() {
|
|
||||||
this.selectedBrands = [...this.modalSelected]
|
|
||||||
this.modalOpen = false
|
|
||||||
this.refreshFilter()
|
|
||||||
},
|
|
||||||
|
|
||||||
clearAll() {
|
clearAll() {
|
||||||
this.selectedBrands = []
|
this.selectedBrands = []
|
||||||
this.selectedCt = []
|
this.selectedCt = []
|
||||||
|
|||||||
Reference in New Issue
Block a user