@ -20,15 +20,6 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
---
|
||||
|
||||
<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()">
|
||||
<!-- 顶部:极简眉头 -->
|
||||
<header class="showshead">
|
||||
@ -36,7 +27,7 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
<nav class="showscrumb" aria-label="Breadcrumb">
|
||||
<a href="/">Home</a>
|
||||
<span class="sep" aria-hidden="true">/</span>
|
||||
<a href="/shows">Runway Archive</a>
|
||||
<a href="/shows">All Shows</a>
|
||||
<span class="sep" aria-hidden="true">/</span>
|
||||
<span class="cur" aria-current="page" x-text="crumbYear"></span>
|
||||
</nav>
|
||||
@ -57,17 +48,12 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
<!-- 主体:左 sidebar + 右 4 列商品 grid -->
|
||||
<div class="showsbody">
|
||||
<!-- LEFT SIDEBAR -->
|
||||
<aside class="showsaside" :class="{ 'is-collapsed': asideCollapsed }">
|
||||
<aside class="showsaside">
|
||||
<div class="showsaside-head">
|
||||
<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 class="showsaside-body" x-show="!asideCollapsed">
|
||||
<div class="showsaside-body">
|
||||
<!-- Designer 多选 -->
|
||||
<section class="showsg">
|
||||
<header class="showsg-h" @click="toggleGroup('designers')">
|
||||
@ -103,7 +89,7 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
<a class="showsmore" x-show="hasMoreBrands && !keyword" @click="brandsExpanded = !brandsExpanded" x-text="brandsExpanded ? 'Show fewer ▴' : `Show all ${allBrands.length} designers ▾`"></a>
|
||||
<a class="showsmore" @click="openBrandModal()" x-text="'浏览全部品牌 +'"></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -194,7 +180,7 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
|
||||
<!-- Empty State -->
|
||||
<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>
|
||||
|
||||
<!-- Card List -->
|
||||
@ -235,6 +221,59 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
</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>
|
||||
|
||||
@ -286,10 +325,6 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
.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;
|
||||
}
|
||||
html.showsaside-collapsed .showsaside,
|
||||
.showsaside.is-collapsed {
|
||||
@apply flex-none w-[44px];
|
||||
}
|
||||
|
||||
.showsaside-head {
|
||||
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
||||
@ -297,25 +332,10 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
.showsaside-h {
|
||||
@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 {
|
||||
@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 {
|
||||
@apply border-t border-[#e6e6e6] py-[12px] pb-[6px] first-of-type:border-t-0 first-of-type:pt-1;
|
||||
@ -479,11 +499,62 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
|
||||
@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, toAbs, type ArticleItem, type BrandEntry } from "../lib/api"
|
||||
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>`
|
||||
@ -499,7 +570,6 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
ICON_USER,
|
||||
ICON_DATE,
|
||||
SPIN_SVG,
|
||||
asideCollapsed: localStorage.getItem("showsaside-collapsed") === "1",
|
||||
collapsedGroups: {
|
||||
designers: localStorage.getItem("showsg-designers") === "1",
|
||||
collection: localStorage.getItem("showsg-collection") === "1",
|
||||
@ -513,6 +583,17 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
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,
|
||||
@ -620,12 +701,6 @@ 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") {
|
||||
this.collapsedGroups[group] = !this.collapsedGroups[group]
|
||||
localStorage.setItem(`showsg-${group}`, this.collapsedGroups[group] ? "1" : "")
|
||||
@ -642,6 +717,73 @@ if (!yearChips.length) yearChips = yearSeed
|
||||
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 = []
|
||||
|
||||
Reference in New Issue
Block a user