--- /** * 详情页图集体(走秀 / 街拍共用)。 * * 设计动机:RunwayLookCard 与 StreetSnapCard 的「标题 + 图集网格 + 未登录占位遮罩 + 大图/副图灯箱」 * 逐块同构,差异只有几处声明式开关: * - aspect 主图与灯箱缩略图比例(当前两页都 2/3 —— 后端素材统一 2:3,见下方 prop 说明) * - showCaptions 图注:街拍有(图片名),走秀无 * - countLabel 工具栏计数文案:LOOKS / PHOTOS * - defaultDensity 默认每行张数(走秀与街拍均 5,都可在工具栏切到 3) * - kind 决定已登录补全量用哪个取数函数(走秀/街拍各一个接口) * - slot after-grid 图集之后的内容(走秀的系列描述;街拍没有) * * 故把「不变的标记」收口在本组件,把上面这些差异用 props / 插槽注入; * 页面级差异(SSR 取数、TDK、meta.type、标题里的 \t 归一)仍留在两个详情视图与两个薄壳里。 * * 与灯箱的关系:大图灯箱 GalleryLightbox / 副图灯箱 GalleryDetailLightbox / 交互 lib/gallery.ts * 早就共享;本组件只是把它们的装配(含 details 插槽)也一并收口。 * * ⚠️ Alpine 脚本注意:脚本是打包模块,读不到 props,故 rootId / alpineName / kind / favType / * defaultDensity 全部经根节点 data-* 传下去,脚本在 alpine:init 时读一次。 */ import GalleryLightbox from "@/components/GalleryLightbox.astro" import GalleryDetailLightbox from "@/components/GalleryDetailLightbox.astro" import DetailCountBadge from "@/components/DetailCountBadge.astro" import LbDetailsStrip from "@/components/LbDetailsStrip.astro" import { getI18n } from "@/lib/i18n" interface GalleryImage { id: string url: string thumb: string raw: string name: string } interface Props { /** 模块身份:决定已登录补全量走哪个详情接口(runway=图集接口 / street=街拍接口) */ kind: "runway" | "street" /** 根节点 id(灯箱交互靠它查根元素上的 data-*) */ rootId: string /** Alpine 组件注册名(两个页面各一个,避免同名互相覆盖) */ alpineName: string /** 图集网格 id(灯箱按它查 [data-image-index] / [data-fav-id]) */ galleryId: string /** 单图收藏类型:runway_image | snap_image */ favType: string /** * 主图与灯箱缩略图比例。 * 后端素材统一 2:3(实测 S4 原图 240×360、high 样式 720×1080,宽高比均 0.6667), * 故走秀与街拍都传 "2/3" —— 此时 object-cover 零裁切;写成 3/4 会把 2:3 的图 * 强行按 0.75 裁掉上下各约 11%(一行 5 张、格子变小时尤其明显)。 * 保留本 prop 只是为将来某模块素材比例真的不同时能单独覆盖,不是"两页各一套"。 */ aspect: "2/3" | "3/4" /** 是否取到了实体(false 时渲染「缺 id / 取数失败」空态)。不用 title 代替:标题允许为空 */ hasItem: boolean /** H1 文案(调用方已处理好多语言 / \t 归一) */ title: string /** SSR 首屏图片(调用方已切到 PREVIEW_LIMIT 张) */ previewImages: GalleryImage[] /** 工具栏计数(真实总数优先) */ totalCount: number /** 计数单位文案:LOOKS / PHOTOS */ countLabel: string /** 后端截断了图集(未登录) */ preview: boolean /** 占位遮罩格数(补足「剩余 N 张」) */ lockedCount: number /** 递给客户端的副图投影(id + detail),与登录后完整 images 同构解析 */ detailPayload: unknown /** 图集元信息:写进 data-*,供收藏 / 灯箱使用 */ galleryUid: string galleryTitle: string galleryBrand: string /** 是否渲染图片名图注(街拍有、走秀无) */ showCaptions?: boolean /** 默认每行张数(移动端固定 2 列,此值只影响 md+) */ defaultDensity?: 3 | 5 /** 取数失败 / 缺 id 时的提示文案(调用方已按语言算好) */ errorText: string /** 空态返回首页链接 */ homeHref: string } const { kind, rootId, alpineName, galleryId, favType, aspect, hasItem, title, previewImages, totalCount, countLabel, preview, lockedCount, detailPayload, galleryUid, galleryTitle, galleryBrand, showCaptions = false, defaultDensity = 5, errorText, homeHref, } = Astro.props const { t } = getI18n(Astro) // 两个字面量都出现在本文件源码里,保证 Tailwind v4 JIT 能扫描到 const imgAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]" const hasImages = previewImages.length > 0 ---
{ hasItem ? (

{title}

{hasImages && (
{/* 工具栏:图集计数 + 每行张数切换(移动端固定 2 列,density 只影响 md+) */}
{totalCount} {countLabel}
{/* 只有「3 张 / 5 张」两档,用竖条图标直观表达列数(避免文字数字按钮) */}
{previewImages.map((im, i) => (
{/* 图片与覆盖层(收藏 / 副图角标)必须包在同一个 relative 盒里: 角标是 absolute bottom-2,若锚点落在含图注的 figure 上,就会压在图注上而不是图片左下角。 */}
{im.name} {/* 单图收藏:hover 显示,已收藏常驻实心 */} {/* 副图数量角标(共享组件):有副图的主图左下角标「张数」,点它直接开副图灯箱 */}
{showCaptions && im.name && (
{im.name}
)}
))} {/* 未登录预览占位遮罩:补满「剩余 N 张」空图,提示用户登录后可见, 避免误以为图集只有预览上限那几张。已登录(x-show false)自动隐藏,由 extraImages 真实图替补。 */} {lockedCount > 0 && Array.from({ length: lockedCount }).map(() => (
{/* 与真实图片等高的图位:锁图标 + 提示,点击跳登录 */}
{t('login to view')}
{/* 与真实图注等高的空白衬垫,保证遮罩格与真实图格总高一致。 这里不放「+N photos」文案:lockedCount 是剩余总数,逐格重复显示会误导。 */} {showCaptions && }
)) } {/* 登录后追加的额外图片:extraImages 由客户端 loadAuthedState() 在已登录时填充,未登录为空不渲染 */}
)} {/* 图集之后的页面专属内容(走秀的系列描述;街拍不传) */}
) : (

{errorText}

← {t('Back to Index')}
) } {/* 大图灯箱(共享):当前主图的副图经 details 插槽注入。左栏缩略图条已移除(见 GalleryLightbox)。 */} {hasImages && ( )} {/* 副图灯箱(共享):点大图灯箱右下角的副图缩略图、或网格左下角角标打开 */} {hasImages && }