update
This commit is contained in:
@ -1,9 +1,13 @@
|
||||
---
|
||||
/**
|
||||
* 街拍详情卡片(薄壳)。
|
||||
*
|
||||
* 只负责街拍特有的事:把后端 RawStreetSnap 映射成图集数据、算未登录占位格数、
|
||||
* 把标题里的字面 \t(地点 \t 场次)归一成 " · "。标记与交互全部交给共享的
|
||||
* DetailGalleryView(与走秀同一套),差异经 props 声明:带图片名图注等。
|
||||
*/
|
||||
import type { RawStreetSnap } from "@/lib/api"
|
||||
import GalleryLightbox from "@/components/GalleryLightbox.astro"
|
||||
import DetailCountBadge from "@/components/DetailCountBadge.astro"
|
||||
import GalleryDetailLightbox from "@/components/GalleryDetailLightbox.astro"
|
||||
import LbDetailsStrip from "@/components/LbDetailsStrip.astro"
|
||||
import DetailGalleryView from "@/components/DetailGalleryView.astro"
|
||||
import { getI18n } from "@/lib/i18n"
|
||||
import { href, ROUTES } from '@/lib/routes'
|
||||
import { ssrAbs } from "@/lib/request"
|
||||
@ -20,7 +24,7 @@ const { locale, t } = getI18n(Astro)
|
||||
|
||||
const images = (snap?.images ?? [])
|
||||
.map((im) => ({
|
||||
id: im.id,
|
||||
id: im.id ?? "",
|
||||
url: ssrAbs(im.image || ""),
|
||||
// 缩略图:灯箱左栏缩略图条只显示 76~118px,用后端给的 thumb 即可;未返回时回落展示图。
|
||||
thumb: ssrAbs(im.thumb || im.image || ""),
|
||||
@ -30,6 +34,12 @@ const images = (snap?.images ?? [])
|
||||
}))
|
||||
.filter((im) => im.url)
|
||||
|
||||
// 递给浏览器端 Alpine 的副图数据:只序列化「带副图的主图」投影(look_index + detail),
|
||||
// 供 lib/galleryDetail.ts 构建 look_index → 副图 映射(与登录后完整 images 同构解析)。
|
||||
const detailPayload = (snap?.images ?? [])
|
||||
.filter((im) => (im.detail?.length ?? 0) > 0)
|
||||
.map((im) => ({ look_index: Number(im.look_index ?? 0), detail: im.detail }))
|
||||
|
||||
// 未登录预览上限:详情页(图集)未登录最多展示前 PREVIEW_LIMIT 张图片;其余由登录门禁提示,
|
||||
// 已登录(客户端 loadAuthedState 拉全量)或图片本就不超上限时全部展示。前 PREVIEW_LIMIT 张服务端已渲染(SEO/无 JS 友好)。
|
||||
const PREVIEW_LIMIT = 5
|
||||
@ -39,204 +49,36 @@ const previewImages = images.slice(0, PREVIEW_LIMIT)
|
||||
// 已登录(preview=false)或图片本就不超上限时 lockedCount=0,不渲染占位。
|
||||
const lockedCount = preview ? Math.max(0, (imageTotal || 0) - previewImages.length) : 0
|
||||
|
||||
// 递给浏览器端 Alpine 的副图数据:只序列化「带副图的主图」投影(look_index + detail),
|
||||
// 供 lib/galleryDetail.ts 构建 look_index → 副图 映射(与登录后完整 images 同构解析)。
|
||||
const detailPayload = (snap?.images ?? [])
|
||||
.filter((im) => (im.detail?.length ?? 0) > 0)
|
||||
.map((im) => ({ look_index: Number(im.look_index ?? 0), detail: im.detail }))
|
||||
|
||||
const homeHref = href(locale, ROUTES.home)
|
||||
|
||||
// theimpression 标题含字面 \t:地点 \t 场次,列表/详情统一以 " · " 呈现
|
||||
const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
|
||||
|
||||
const errorText = error === "missing"
|
||||
? t('No street snap ID specified')
|
||||
: t('Failed to load street snap. Please refresh or try later.')
|
||||
---
|
||||
|
||||
<div
|
||||
id="street-snap-view"
|
||||
x-data="streetSnapView()"
|
||||
data-fav-type="snap_image"
|
||||
data-gallery-uid={snap?.id ?? ""}
|
||||
data-gallery-title={displayTitle}
|
||||
data-gallery-brand=""
|
||||
data-preview={preview ? "true" : "false"}
|
||||
data-details={JSON.stringify(detailPayload)}
|
||||
class="min-h-screen bg-white text-black font-sans selection:bg-black selection:text-white"
|
||||
>
|
||||
{
|
||||
snap ? (
|
||||
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
|
||||
<h1 class="font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
|
||||
{displayTitle}
|
||||
</h1>
|
||||
|
||||
{previewImages.length > 0 && (
|
||||
<section class="mt-16">
|
||||
<div
|
||||
id="snap-gallery"
|
||||
class="grid grid-cols-2 md:grid-cols-3 gap-4"
|
||||
>
|
||||
{previewImages.map((im, i) => (
|
||||
<figure
|
||||
data-image-index={i}
|
||||
data-look-index={im.lookIndex}
|
||||
data-image-url={im.url}
|
||||
data-image-name={im.name}
|
||||
data-fav-id={im.id}
|
||||
data-fav-url={im.raw}
|
||||
@click={`openLb(${i})`}
|
||||
class="relative bg-[var(--c-gray-100)] cursor-zoom-in group"
|
||||
>
|
||||
<img
|
||||
src={im.url}
|
||||
alt={im.name}
|
||||
loading={i < 6 ? "eager" : "lazy"}
|
||||
decoding="async"
|
||||
class="w-full aspect-[3/4] object-cover group-hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
|
||||
{/* 单图收藏:hover 显示,已收藏常驻实心 */}
|
||||
<button
|
||||
login-required
|
||||
type="button"
|
||||
data-fav-id={im.id}
|
||||
data-fav-url={im.raw}
|
||||
@click.stop="toggleFav($el.dataset.favId, $el.dataset.favUrl)"
|
||||
:class="isFav($el.dataset.favId) ? 'is-fav opacity-100' : 'opacity-0 group-hover:opacity-100'"
|
||||
class="fav-btn absolute right-2 top-2 z-10 w-8 h-8 rounded-full border border-black/15 bg-white/80 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black/5 hover:border-black/30 transition-all cursor-pointer"
|
||||
aria-label={t('save')}
|
||||
>
|
||||
<svg class="w-4 h-4" 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>
|
||||
|
||||
{/* 副图数量角标(共享组件):有副图的主图左下角标「张数」,点它开副图灯箱 */}
|
||||
<DetailCountBadge look={String(im.lookIndex)} />
|
||||
|
||||
{im.name && (
|
||||
<figcaption class="text-[11px] uppercase tracking-widest text-[var(--c-gray-600)] mt-2">
|
||||
{im.name}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
) )}
|
||||
|
||||
{/* 未登录预览占位遮罩:补满「剩余 N 张」空图,提示用户登录后可见,
|
||||
避免误以为图集只有预览上限那几张。已登录(x-show false)自动隐藏,由 extraImages 真实图替补。 */}
|
||||
{lockedCount > 0 &&
|
||||
Array.from({ length: lockedCount }).map(() => (
|
||||
<figure
|
||||
x-show="!isAuthed && preview"
|
||||
x-cloak
|
||||
login-redirect
|
||||
class="relative bg-[var(--c-gray-100)] cursor-pointer group overflow-hidden"
|
||||
>
|
||||
{/* 与真实图片等高的 3:4 图位:锁图标 + 提示,点击跳登录 */}
|
||||
<div class="w-full aspect-[3/4] flex items-center justify-center text-[var(--c-gray-600)] group-hover:text-[var(--c-gray-700)] transition-colors pointer-events-none">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg class="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4.5" y="10.5" width="15" height="10" rx="2" />
|
||||
<path d="M8 10.5V7a4 4 0 0 1 8 0v3.5" />
|
||||
</svg>
|
||||
<span class="text-[10px] uppercase tracking-[0.18em]">{t('login to view')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 与真实图 figcaption 同高的 caption 行(正常图显示 img01 等小字):
|
||||
占位显示 +N photos,保证遮罩格子与真实图片格子总高一致 */}
|
||||
<figcaption class="text-[11px] uppercase tracking-widest text-[var(--c-gray-600)] mt-2">+{lockedCount} {t('photos')}</figcaption>
|
||||
</figure>
|
||||
))
|
||||
}
|
||||
|
||||
{/* 登录后追加的额外图片:extraImages 由客户端 loadAuthedState() 在已登录时填充,未登录为空不渲染 */}
|
||||
<template x-for="(im, i) in extraImages" :key="im.url" >
|
||||
<figure
|
||||
:data-image-index="5 + i"
|
||||
:data-look-index="im.lookIndex"
|
||||
:data-image-url="im.url"
|
||||
:data-image-name="im.name"
|
||||
:data-fav-id="im.id"
|
||||
:data-fav-url="im.raw"
|
||||
@click="openLb(5 + i)"
|
||||
class="relative bg-[var(--c-gray-100)] cursor-zoom-in group"
|
||||
>
|
||||
<img
|
||||
:src="im.url"
|
||||
:alt="im.name"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full aspect-[3/4] object-cover group-hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
<button
|
||||
login-required
|
||||
type="button"
|
||||
:data-fav-id="im.id"
|
||||
:data-fav-url="im.raw"
|
||||
@click.stop="toggleFav($el.dataset.favId, $el.dataset.favUrl)"
|
||||
:class="isFav($el.dataset.favId) ? 'is-fav opacity-100' : 'opacity-0 group-hover:opacity-100'"
|
||||
class="fav-btn absolute right-2 top-2 z-10 w-8 h-8 rounded-full border border-black/15 bg-white/80 text-[#111] flex items-center justify-center backdrop-blur-md hover:bg-black/5 hover:border-black/30 transition-all cursor-pointer"
|
||||
aria-label={t('save')}
|
||||
>
|
||||
<svg class="w-4 h-4" 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>
|
||||
<DetailCountBadge look="im.lookIndex" />
|
||||
<template x-if="im.name">
|
||||
<figcaption class="text-[11px] uppercase tracking-widest text-[var(--c-gray-600)] mt-2" x-text="im.name"></figcaption>
|
||||
</template>
|
||||
</figure>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
</article>
|
||||
) : (
|
||||
<div class="flex flex-col items-center justify-center min-h-[60vh] px-5 text-center">
|
||||
<p class="text-sm text-[var(--c-gray-700)] uppercase tracking-widest mb-4">
|
||||
{error === "missing" ? t('No street snap ID specified') : t('Failed to load street snap. Please refresh or try later.')}
|
||||
</p>
|
||||
|
||||
|
||||
<a
|
||||
href={homeHref}
|
||||
class="text-[12px] uppercase tracking-[0.2em] border border-black px-4 py-2 hover:bg-black hover:text-white transition-colors"
|
||||
>
|
||||
← {t('Back to Index')}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 大图灯箱:与走秀详情共用 components/GalleryLightbox.astro(标记)+ lib/gallery.ts(交互)。
|
||||
当前主图的副图经 details 插槽注入(与走秀一致)。 */}
|
||||
{snap && previewImages.length > 0 && (
|
||||
<GalleryLightbox images={previewImages} aspect="3/4">
|
||||
<LbDetailsStrip slot="details" />
|
||||
</GalleryLightbox>
|
||||
)}
|
||||
{/* 副图(细节)灯箱:点大图灯箱右下角的副图缩略图打开 */}
|
||||
{snap && previewImages.length > 0 && <GalleryDetailLightbox />}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { getStreetSnapDetailAuthed } from "@/lib/api"
|
||||
import { createGalleryView } from "@/lib/gallery"
|
||||
import { createDetailGallery } from "@/lib/galleryDetail"
|
||||
|
||||
// 街拍详情:与走秀共用「图集 + 大图灯箱 + 细节(副图)灯箱」
|
||||
// (lib/gallery.ts + lib/galleryDetail.ts),无额外扩展。
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("streetSnapView", () =>
|
||||
createDetailGallery(
|
||||
createGalleryView({
|
||||
rootSelector: "#street-snap-view",
|
||||
gallerySelector: "#snap-gallery",
|
||||
favType: "snap_image",
|
||||
fetchDetail: getStreetSnapDetailAuthed,
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
</script>
|
||||
<DetailGalleryView
|
||||
kind="street"
|
||||
rootId="street-snap-view"
|
||||
alpineName="streetSnapView"
|
||||
galleryId="snap-gallery"
|
||||
favType="snap_image"
|
||||
aspect="2/3"
|
||||
hasItem={!!snap}
|
||||
title={displayTitle}
|
||||
previewImages={previewImages}
|
||||
totalCount={imageTotal || images.length}
|
||||
countLabel="PHOTOS"
|
||||
preview={preview}
|
||||
lockedCount={lockedCount}
|
||||
detailPayload={detailPayload}
|
||||
galleryUid={snap?.id ?? ""}
|
||||
galleryTitle={displayTitle}
|
||||
galleryBrand=""
|
||||
showCaptions
|
||||
defaultDensity={5}
|
||||
errorText={errorText}
|
||||
homeHref={homeHref}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user