@ -1,7 +1,9 @@
|
||||
---
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||
import BrandModal from "@/components/BrandModal.astro"
|
||||
import { getSsgHotBrands } from "@/lib/api"
|
||||
import { getI18n } from '@/i18n/utils'
|
||||
import "@/styles/look-grid.css"
|
||||
|
||||
|
||||
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
|
||||
@ -247,392 +249,22 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 品牌筛选弹窗:搜索 + A-Z 跳转 + 热门推荐,按需加载,避免一次拉全量 -->
|
||||
<div class="showsmodal" x-show="modalOpen" x-cloak @keydown.escape.window="closeBrandModal()">
|
||||
<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"
|
||||
/>
|
||||
<p class="showsmodal-tip">最多展示 30 条结果,可用搜索或字母筛选缩小范围</p>
|
||||
</div>
|
||||
<div class="showsmodal-az">
|
||||
<button class="az az-hot" type="button" :class="{ active: modalLetter === 'HOT' }" @click="modalPickLetter('HOT')">HOT</button>
|
||||
<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 === 'HOT'">热门推荐</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="radio"
|
||||
name="modalDesigner"
|
||||
:value="b.id"
|
||||
x-model="modalSelected"
|
||||
:class="{ 'is-checked': modalSelected === b.id }"
|
||||
/>
|
||||
<span class="showsmodal-name" x-text="b.display || b.name"></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>
|
||||
<!-- 品牌筛选弹窗:抽成独立组件 BrandModal.astro(状态由本页 showsPage 提供) -->
|
||||
<BrandModal />
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script id="showshotbrands-data" type="application/json" set:html={JSON.stringify(hotBrands)}></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] ;
|
||||
}
|
||||
.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];
|
||||
}
|
||||
|
||||
|
||||
|
||||
.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="radio"] {
|
||||
@apply appearance-none w-3.5 h-3.5 border-[1.5px] border-black bg-white cursor-pointer shrink-0 rounded-full;
|
||||
}
|
||||
/* 选中态由 Alpine 状态驱动(.is-checked),不依赖原生 :checked 伪类,
|
||||
因为 @click.prevent 会阻止原生选中,伪类不可靠;实心黑圆一眼可辨 */
|
||||
.showschecks input[type="radio"].is-checked {
|
||||
@apply bg-white border-black;
|
||||
background-image: radial-gradient(circle, #000 36%, transparent 40%);
|
||||
}
|
||||
.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 block w-full mt-[12px] text-center text-[11px] uppercase tracking-[0.22em] text-black font-bold cursor-pointer;
|
||||
}
|
||||
.showsclear {
|
||||
@apply mt-[22px] block w-full text-left pt-[16px] border-t border-black/10 text-[11px] uppercase tracking-[0.18em] text-[#666] cursor-pointer hover:text-black transition-colors duration-150;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.showspager {
|
||||
@apply flex flex-wrap items-center justify-center gap-2 mt-[38px];
|
||||
}
|
||||
.pg {
|
||||
@apply min-w-[40px] h-[40px] px-3 border-[1.5px] border-black bg-white text-black text-[11px] uppercase tracking-[0.12em] font-bold cursor-pointer rounded-none transition-colors duration-200;
|
||||
}
|
||||
.pg.is-current {
|
||||
@apply bg-black text-white cursor-default;
|
||||
}
|
||||
.pg:disabled {
|
||||
@apply opacity-30 cursor-default;
|
||||
}
|
||||
.pg.enabled:hover {
|
||||
@apply bg-black text-white;
|
||||
}
|
||||
.pg-ellipsis {
|
||||
@apply px-1 text-[#999] select-none text-[11px];
|
||||
}
|
||||
.pg-wrap {
|
||||
@apply inline-flex items-center;
|
||||
}
|
||||
|
||||
.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; } }
|
||||
/* ===== 品牌筛选弹窗(语言切换器风格:无 mask、顶部小箭头、干净白底列表) ===== */
|
||||
.showsmodal {
|
||||
@apply fixed inset-0 z-[60] flex items-start justify-center pt-[14vh] pointer-events-none;
|
||||
}
|
||||
.showsmodal-mask {
|
||||
@apply absolute inset-0 pointer-events-auto cursor-default;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.showsmodal-panel {
|
||||
@apply relative w-full max-w-[680px] max-h-[72vh] bg-white flex flex-col border border-black/10 pointer-events-auto;
|
||||
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
/* 顶部小三角箭头(语言切换器同款:白底 + 浅边框) */
|
||||
.showsmodal-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: white;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
.showsmodal-head {
|
||||
@apply flex items-center justify-between px-4 pt-4 pb-2 shrink-0;
|
||||
}
|
||||
.showsmodal-head h3 {
|
||||
@apply m-0 text-[12px] tracking-[0.22em] uppercase font-bold;
|
||||
}
|
||||
.showsmodal-x {
|
||||
@apply appearance-none bg-transparent border-0 w-6 h-6 inline-flex items-center justify-center cursor-pointer text-[14px] leading-none text-[#999] transition-colors hover:text-black;
|
||||
}
|
||||
.showsmodal-search {
|
||||
@apply px-4 pt-1 pb-3 shrink-0;
|
||||
}
|
||||
.showsmodal-input {
|
||||
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[#fafafa] focus:bg-white focus:border-black;
|
||||
}
|
||||
.showsmodal-tip {
|
||||
@apply mt-2 text-[11px] leading-snug text-black/45 font-mono;
|
||||
}
|
||||
.showsmodal-az {
|
||||
@apply flex flex-wrap gap-1 px-4 pb-3 shrink-0;
|
||||
}
|
||||
.showsmodal-az .az {
|
||||
@apply appearance-none bg-white border border-black/10 w-6 h-6 text-[10px] cursor-pointer leading-none font-mono transition-colors hover:bg-black/5;
|
||||
}
|
||||
.showsmodal-az .az.active {
|
||||
@apply bg-black text-white border-black;
|
||||
}
|
||||
.showsmodal-az .az-hot {
|
||||
@apply w-auto px-2 font-sans font-bold tracking-[0.12em] text-[9px];
|
||||
}
|
||||
.showsmodal-body {
|
||||
@apply flex-1 min-h-0 overflow-y-auto px-2 py-1 overscroll-contain;
|
||||
}
|
||||
.showsmodal-hint {
|
||||
@apply text-[10px] tracking-[0.22em] uppercase text-[#999] px-3 py-2;
|
||||
}
|
||||
.showsmodal-loading {
|
||||
@apply flex justify-center py-6 text-[#bdbdbd];
|
||||
}
|
||||
.showsmodal-list {
|
||||
@apply list-none m-0 p-0;
|
||||
}
|
||||
.showsmodal-row {
|
||||
@apply flex items-center gap-2 w-full cursor-pointer px-3 py-2 text-[13px] transition-colors hover:bg-black/5;
|
||||
}
|
||||
.showsmodal-row input[type="radio"] {
|
||||
@apply appearance-none w-4 h-4 border-[1.5px] border-black bg-white cursor-pointer shrink-0 rounded-full;
|
||||
}
|
||||
.showsmodal-row input[type="radio"].is-checked {
|
||||
@apply bg-white border-black;
|
||||
background-image: radial-gradient(circle, #000 36%, transparent 40%);
|
||||
}
|
||||
.showsmodal-name {
|
||||
@apply flex-1 truncate;
|
||||
}
|
||||
.showsmodal-ct {
|
||||
@apply text-[#999] text-[10px] ml-auto;
|
||||
}
|
||||
.showsmodal-empty {
|
||||
@apply py-6 text-center text-[#999] text-[13px];
|
||||
}
|
||||
.showsmodal-foot {
|
||||
@apply flex items-center justify-between px-4 py-3 border-t border-black/5 shrink-0 text-[11px] text-[#888];
|
||||
}
|
||||
.showsmodal-counter {
|
||||
@apply tracking-[0.06em];
|
||||
}
|
||||
.showsmodal-ok {
|
||||
@apply appearance-none bg-black text-white border border-black px-4 py-1.5 cursor-pointer text-[12px] tracking-[0.1em] transition-colors hover:bg-black/85;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getArticles, getBrands, toAbs, type ArticleItem, type BrandEntry } from "@/lib/api"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
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>`
|
||||
import { SPIN_SVG, makeCardSlots, buildPageList } from "@/lib/looks-grid"
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("showsPage", () => {
|
||||
@ -716,13 +348,6 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
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)
|
||||
},
|
||||
@ -845,19 +470,7 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
// ===== 分页导航 =====
|
||||
// 生成窗口化的页码列表:始终含首尾页,当前页 ±1,其余用 "..." 折叠。
|
||||
get pageList() {
|
||||
const total = this.lastPage
|
||||
const cur = this.page
|
||||
if (total <= 7) {
|
||||
return Array.from({ length: total }, (_: any, i: number) => i + 1)
|
||||
}
|
||||
const pages: (number | "...")[] = [1]
|
||||
const start = Math.max(2, cur - 1)
|
||||
const end = Math.min(total - 1, cur + 1)
|
||||
if (start > 2) pages.push("...")
|
||||
for (let i = start; i <= end; i++) pages.push(i)
|
||||
if (end < total - 1) pages.push("...")
|
||||
pages.push(total)
|
||||
return pages
|
||||
return buildPageList(this.lastPage, this.page)
|
||||
},
|
||||
|
||||
goPage(p: number) {
|
||||
@ -878,18 +491,9 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
// 渲染文章url
|
||||
itemHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.item(it.id)) },
|
||||
|
||||
// 渲染thumb图片列表
|
||||
// 渲染thumb图片列表(复用共享工具:固定 5 槽位 + 末位 moreLabel 兜底)
|
||||
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
|
||||
return makeCardSlots(it.gallery || [], it.image_count || 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user