@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
160
src/components/BrandModal.astro
Normal file
160
src/components/BrandModal.astro
Normal file
@ -0,0 +1,160 @@
|
||||
---
|
||||
// 品牌筛选弹窗(仅 markup + 样式)。
|
||||
// Alpine 状态(modalOpen / modalKeyword / modalLetter / modalResults / modalLoading /
|
||||
// modalSelected / SPIN_SVG 等)由父级 RunwayLooks 的 showsPage() 提供,本组件渲染在
|
||||
// .showsroot 的 x-data 作用域内,故可直接引用这些变量,无需 props。
|
||||
---
|
||||
|
||||
<!-- 品牌筛选弹窗:搜索 + 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>
|
||||
|
||||
<style>
|
||||
@reference "tailwindcss";
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
/* ===== 品牌筛选弹窗(语言切换器风格:无 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>
|
||||
@ -22,7 +22,7 @@ const { locale } = getI18n(Astro)
|
||||
{note || "该模块正在筹备中,尚未对外开放。站点正按模块逐个完善,你可以先浏览已上线的走秀档案。"}
|
||||
</p>
|
||||
<div class="cs-actions">
|
||||
<a class="cs-btn" href={href(locale, ROUTES.articles)}>浏览走秀档案 →</a>
|
||||
<a class="cs-btn" href={href(locale, ROUTES.runwayLooks)}>浏览走秀档案 →</a>
|
||||
<a class="cs-btn-ghost" href={href(locale, ROUTES.home)}>返回首页</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
---
|
||||
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,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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@ import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||
import { getI18n } from '@/i18n/utils'
|
||||
import { clientHref, ROUTES } from '@/lib/routes'
|
||||
import { toAbs } from '@/lib/api'
|
||||
import "@/styles/look-grid.css"
|
||||
|
||||
const { locale, t } = getI18n(Astro)
|
||||
|
||||
@ -173,186 +174,12 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
|
||||
<script id="snaps-year-opts" type="application/json" set:html={JSON.stringify({ yearOptions, sortOptions })}></script>
|
||||
|
||||
<style>
|
||||
@reference "tailwindcss";
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
.snapsroot {
|
||||
@apply m-auto max-w-[1600px] pt-7 text-[#0a0a0a];
|
||||
}
|
||||
|
||||
.snapshead {
|
||||
@apply flex justify-between items-end gap-6 py-5 pb-[22px];
|
||||
}
|
||||
.snapsh1 {
|
||||
@apply text-[clamp(34px,5.5vw,64px)] leading-[0.95] font-black tracking-[-0.02em] my-0 mb-[8px];
|
||||
}
|
||||
|
||||
/* 已激活筛选 pills */
|
||||
.snapspills {
|
||||
@apply flex flex-wrap gap-2 mb-4;
|
||||
}
|
||||
.snapspill {
|
||||
@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;
|
||||
}
|
||||
.snapspill .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;
|
||||
}
|
||||
|
||||
/* 主体:左 sidebar + 右网格 */
|
||||
.snapsbody {
|
||||
@apply flex gap-[36px] items-start max-md:flex-col max-md:gap-[18px];
|
||||
}
|
||||
|
||||
.snapsaside {
|
||||
@apply flex-none w-[240px] bg-white sticky top-5 max-h-[calc(100vh-40px)] rounded-none flex flex-col max-md:static max-md:max-h-none max-md:w-full;
|
||||
}
|
||||
.snapsaside-head {
|
||||
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
||||
}
|
||||
.snapsaside-h {
|
||||
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none;
|
||||
}
|
||||
.snapsaside-body {
|
||||
@apply p-[14px_14px_18px] overflow-y-auto flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#ccc_#fff];
|
||||
}
|
||||
|
||||
.snapsg {
|
||||
@apply border-t border-[#e6e6e6] py-[12px] pb-[6px] first-of-type:border-t-0 first-of-type:pt-1;
|
||||
}
|
||||
.snapsg-h {
|
||||
@apply flex justify-between items-center text-[11px] tracking-[0.22em] uppercase font-bold text-black mb-2 cursor-pointer select-none;
|
||||
}
|
||||
.snapsg-chev {
|
||||
@apply inline-block text-[9px] leading-none text-[#999] transition-transform duration-200 ml-1;
|
||||
}
|
||||
.snapsg-chev.is-collapsed { @apply -rotate-90; }
|
||||
|
||||
.snapschecks {
|
||||
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin];
|
||||
}
|
||||
.snapschecks 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];
|
||||
}
|
||||
.snapschecks 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 会阻止原生选中,伪类不可靠;实心黑圆一眼可辨 */
|
||||
.snapschecks input[type="radio"].is-checked {
|
||||
@apply bg-white border-black;
|
||||
background-image: radial-gradient(circle, #000 36%, transparent 40%);
|
||||
}
|
||||
.snapschecks--flat li { @apply py-[4px]; }
|
||||
.snapschecks--flat li label { @apply inline-flex items-center gap-2 cursor-pointer w-full; }
|
||||
|
||||
.snapsclear {
|
||||
@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;
|
||||
}
|
||||
|
||||
/* 主体内容区 */
|
||||
.snapsmain { @apply flex-1 min-w-0; }
|
||||
.snapscontent {
|
||||
@apply relative;
|
||||
}
|
||||
.snapscontent.is-loading::before {
|
||||
content: "";
|
||||
@apply absolute -top-1 left-0 h-[2px] w-[38%] bg-black z-10;
|
||||
animation: snapProg 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ===== 卡片网格(与 runway-looks 同款) ===== */
|
||||
.snapsgrid {
|
||||
@apply grid grid-cols-2 gap-[22px] relative max-[560px]:grid-cols-1;
|
||||
}
|
||||
|
||||
.snapscard {
|
||||
@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];
|
||||
}
|
||||
.snapscard:hover .snapscard-img img {
|
||||
@apply scale-104;
|
||||
}
|
||||
|
||||
.snapscard-left {
|
||||
@apply flex-none w-[30%] min-w-[110px] max-[700px]:w-full max-[700px]:flex-none;
|
||||
}
|
||||
.snapscard-img {
|
||||
@apply relative w-full aspect-[3/4] bg-[#f1f1f1] overflow-hidden rounded-none;
|
||||
}
|
||||
.snapscard-img img {
|
||||
@apply w-full h-full object-cover transition-transform duration-600 opacity-0;
|
||||
}
|
||||
.snapscard-img img.is-loaded {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.snapsspin {
|
||||
@apply absolute inset-0 flex items-center justify-center text-[#bdbdbd] opacity-100 transition-opacity duration-300 pointer-events-none z-10;
|
||||
}
|
||||
.snapscard-img.is-loaded .snapsspin,
|
||||
.snapscard-thumb.is-loaded .snapsspin {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.snapscard-right {
|
||||
@apply flex-1 min-w-0 flex flex-col justify-between max-[700px]:w-full;
|
||||
}
|
||||
.snapscard-info {
|
||||
@apply flex-none min-w-0 flex flex-col p-[2px_2px_0];
|
||||
}
|
||||
.snapscard-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;
|
||||
}
|
||||
.snapscard-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;
|
||||
}
|
||||
|
||||
.snapscard-strip {
|
||||
@apply grid grid-cols-5 gap-1 mt-[10px];
|
||||
}
|
||||
.snapscard-thumb {
|
||||
@apply relative aspect-[3/4] bg-[#f1f1f1] overflow-hidden;
|
||||
}
|
||||
.snapscard-thumb img {
|
||||
@apply w-full h-full object-cover opacity-0 transition-opacity duration-450 ease-in-out;
|
||||
}
|
||||
.snapscard-thumb img.is-loaded {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.snapscard-thumb.placeholder {
|
||||
@apply bg-[#f6f6f6];
|
||||
}
|
||||
|
||||
.snapscard--skel .snapscard-img,
|
||||
.snapscard--skel .snapscard-thumb,
|
||||
.snapscard--skel .snapsline {
|
||||
@apply bg-[#efefef];
|
||||
animation: snapShim 1.6s ease infinite;
|
||||
}
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.snappager { @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; }
|
||||
|
||||
.snapsempty {
|
||||
@apply col-span-full p-[80px_20px] text-center text-xs tracking-[0.22em] uppercase text-[#999] border border-dashed border-[#ccc];
|
||||
}
|
||||
|
||||
@keyframes snapProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||
@keyframes snapShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getStreetSnaps, toAbs, type StreetSnapItem } 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"
|
||||
|
||||
const DEFAULT_SORT = "image_count"
|
||||
|
||||
@ -414,15 +241,10 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
},
|
||||
|
||||
// 卡片右侧缩略图条带:固定 5 个槽位,缺图留 placeholder
|
||||
// 街拍 images 是 { image, name }[](与 runway 的 string[] gallery 不同)
|
||||
// 街拍 images 是 { image, name }[](与 runway 的 string[] gallery 不同),先抽成纯 URL 数组
|
||||
getCardSlots(it: StreetSnapItem) {
|
||||
const imgs = it.images || []
|
||||
const slots: { src: string; loaded: boolean }[] = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const im: any = imgs[i]
|
||||
slots.push({ src: im ? (im.image || im) : "", loaded: false })
|
||||
}
|
||||
return slots
|
||||
const imgs = (it.images || []).map((im: any) => (im && (im.image || im)) || "")
|
||||
return makeCardSlots(imgs)
|
||||
},
|
||||
|
||||
async loadPage(page: number) {
|
||||
@ -449,17 +271,7 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
},
|
||||
|
||||
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) {
|
||||
@ -474,7 +286,8 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
nextPage() { this.goPage(this.page + 1) },
|
||||
|
||||
toAbsUrl(url: string) { return toAbs(url) },
|
||||
itemHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.streetSnap(it.id)) },
|
||||
// 街拍详情统一收口到 /item/[id](与服务端 /item 回落逻辑对应)
|
||||
itemHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.item(it.id)) },
|
||||
|
||||
// theimpression 标题含字面 \t:地点 \t 场次,拆成 [location, event]
|
||||
titleParts(it: StreetSnapItem): string[] {
|
||||
|
||||
@ -1,210 +0,0 @@
|
||||
---
|
||||
import astroLogo from '@/assets/astro.svg';
|
||||
import background from '@/assets/background.svg';
|
||||
---
|
||||
|
||||
<div id="container">
|
||||
<img id="background" src={background.src} alt="" fetchpriority="high" />
|
||||
<main>
|
||||
<section id="hero">
|
||||
<a href="https://astro.build"
|
||||
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
|
||||
>
|
||||
<h1>
|
||||
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
|
||||
</h1>
|
||||
<section id="links">
|
||||
<a class="button" href="https://docs.astro.build">Read our docs</a>
|
||||
<a href="https://astro.build/chat"
|
||||
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<a href="https://astro.build/blog/astro-6-beta/" id="news" class="box">
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
|
||||
fill="#111827"></path></svg
|
||||
>
|
||||
<h2>What's New in Astro 6.0?</h2>
|
||||
<p>
|
||||
Redesigned dev server, fonts, live collections, built-in CSP support, and more! Click to
|
||||
explore Astro 6.0's new features.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
filter: blur(100px);
|
||||
}
|
||||
|
||||
#container {
|
||||
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
#links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
color: #111827;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
#links a:hover {
|
||||
color: rgb(78, 80, 86);
|
||||
}
|
||||
|
||||
#links a svg {
|
||||
height: 1em;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
color: white;
|
||||
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#links a.button:hover {
|
||||
color: rgb(230, 230, 230);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family:
|
||||
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
||||
monospace;
|
||||
font-weight: normal;
|
||||
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 1em;
|
||||
font-weight: normal;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4b5563;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.006em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
background:
|
||||
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
|
||||
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
border-radius: 16px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
#news {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
max-width: 300px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
backdrop-filter: blur(50px);
|
||||
}
|
||||
|
||||
#news:hover {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
@media screen and (max-height: 368px) {
|
||||
#news {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: block;
|
||||
padding-top: 10%;
|
||||
}
|
||||
|
||||
#links {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
#news {
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
bottom: 2.5rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +0,0 @@
|
||||
---
|
||||
// 页面主体(与语言无关的内容),由根页面与 [locale] 页面共用
|
||||
import ComingSoon from "@/components/ComingSoon.astro"
|
||||
---
|
||||
|
||||
<ComingSoon module="About" moduleZh="关于" />
|
||||
@ -1,262 +0,0 @@
|
||||
---
|
||||
// 账号表单(登录 / 注册通用),编辑风黑白设计,匹配全站视觉。
|
||||
// - mode: "login" | "register"
|
||||
// - 提交:POST /api/auth/login 或 /api/auth/register,成功后写入本地会话并跳转。
|
||||
// - 后端不可用时也能优雅显示错误(不破坏设计)。
|
||||
import { getI18n } from "@/i18n/utils"
|
||||
import { href, ROUTES } from "@/lib/routes"
|
||||
|
||||
interface Props {
|
||||
mode?: "login" | "register"
|
||||
}
|
||||
const { mode = "login" } = Astro.props
|
||||
const { locale } = getI18n(Astro)
|
||||
|
||||
const isLogin = mode === "login"
|
||||
const title = isLogin ? "Sign In" : "Create Account"
|
||||
const kickerRight = isLogin ? "Sign In" : "Create Account"
|
||||
const subtitle = isLogin
|
||||
? "Sign in to your account to access the runway archive."
|
||||
: "Create an account to save shows and build your own archive."
|
||||
const submitLabel = isLogin ? "Sign In" : "Create Account"
|
||||
// 登录/注册成功后回跳目标:优先 ?redirect=,否则回 /en/
|
||||
const redirect = Astro.url.searchParams.get("redirect") || href(locale, ROUTES.home)
|
||||
|
||||
const loginHref = href(locale, ROUTES.login)
|
||||
const registerHref = href(locale, ROUTES.register)
|
||||
---
|
||||
|
||||
<section class="auth-wrap">
|
||||
<div class="auth-kicker">
|
||||
<span>Account</span>
|
||||
<span>{kickerRight}</span>
|
||||
</div>
|
||||
|
||||
<h1 class="auth-title">{title}</h1>
|
||||
<p class="auth-sub">{subtitle}</p>
|
||||
|
||||
<form id="auth-form" class="auth-form" data-mode={mode} data-redirect={redirect} novalidate>
|
||||
{!isLogin && (
|
||||
<label class="auth-field">
|
||||
<span class="auth-label">Username</span>
|
||||
<input
|
||||
class="auth-input"
|
||||
type="text"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
placeholder="yourname"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label class="auth-field">
|
||||
<span class="auth-label">Email</span>
|
||||
<input
|
||||
class="auth-input"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="auth-field">
|
||||
<span class="auth-label">Password</span>
|
||||
<input
|
||||
class="auth-input"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete={isLogin ? "current-password" : "new-password"}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
{!isLogin && (
|
||||
<label class="auth-field">
|
||||
<span class="auth-label">Confirm Password</span>
|
||||
<input
|
||||
class="auth-input"
|
||||
type="password"
|
||||
name="confirm"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<p id="auth-error" class="auth-error" hidden></p>
|
||||
|
||||
<button type="submit" class="auth-submit">{submitLabel}</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-foot">
|
||||
{isLogin ? (
|
||||
<>
|
||||
<a class="auth-link" href={registerHref}>Don’t have an account? Sign up →</a>
|
||||
<a class="auth-link muted" href="#">Forgot your password?</a>
|
||||
</>
|
||||
) : (
|
||||
<a class="auth-link" href={loginHref}>Already have an account? Sign in →</a>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.auth-wrap {
|
||||
@apply w-full max-w-[460px] mx-auto;
|
||||
padding-top: clamp(40px, 7vw, 90px);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.auth-kicker {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #000;
|
||||
padding-top: 18px;
|
||||
@apply font-mono text-[11px] tracking-[0.25em] uppercase text-[#666];
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
@apply font-serif font-black tracking-[-0.02em] text-black;
|
||||
line-height: 0.9;
|
||||
font-size: clamp(44px, 9vw, 88px);
|
||||
margin: 0.16em 0 0.18em;
|
||||
}
|
||||
|
||||
.auth-sub {
|
||||
@apply font-mono text-[12px] text-[#666];
|
||||
letter-spacing: 0.04em;
|
||||
margin: 0 0 34px;
|
||||
max-width: 42ch;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
.auth-field {
|
||||
@apply block mb-[22px];
|
||||
}
|
||||
|
||||
.auth-label {
|
||||
display: block;
|
||||
@apply font-mono text-[11px] uppercase tracking-[0.2em] text-black mb-[8px];
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
@apply w-full border border-black bg-white px-[14px] py-[12px] font-sans text-[14px] text-black;
|
||||
outline: none;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
.auth-input::placeholder {
|
||||
@apply text-black/30;
|
||||
}
|
||||
.auth-input:focus {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
@apply font-mono text-[12px] text-black;
|
||||
border: 1px solid #000;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
padding: 10px 12px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.auth-submit {
|
||||
@apply w-full bg-black text-white border border-black px-[18px] py-[14px] text-[12px] uppercase tracking-[0.18em] font-bold cursor-pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
.auth-submit:hover {
|
||||
@apply bg-white text-black;
|
||||
}
|
||||
.auth-submit:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.auth-foot {
|
||||
@apply mt-[26px] pt-[22px] border-t border-zinc-200 flex flex-col gap-[10px];
|
||||
@apply font-mono text-[12px] text-[#666];
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
@apply text-black no-underline;
|
||||
}
|
||||
.auth-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.auth-link.muted {
|
||||
@apply text-[#666];
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { saveSession } from "@/lib/api"
|
||||
|
||||
const form = document.getElementById("auth-form") as HTMLFormElement | null
|
||||
if (form) {
|
||||
const errEl = document.getElementById("auth-error") as HTMLParagraphElement | null
|
||||
const btn = form.querySelector('button[type="submit"]') as HTMLButtonElement | null
|
||||
const mode = (form.dataset.mode || "login") as "login" | "register"
|
||||
const redirect = form.dataset.redirect || "/"
|
||||
|
||||
const showErr = (msg: string) => {
|
||||
if (!errEl) return
|
||||
errEl.textContent = msg
|
||||
errEl.hidden = false
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault()
|
||||
if (errEl) errEl.hidden = true
|
||||
const data = Object.fromEntries(new FormData(form).entries()) as Record<string, string>
|
||||
|
||||
if (mode === "register" && data.password !== data.confirm) {
|
||||
showErr("Passwords do not match.")
|
||||
return
|
||||
}
|
||||
|
||||
const label = mode === "login" ? "Sign In" : "Create Account"
|
||||
const orig = btn?.textContent ?? label
|
||||
if (btn) {
|
||||
btn.disabled = true
|
||||
btn.textContent = "…"
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = mode === "login" ? "/api/v1/auth/login" : "/api/v1/auth/register"
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
const json: any = await res.json().catch(() => ({}))
|
||||
if (!res.ok) {
|
||||
showErr(json?.message || json?.error || `Unable to ${label.toLowerCase()}. Please try again.`)
|
||||
return
|
||||
}
|
||||
const token = json?.data?.token ?? json?.token
|
||||
const user = json?.data?.user ?? json?.user
|
||||
if (token && user) {
|
||||
saveSession(token, user)
|
||||
location.href = redirect
|
||||
return
|
||||
}
|
||||
showErr("Unexpected response. Please try again.")
|
||||
} catch {
|
||||
showErr("Network error. Please try again.")
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false
|
||||
btn.textContent = orig
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -1,6 +0,0 @@
|
||||
---
|
||||
// 页面主体(与语言无关的内容),由根页面与 [locale] 页面共用
|
||||
import ComingSoon from "@/components/ComingSoon.astro"
|
||||
---
|
||||
|
||||
<ComingSoon module="Contact" moduleZh="联系" />
|
||||
@ -1,6 +0,0 @@
|
||||
---
|
||||
// 页面主体(与语言无关的内容),由根页面与 [locale] 页面共用
|
||||
import ComingSoon from "@/components/ComingSoon.astro"
|
||||
---
|
||||
|
||||
<ComingSoon module="Latest Projects" moduleZh="最新项目" />
|
||||
@ -1,6 +0,0 @@
|
||||
---
|
||||
// 页面主体(与语言无关的内容),由根页面与 [locale] 页面共用
|
||||
import ComingSoon from "@/components/ComingSoon.astro"
|
||||
---
|
||||
|
||||
<ComingSoon module="Sign In" moduleZh="登录" note="账户系统正在筹备中,暂未开放注册与登录。" />
|
||||
@ -1,6 +0,0 @@
|
||||
---
|
||||
// 页面主体(与语言无关的内容),由根页面与 [locale] 页面共用
|
||||
import ComingSoon from "@/components/ComingSoon.astro"
|
||||
---
|
||||
|
||||
<ComingSoon module="Portfolio" moduleZh="作品集" />
|
||||
@ -1,6 +0,0 @@
|
||||
---
|
||||
// 页面主体(与语言无关的内容),由根页面与 [locale] 页面共用
|
||||
import ComingSoon from "@/components/ComingSoon.astro"
|
||||
---
|
||||
|
||||
<ComingSoon module="Create Account" moduleZh="注册" note="账户系统正在筹备中,暂未开放注册与登录。" />
|
||||
@ -1,285 +0,0 @@
|
||||
---
|
||||
import type { RawArticle } from "@/lib/api"
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||
import { ssrBase } from "@/lib/api"
|
||||
|
||||
interface Props {
|
||||
article: RawArticle | null
|
||||
locale?: "en" | "cn"
|
||||
error?: "missing" | "failed" | null
|
||||
}
|
||||
const { article, locale = "en", error = null } = Astro.props
|
||||
|
||||
// 把相对图片地址补全为绝对地址(服务端完成,避免浏览器再拼;base 由 ssr.ts 统一解析)
|
||||
const full = (u: string): string =>
|
||||
!u ? "" : /^https?:\/\//i.test(u) ? u : ssrBase().replace(/\/$/, "") + u
|
||||
|
||||
// 图片数组:服务端预处理为灯箱可用的 {id,url,name}
|
||||
const images = (article?.images ?? []).map((im) => ({
|
||||
id: im.id,
|
||||
url: full(im.image || ""),
|
||||
name: im.name || "",
|
||||
}))
|
||||
|
||||
// 正文段落(如有)
|
||||
const descParas = (article?.description || "")
|
||||
.split(/\n\n+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
// 文案(按语言切换)
|
||||
const L = {
|
||||
en: {
|
||||
back: "← Back to Index",
|
||||
source: "Source ↗",
|
||||
runway: "Runway",
|
||||
notFound: "Article not found",
|
||||
missing: "No article ID specified",
|
||||
failed: "Failed to load article. Please refresh or try later.",
|
||||
},
|
||||
cn: {
|
||||
back: "← 返回首页",
|
||||
source: "来源 ↗",
|
||||
runway: "秀场",
|
||||
notFound: "未找到该文章",
|
||||
missing: "未指定文章 ID",
|
||||
failed: "加载文章失败,请刷新或稍后再试",
|
||||
},
|
||||
}
|
||||
const lbl = L[locale === "cn" ? "cn" : "en"]
|
||||
const backLabel = lbl.back.replace("← ", "")
|
||||
// 面包屑:首页 → 走秀档案(/shows) → 品牌筛选(带 brand_id) → 当前文章(复用项目 Breadcrumb 组件)
|
||||
const homeHref = locale === "cn" ? "/cn" : "/en/"
|
||||
const archiveHref = locale === "cn" ? "/cn/shows" : "/en/shows"
|
||||
const crumbs: { label: string; href: string }[] = [
|
||||
{ label: "Runway Looks", href: archiveHref },
|
||||
]
|
||||
if (article?.brand_id) {
|
||||
crumbs.push({
|
||||
label: article.brand_name || "Brand",
|
||||
href: `${archiveHref}?brand_id=${article.brand_id}`,
|
||||
})
|
||||
}
|
||||
---
|
||||
|
||||
<!-- 外层 x-data 承载灯箱状态;图片数据由服务端注入,无需浏览器再 fetch -->
|
||||
<div x-data={`articleView(${JSON.stringify(images)})`} class="min-h-screen bg-white text-black font-sans selection:bg-black selection:text-white">
|
||||
|
||||
{article ? (
|
||||
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
|
||||
<!-- 面包屑(复用项目 Breadcrumb 组件):首页 / 走秀档案 / 品牌 / 当前文章 -->
|
||||
<Breadcrumb items={crumbs}>
|
||||
<span slot="current" class="max-w-[60vw] truncate">{article.title}</span>
|
||||
</Breadcrumb>
|
||||
|
||||
|
||||
<!-- 标题 -->
|
||||
<h1 class="font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">{article.title}</h1>
|
||||
|
||||
<!-- 图片集(服务端渲染,利于 SEO 与首屏) -->
|
||||
{images.length > 0 && (
|
||||
<section class="mt-16">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{images.map((im, i) => (
|
||||
<figure @click={`openLb(${i})`} class="bg-[#f2f2f2] cursor-zoom-in group">
|
||||
<img src={im.url} alt={im.name} loading="lazy" class="w-full aspect-[3/4] object-cover group-hover:opacity-90 transition-opacity" />
|
||||
{im.name && <figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2">{im.name}</figcaption>}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- 正文(如有) -->
|
||||
{descParas.length > 0 && (
|
||||
<section class="mt-16 max-w-[720px] space-y-5 text-[15px] leading-[1.8] text-[#222]">
|
||||
{descParas.map((p) => <p>{p}</p>)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
</article>
|
||||
) : (
|
||||
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
|
||||
<p class="font-mono text-sm text-[#888] uppercase tracking-widest mb-4">{error === "missing" ? lbl.missing : lbl.failed}</p>
|
||||
<a href={homeHref} class="text-[12px] font-mono uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors">← {backLabel}</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- 4. 全屏灯箱组件 (Lightbox,Alpine 客户端交互) -->
|
||||
<div
|
||||
x-show="lbOpen"
|
||||
x-cloak
|
||||
@keydown.window.escape="closeLb()"
|
||||
@keydown.window.arrow-right="nextLb()"
|
||||
@keydown.window.arrow-left="prevLb()"
|
||||
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} / ${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 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.url" alt="" loading="lazy" 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 @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 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 @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 @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 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>
|
||||
|
||||
<!-- 大图显示区域(支持点击切换放大/缩小状态) -->
|
||||
<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="images && images[lbIndex]">
|
||||
<img
|
||||
:src="images[lbIndex].url"
|
||||
:alt="images[lbIndex].name || ''"
|
||||
@click="toggleZoom()"
|
||||
loading="lazy"
|
||||
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 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="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>
|
||||
|
||||
<!-- Alpine.js 业务逻辑:仅负责灯箱交互,图片数据由服务端注入 -->
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('articleView', (images = []) => ({
|
||||
images,
|
||||
lbOpen: false,
|
||||
lbIndex: 0,
|
||||
lbZoomed: false,
|
||||
|
||||
// 触摸滑动记录
|
||||
touchStartX: 0,
|
||||
touchStartY: 0,
|
||||
|
||||
// 打开灯箱
|
||||
openLb(index = 0) {
|
||||
if (!this.images?.length) return
|
||||
this.lbIndex = index
|
||||
this.lbZoomed = false
|
||||
this.lbOpen = true
|
||||
document.body.style.overflow = "hidden"
|
||||
this.scrollThumbIntoView()
|
||||
},
|
||||
|
||||
// 关闭灯箱
|
||||
closeLb() {
|
||||
this.lbOpen = false
|
||||
this.lbZoomed = false
|
||||
document.body.style.overflow = ""
|
||||
},
|
||||
|
||||
// 切换视图
|
||||
showLb(index) {
|
||||
const total = this.images?.length || 0
|
||||
if (total === 0) return
|
||||
this.lbIndex = (index + total) % total
|
||||
this.resetZoom()
|
||||
this.scrollThumbIntoView()
|
||||
},
|
||||
|
||||
nextLb() { this.showLb(this.lbIndex + 1) },
|
||||
prevLb() { this.showLb(this.lbIndex - 1) },
|
||||
|
||||
toggleZoom() {
|
||||
this.lbZoomed = !this.lbZoomed
|
||||
if (this.$refs.lbMain) this.$refs.lbMain.scrollTop = 0
|
||||
},
|
||||
|
||||
resetZoom() {
|
||||
this.lbZoomed = false
|
||||
if (this.$refs.lbMain) this.$refs.lbMain.scrollTop = 0
|
||||
},
|
||||
|
||||
// 左侧缩略图自动居中滚到视野中央
|
||||
scrollThumbIntoView() {
|
||||
this.$nextTick(() => {
|
||||
const thumbsEl = this.$refs.lbThumbs
|
||||
if (!thumbsEl) return
|
||||
const activeThumb = thumbsEl.children[this.lbIndex]
|
||||
if (activeThumb) {
|
||||
const target = activeThumb.offsetTop - (thumbsEl.clientHeight - activeThumb.clientHeight) / 2
|
||||
thumbsEl.scrollTo({ top: Math.max(0, target), behavior: "smooth" })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 手势处理
|
||||
handleTouchStart(e) {
|
||||
if (e.touches.length === 1) {
|
||||
this.touchStartX = e.touches[0].clientX
|
||||
this.touchStartY = e.touches[0].clientY
|
||||
}
|
||||
},
|
||||
|
||||
handleTouchEnd(e) {
|
||||
if (this.lbZoomed) return
|
||||
const t = e.changedTouches[0]
|
||||
const dx = t.clientX - this.touchStartX
|
||||
const dy = t.clientY - this.touchStartY
|
||||
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
|
||||
dx < 0 ? this.nextLb() : this.prevLb()
|
||||
}
|
||||
}
|
||||
}))
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 隐藏缩略图侧边栏滚动条 */
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
</style>
|
||||
@ -7,8 +7,6 @@ import { setApiLocale } from '@/lib/api'
|
||||
import { i18n } from 'astro:config/client';
|
||||
import { toCodes } from 'astro:i18n';
|
||||
|
||||
import LanguageDetector from '@/components/LanguageDetector.astro'
|
||||
|
||||
const { title } = Astro.props as { title?: string }
|
||||
const siteTitle = title ? `${title} — Portfolio Studio` : "Portfolio Studio"
|
||||
|
||||
@ -50,7 +48,6 @@ const headers = {
|
||||
<!doctype html>
|
||||
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
||||
<head>
|
||||
<!-- TODO <LanguageDetector /> -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
|
||||
59
src/lib/looks-grid.ts
Normal file
59
src/lib/looks-grid.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 走秀 / 街拍 列表页共享的 Alpine 工具(从 RunwayLooks.astro / StreetSnaps.astro 抽出)。
|
||||
* 仅承载两个模块同构、且与各自筛选项无关的逻辑:加载转圈 SVG、卡片缩略图槽位、分页页码计算。
|
||||
* 页面特有逻辑(品牌弹窗、各筛选维度、取数 API)仍保留在各组件内。
|
||||
*/
|
||||
|
||||
export 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>`
|
||||
|
||||
export interface CardSlot {
|
||||
src: string
|
||||
loaded: boolean
|
||||
isLast?: boolean
|
||||
moreLabel?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片底部缩略图条带:固定 count 个槽位,缺图留空(前端用 .placeholder 占位)。
|
||||
* @param images 图片数组;runway 为 string[](gallery),street 为 { image, name }[](images)。
|
||||
* @param opts.moreLabel 当图片多于 count 时,在最后一个槽位上叠加 "+N" 文案的回调。
|
||||
*/
|
||||
export function makeCardSlots(
|
||||
images: any[],
|
||||
count = 5,
|
||||
opts?: { moreLabel?: (extra: number) => string },
|
||||
): CardSlot[] {
|
||||
const list = images || []
|
||||
const slots: CardSlot[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const im = list[i]
|
||||
let src = ""
|
||||
if (im) src = typeof im === "string" ? im : im.image || im.src || ""
|
||||
const slot: CardSlot = { src, loaded: false }
|
||||
if (opts?.moreLabel && i === count - 1 && list.length > count) {
|
||||
slot.isLast = true
|
||||
slot.moreLabel = opts.moreLabel(list.length - count)
|
||||
}
|
||||
slots.push(slot)
|
||||
}
|
||||
return slots
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页页码:总数 <=7 全展示;否则围绕当前页展示 [1, ..., cur-1..cur+1, ..., total]。
|
||||
*/
|
||||
export function buildPageList(total: number, cur: number): (number | "...")[] {
|
||||
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
|
||||
}
|
||||
@ -19,11 +19,10 @@ export const ROUTES = {
|
||||
runwayLooks: '/runway-looks',
|
||||
// 街拍
|
||||
streetSnaps: '/street-snaps',
|
||||
// 动态路由(工厂函数)
|
||||
// 动态路由(工厂函数)。所有详情(走秀 lookbook / 街拍)统一收口到 /item/[id],
|
||||
// 由 src/pages/{locale}/item/[id].astro 按 article→street 顺序回落渲染。
|
||||
item: (id: string | number) => `/item/${id}`,
|
||||
article: (id: string | number) => `/article?id=${id}`,
|
||||
streetSnap: (id: string | number) => `/street-snaps/${id}`,
|
||||
// runwayLooksDetail: (id: string | number) => `/runway-looks/${id}`,
|
||||
streetSnap: (id: string | number) => `/item/${id}`,
|
||||
} as const;
|
||||
|
||||
/** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import Article from "@/components/pages/article.astro"
|
||||
import { getSsrArticle } from "@/lib/api"
|
||||
|
||||
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/article?id= 链接,无需改动其他页面)
|
||||
export const prerender = false
|
||||
|
||||
const id = Astro.url.searchParams.get("id")
|
||||
const { article, error } = await getSsrArticle(id, Astro.currentLocale)
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Article article={article} locale="en" error={error} />
|
||||
</Layout>
|
||||
@ -1,15 +1,25 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import Item from "@/components/Item.astro"
|
||||
import { getSsrArticle } from "@/lib/api"
|
||||
import StreetSnap from "@/components/StreetSnap.astro"
|
||||
import { getSsrArticle, getSsrStreetSnap } from "@/lib/api"
|
||||
|
||||
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/items/:id 链接,无需改动其他页面)
|
||||
// 按需 SSR:走秀详情与街拍详情统一收口到 /item/[id]。
|
||||
// 先按 article 取数(runway lookbook);取不到再回落 street snap,实现「所有详情进 /item」。
|
||||
export const prerender = false
|
||||
|
||||
const { id } = Astro.params
|
||||
const { article, error } = await getSsrArticle(id, Astro.currentLocale)
|
||||
const { article, error: articleErr } = await getSsrArticle(id, Astro.currentLocale)
|
||||
|
||||
let snap = null
|
||||
let snapErr: "missing" | "failed" | null = null
|
||||
if (!article) {
|
||||
const res = await getSsrStreetSnap(id, Astro.currentLocale)
|
||||
snap = res.snap
|
||||
snapErr = res.error
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Item article={article} error={error} />
|
||||
{article ? <Item article={article} error={articleErr} /> : <StreetSnap snap={snap} error={snapErr} />}
|
||||
</Layout>
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import StreetSnap from "@/components/StreetSnap.astro"
|
||||
import { getSsrStreetSnap } from "@/lib/api"
|
||||
|
||||
// 按需 SSR:服务端按 :id 取数并渲染(保留 /cn/street-snaps/:id 链接)
|
||||
export const prerender = false
|
||||
|
||||
const { id } = Astro.params
|
||||
const { snap, error } = await getSsrStreetSnap(id, Astro.currentLocale)
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<StreetSnap snap={snap} error={error} />
|
||||
</Layout>
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import Article from "@/components/pages/article.astro"
|
||||
import { getSsrArticle } from "@/lib/api"
|
||||
|
||||
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/article?id= 链接,无需改动其他页面)
|
||||
export const prerender = false
|
||||
|
||||
const id = Astro.url.searchParams.get("id")
|
||||
const { article, error } = await getSsrArticle(id, Astro.currentLocale)
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Article article={article} locale="en" error={error} />
|
||||
</Layout>
|
||||
@ -1,15 +1,25 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import Item from "@/components/Item.astro"
|
||||
import { getSsrArticle } from "@/lib/api"
|
||||
import StreetSnap from "@/components/StreetSnap.astro"
|
||||
import { getSsrArticle, getSsrStreetSnap } from "@/lib/api"
|
||||
|
||||
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/items/:id 链接,无需改动其他页面)
|
||||
// 按需 SSR:走秀详情与街拍详情统一收口到 /item/[id]。
|
||||
// 先按 article 取数(runway lookbook);取不到再回落 street snap,实现「所有详情进 /item」。
|
||||
export const prerender = false
|
||||
|
||||
const { id } = Astro.params
|
||||
const { article, error } = await getSsrArticle(id, Astro.currentLocale)
|
||||
const { article, error: articleErr } = await getSsrArticle(id, Astro.currentLocale)
|
||||
|
||||
let snap = null
|
||||
let snapErr: "missing" | "failed" | null = null
|
||||
if (!article) {
|
||||
const res = await getSsrStreetSnap(id, Astro.currentLocale)
|
||||
snap = res.snap
|
||||
snapErr = res.error
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Item article={article} error={error} />
|
||||
{article ? <Item article={article} error={articleErr} /> : <StreetSnap snap={snap} error={snapErr} />}
|
||||
</Layout>
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import StreetSnap from "@/components/StreetSnap.astro"
|
||||
import { getSsrStreetSnap } from "@/lib/api"
|
||||
|
||||
// 按需 SSR:服务端按 :id 取数并渲染(保留 /en/street-snaps/:id 链接)
|
||||
export const prerender = false
|
||||
|
||||
const { id } = Astro.params
|
||||
const { snap, error } = await getSsrStreetSnap(id, Astro.currentLocale)
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<StreetSnap snap={snap} error={error} />
|
||||
</Layout>
|
||||
257
src/styles/look-grid.css
Normal file
257
src/styles/look-grid.css
Normal file
@ -0,0 +1,257 @@
|
||||
/* 走秀 / 街拍 列表页共享样式(从 RunwayLooks.astro / StreetSnaps.astro 抽出)。
|
||||
两模块 UI 同构:左封面 + 右标题 + 底部 5 缩略图条带 + 侧栏筛选 + 分页 + 骨架屏。
|
||||
类名保留各模块原前缀(.shows* / .snaps*),仅把加载动画 keyframes 统一为 looksProg/looksShim。
|
||||
由两个列表组件在前端 frontmatter `import "@/styles/look-grid.css"` 引入,本地 <style> 已删除。 */
|
||||
|
||||
@reference "tailwindcss";
|
||||
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
/* ====================== 顶部 / 主体布局 ====================== */
|
||||
.showsroot,
|
||||
.snapsroot {
|
||||
@apply m-auto max-w-[1600px] pt-7 text-[#0a0a0a];
|
||||
}
|
||||
|
||||
.showshead {
|
||||
@apply flex justify-between items-end gap-6 py-5 pb-[26px];
|
||||
}
|
||||
.snapshead {
|
||||
@apply flex justify-between items-end gap-6 py-5 pb-[22px];
|
||||
}
|
||||
.showsh1 {
|
||||
@apply text-[clamp(40px,6vw,72px)] leading-[0.9] font-black tracking-[-0.02em] my-0 mb-[10px];
|
||||
}
|
||||
.snapsh1 {
|
||||
@apply text-[clamp(34px,5.5vw,64px)] leading-[0.95] font-black tracking-[-0.02em] my-0 mb-[8px];
|
||||
}
|
||||
|
||||
/* 已激活筛选 pills */
|
||||
.showspills { @apply flex flex-wrap gap-2 pt-3.5; }
|
||||
.snapspills { @apply flex flex-wrap gap-2 mb-4; }
|
||||
.showspill,
|
||||
.snapspill {
|
||||
@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,
|
||||
.snapspill .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,
|
||||
.snapsbody {
|
||||
@apply flex gap-[36px] items-start max-md:flex-col max-md:gap-[18px];
|
||||
}
|
||||
|
||||
/* ====================== 左 sidebar(REFINE 筛选) ====================== */
|
||||
.showsaside,
|
||||
.snapsaside {
|
||||
@apply flex-none w-[240px] bg-white sticky top-5 max-h-[calc(100vh-40px)] flex flex-col max-md:static max-md:max-h-none max-md:w-full;
|
||||
}
|
||||
.showsaside {
|
||||
@apply rounded-none transition-[flex-basis] duration-250 ease-in-out;
|
||||
}
|
||||
.showsaside-head,
|
||||
.snapsaside-head {
|
||||
@apply flex items-center justify-between p-[14px_14px_12px] border-b border-black shrink-0;
|
||||
}
|
||||
.showsaside-h,
|
||||
.snapsaside-h {
|
||||
@apply text-[11px] tracking-[0.26em] uppercase font-bold m-0 leading-none transition-opacity duration-200;
|
||||
}
|
||||
.showsaside-body,
|
||||
.snapsaside-body {
|
||||
@apply p-[14px_14px_18px] overflow-y-auto flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#ccc_#fff];
|
||||
}
|
||||
|
||||
/* 筛选分组 */
|
||||
.showsg,
|
||||
.snapsg {
|
||||
@apply border-t border-[#e6e6e6] py-[12px] pb-[6px] first-of-type:border-t-0 first-of-type:pt-1;
|
||||
}
|
||||
.showsg-h,
|
||||
.snapsg-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,
|
||||
.snapsg-chev {
|
||||
@apply inline-block text-[9px] leading-none text-[#999] transition-transform duration-200 ml-1;
|
||||
}
|
||||
.showsg-chev.is-collapsed,
|
||||
.snapsg-chev.is-collapsed { @apply -rotate-90; }
|
||||
.showsg-count {
|
||||
@apply text-[#999] font-normal text-[10px] tracking-[0.14em];
|
||||
}
|
||||
|
||||
/* 复选 / 单选列表 */
|
||||
.showschecks,
|
||||
.snapschecks {
|
||||
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin];
|
||||
}
|
||||
.showschecks li,
|
||||
.snapschecks 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"],
|
||||
.snapschecks 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,
|
||||
.snapschecks 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,
|
||||
.snapschecks--flat li { @apply py-[4px]; }
|
||||
.showschecks--flat li label,
|
||||
.snapschecks--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,
|
||||
.snapsclear {
|
||||
@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,
|
||||
.snapsmain { @apply flex-1 min-w-0; }
|
||||
.snapscontent { @apply relative; }
|
||||
|
||||
.showsgrid,
|
||||
.snapsgrid {
|
||||
@apply grid grid-cols-2 gap-[22px] relative max-[560px]:grid-cols-1;
|
||||
}
|
||||
.showsgrid.is-loading::before,
|
||||
.snapscontent.is-loading::before {
|
||||
content: "";
|
||||
@apply absolute -top-1 left-0 h-[2px] w-[38%] bg-black z-10;
|
||||
animation: looksProg 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.showscard,
|
||||
.snapscard {
|
||||
@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,
|
||||
.snapscard:hover .snapscard-img img { @apply scale-104; }
|
||||
|
||||
.showscard-left,
|
||||
.snapscard-left {
|
||||
@apply flex-none w-[30%] min-w-[110px] max-[700px]:w-full max-[700px]:flex-none;
|
||||
}
|
||||
.showscard-img,
|
||||
.snapscard-img {
|
||||
@apply relative w-full aspect-[3/4] bg-[#f1f1f1] overflow-hidden rounded-none;
|
||||
}
|
||||
.showscard-img img,
|
||||
.snapscard-img img {
|
||||
@apply w-full h-full object-cover transition-transform duration-600 opacity-0;
|
||||
}
|
||||
.showscard-img img.is-loaded,
|
||||
.snapscard-img img.is-loaded { @apply opacity-100; }
|
||||
|
||||
.showsspin,
|
||||
.snapsspin {
|
||||
@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,
|
||||
.snapscard-img.is-loaded .snapsspin,
|
||||
.showscard-thumb.is-loaded .showsspin,
|
||||
.snapscard-thumb.is-loaded .snapsspin { @apply opacity-0; }
|
||||
|
||||
/* runway 专属徽标 */
|
||||
.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,
|
||||
.snapscard-right {
|
||||
@apply flex-1 min-w-0 flex flex-col justify-between max-[700px]:w-full;
|
||||
}
|
||||
.showscard-info,
|
||||
.snapscard-info {
|
||||
@apply flex-none min-w-0 flex flex-col p-[2px_2px_0];
|
||||
}
|
||||
.showscard-title,
|
||||
.snapscard-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,
|
||||
.snapscard-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;
|
||||
}
|
||||
/* runway 专属:卡片底部多行 meta */
|
||||
.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,
|
||||
.snapscard-strip {
|
||||
@apply grid grid-cols-5 gap-1 mt-[10px];
|
||||
}
|
||||
.showscard-thumb,
|
||||
.snapscard-thumb {
|
||||
@apply relative aspect-[3/4] bg-[#f1f1f1] overflow-hidden;
|
||||
}
|
||||
.showscard-thumb img,
|
||||
.snapscard-thumb img {
|
||||
@apply w-full h-full object-cover opacity-0 transition-opacity duration-450 ease-in-out;
|
||||
}
|
||||
.showscard-thumb img.is-loaded,
|
||||
.snapscard-thumb img.is-loaded { @apply opacity-100; }
|
||||
.showscard-thumb.placeholder,
|
||||
.snapscard-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,
|
||||
.snapscard--skel .snapscard-img,
|
||||
.snapscard--skel .snapscard-thumb,
|
||||
.snapscard--skel .snapsline {
|
||||
@apply bg-[#efefef];
|
||||
animation: looksShim 1.6s ease infinite;
|
||||
}
|
||||
|
||||
/* ====================== 分页 ====================== */
|
||||
.showspager,
|
||||
.snappager { @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,
|
||||
.snapsempty {
|
||||
@apply col-span-full p-[80px_20px] text-center text-xs tracking-[0.22em] uppercase text-[#999] border border-dashed border-[#ccc];
|
||||
}
|
||||
|
||||
@keyframes looksProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||
@keyframes looksShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||
Reference in New Issue
Block a user