Files
frontend_v2/src/components/GalleryLightbox.astro
toom1996 4b409b5a29 update
2026-09-20 00:44:14 +08:00

168 lines
7.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
// 走秀 / 街拍详情页共享的全屏大图灯箱:左栏缩略图条 + 大图 + 上/下·左/右翻页 + 单图收藏。
//
// 视觉遵循全站「黑白编辑风」:直角细框、小字 + 宽字距、零填充计数、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"
>
{/* 计数:小字 + 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 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-[var(--c-gray-200)] 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 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-[var(--c-gray-200)] 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 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>
{/* 各页独有的附加层(走秀页注入「当前 look 的细节图簇」;街拍页不传) */}
<slot name="details" />
</div>