This commit is contained in:
toom1996
2026-09-14 21:30:15 +08:00
parent 548155625d
commit afff2d9a49
52 changed files with 2997 additions and 2753 deletions

View File

@ -0,0 +1,179 @@
---
// 走秀 / 街拍详情页共享的全屏大图灯箱:左栏缩略图条 + 大图 + 上/下·左/右翻页 + 单图收藏。
//
// 视觉遵循全站「黑白编辑风」:直角细框、mono 小字 + 宽字距、零填充计数、hover 反色(bg-black + 白字)。
// 交互逻辑全部在 src/lib/gallery.ts(createGalleryView);本组件只负责标记。
//
// 各页独有的附加层通过具名插槽 details 注入(走秀页塞「当前 look 的细节图簇」,街拍页不传)。
import { getI18n } from "@/lib/i18n"
interface Props {
/** SSR 首屏图片(前 PREVIEW_LIMIT 张),需带 thumb 供左栏缩略图条使用 */
images: { thumb: string }[]
/** 缩略图比例:走秀 look 为 2/3,街拍为 3/4 */
aspect?: "2/3" | "3/4"
}
const { images, aspect = "2/3" } = Astro.props
const { t } = getI18n(Astro)
// 两个字面量都出现在本文件源码里,保证 Tailwind v4 JIT 能扫描到
const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
---
<div
x-show="lbOpen"
x-cloak
@keydown.window.escape="lbOpen && closeLb()"
@keydown.window.arrow-right="lbOpen && nextLb()"
@keydown.window.arrow-left="lbOpen && prevLb()"
class="fixed inset-0 z-[100] bg-white flex flex-col select-none"
aria-hidden="true"
>
{/* 计数:mono + 0.22em 字距(全站「小标与计数」档),直角细框,零填充 */}
<div
class="absolute top-5 left-1/2 -translate-x-1/2 z-30 border border-black/15 bg-white/90 px-3 py-1 font-mono text-[11px] tracking-[0.22em] text-[#111] backdrop-blur-md"
x-text="`${String(lbIndex + 1).padStart(2, '0')} / ${String(imageCount).padStart(2, '0')}`"
></div>
{/* 单图收藏:圆形,与列表卡片 .fav-toggle / 详情页右下角 #fav-fab 的全站「收藏=圆形」语义保持一致 */}
<button
login-required
type="button"
@click.stop="toggleFav(currentFavId(), currentFavUrl())"
:class="isFav(currentFavId()) ? 'is-fav' : ''"
class="fav-btn absolute top-5 right-[68px] z-30 w-10 h-10 rounded-full border border-black/15 bg-white/90 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black hover:text-white transition-colors cursor-pointer"
aria-label={t('save')}
>
<svg class="w-[18px] h-[18px]" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 21C12 21 4 14.5 4 8.8C4 6.1 6.1 4 8.7 4C10.3 4 11.5 4.8 12 6C12.5 4.8 13.7 4 15.3 4C17.9 4 20 6.1 20 8.8C20 14.5 12 21 12 21Z"/>
</svg>
</button>
<button
type="button"
@click="closeLb()"
class="absolute top-5 right-5 z-30 w-10 h-10 border border-black/15 bg-white/90 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black hover:text-white transition-colors cursor-pointer"
aria-label={t('Close')}
>
<svg class="w-4 h-4" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
<div class="flex-1 min-h-0 flex items-stretch overflow-hidden relative">
<div
id="lb-thumbs"
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 no-scrollbar"
>
{images.map((im, i) => (
<button
type="button"
data-lb-index={i}
data-thumb-url={im.thumb}
@click={`showLb(${i})`}
class={`lb-thumb relative block w-full ${thumbAspect} mb-2 overflow-hidden cursor-pointer transition-opacity duration-200 opacity-40 hover:opacity-90 bg-[#eee] bg-cover bg-center`}
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
aria-label={`${i + 1}`}
>
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm">
{String(i + 1).padStart(2, "0")}
</span>
</button>
))}
<template x-for="(im, i) in extraImages" :key="im.url">
<button
type="button"
:data-lb-index="5 + i"
:data-thumb-url="im.thumb"
@click="showLb(5 + i)"
class={`lb-thumb relative block w-full ${thumbAspect} mb-2 overflow-hidden cursor-pointer transition-opacity duration-200 opacity-40 hover:opacity-90 bg-[#eee] bg-cover bg-center`}
x-bind:class="lbIndex === Number($el.dataset.lbIndex) ? 'opacity-100 ring-1 ring-black' : ''"
:aria-label="String(5 + i + 1)"
>
<span class="absolute left-1 top-1 z-10 bg-black/70 px-1 py-0.5 font-mono text-[8px] sm:text-[9px] tracking-[0.16em] text-white backdrop-blur-sm" x-text="String(5 + i + 1).padStart(2, '0')"></span>
</button>
</template>
</div>
<button
type="button"
@click.stop="prevLb()"
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/90 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black hover:text-white transition-colors cursor-pointer"
aria-label={t('Previous image')}
>
<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
type="button"
@click.stop="nextLb()"
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/90 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black hover:text-white transition-colors cursor-pointer"
aria-label={t('Next image')}
>
<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
type="button"
@click.stop="prevLb()"
class="absolute left-[82px] sm:left-[130px] top-1/2 -translate-y-1/2 z-20 w-10 h-10 border border-black/15 bg-white/90 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black hover:text-white transition-colors cursor-pointer"
aria-label={t('Previous image')}
>
<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
type="button"
@click.stop="nextLb()"
class="absolute right-2 sm:right-5 top-1/2 -translate-y-1/2 z-20 w-10 h-10 border border-black/15 bg-white/90 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black hover:text-white transition-colors cursor-pointer"
aria-label={t('Next image')}
>
<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
id="lb-main"
@click.self="closeLb()"
class="flex-1 min-w-0 min-h-0 relative flex justify-center px-4 sm:px-18"
x-bind:class="lbZoomed
? 'overflow-y-auto overflow-x-hidden items-start'
: 'items-center'"
>
<img
id="lb-image"
src=""
alt=""
@click="toggleZoom()"
decoding="async"
class="block select-none transition-opacity duration-200"
x-bind: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'"
/>
</div>
</div>
<div
class="absolute left-0 right-0 bottom-11 z-20 px-6 text-center font-mono text-[11px] uppercase tracking-[0.2em] text-black/60 pointer-events-none"
x-text="currentImageName"
></div>
<div
x-show="!lbZoomed"
class="absolute left-0 right-0 bottom-4 z-20 px-6 text-center font-mono text-[10px] uppercase tracking-[0.16em] text-black/40 pointer-events-none transition-opacity duration-200"
>
{t('Click image to zoom · Scroll to view · Esc to close')}
</div>
{/* 各页独有的附加层(走秀页注入「当前 look 的细节图簇」;街拍页不传) */}
<slot name="details" />
</div>