168 lines
7.5 KiB
Plaintext
168 lines
7.5 KiB
Plaintext
---
|
||
// 走秀 / 街拍详情页共享的全屏大图灯箱:左栏缩略图条 + 大图 + 上/下·左/右翻页 + 单图收藏。
|
||
//
|
||
// 视觉遵循全站「黑白编辑风」:直角细框、小字 + 宽字距、零填充计数、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>
|