--- import type { RawArticle } from "@/lib/api" import { getI18n } from "@/i18n/utils" import { href, ROUTES } from '@/lib/routes' import { ssrBase } from "@/lib/api" interface Props { article: RawArticle | null error?: "missing" | "failed" | null preview?: boolean // 后端未登录截断图片集时返回 true,渲染「剩余 N 张」占位遮罩 imageTotal?: number // 图片集真实总数(后端截断前),渲染「剩余 N 张」占位遮罩 } const { article, error = null, preview = false, imageTotal = 0 } = Astro.props const { locale, t } = getI18n(Astro) const full = (u: string): string => { if (!u) return "" if (/^https?:\/\//i.test(u)) return u return ssrBase().replace(/\/$/, "") + u } // 改造后:后端详情接口 images 仅返回主图(look 图),细节图在 details 分组,需经「细节入口」展开。 // 故此处 images 即为主图集合,默认只渲染它们;每张主图带 look_index 用于关联其细节图。 const images = (article?.images ?? []) .map((im) => ({ id: im.id, url: full(im.image || ""), // 缩略图:灯箱左栏缩略图条只显示 76~118px,用后端给的 thumb(240px 样式)即可, // 不必拿 720px 的展示图当缩略图(一场 63 个 look 能省约 1MB)。后端未返回 thumb 时回落展示图。 thumb: full(im.thumb || im.image || ""), raw: im.image || "", // 相对路径:收藏快照存相对地址,不把宿主/端口写死进库 name: im.name || "", lookIndex: Number(im.look_index ?? 0), })) .filter((im) => im.url) // 每个主图(look_index)对应的细节图数量,用于网格卡片左下角标「该主图有几张细节图」。 // 来源与灯箱右下角细节簇一致:后端详情接口的 details 分组(按 look_index 聚合)。 // SSR 首屏图直接查这张表;登录后追加的 extraImages 由 Alpine 的 detailCount() 读同一份数据。 const detailsByLook = new Map() for (const d of (article?.details ?? []) as { look_index?: number; images?: unknown[] }[]) { const li = Number(d.look_index ?? 0) if (li) detailsByLook.set(li, (d.images ?? []).length) } // 未登录预览上限:详情页(图集)未登录最多展示前 PREVIEW_LIMIT 张图片;其余由登录门禁提示, // 已登录(客户端 loadAuthedState 拉全量)或图片本就不超上限时全部展示。前 PREVIEW_LIMIT 张服务端已渲染(SEO/无 JS 友好)。 const PREVIEW_LIMIT = 5 const previewImages = images.slice(0, PREVIEW_LIMIT) // 未登录预览占位遮罩数:补足「剩余 N 张」空图,提示登录后可见,避免误以为图集只有预览上限那几张。 // 已登录(preview=false)或图片本就不超上限时 lockedCount=0,不渲染占位。 const lockedCount = preview ? Math.max(0, (imageTotal || 0) - previewImages.length) : 0 const descParas = (article?.description || "") .split(/\n\n+/) .map((s) => s.trim()) .filter(Boolean) const homeHref = href(locale, ROUTES.home) ---
{ article ? (

{article.title}

{previewImages.length > 0 && (
{/* 每行张数切换(应对图集图片很多的情况):2/3/4/5 列,移动端固定 2 列 */}
{(imageTotal || images.length)} LOOKS
{/* 每行张数切换:只有「3 张 / 5 张」两档,用竖条图标直观表达列数(避免文字数字按钮)。 density 只影响 md+;移动端固定 2 列(5 列在手机上不可用)。 */}
{previewImages.map((im, i) => { const dc = detailsByLook.get(im.lookIndex) || 0 return (
{im.name} {/* 单图收藏:hover 显示,已收藏常驻实心 */} {/* 细节图数量角标:有细节图的主图在左下角标注「张数」(图层图标 + 数字), 点它直接开细节灯箱。纯主图(无细节图)不显示任何角标,保持网格干净。 */} {dc > 0 && ( )}
) })} {/* 未登录预览占位遮罩:补满「剩余 N 张」空图,提示用户登录后可见, 避免误以为图集只有预览上限那几张。已登录(x-show false)自动隐藏,由 extraImages 真实图替补。 */} {lockedCount > 0 && Array.from({ length: lockedCount }).map(() => (
{/* 与真实图片等高的 3:4 图位:锁图标 + 提示,点击跳登录 */}
{t('login to view')}
{/* 真实图已无 look 序号 caption,占位格也改为绝对定位的 +N photos, 不撑高格子,保证遮罩格与真实图片格总高一致 */} +{lockedCount} {t('photos')}
)) } {/* 登录后追加的额外图片:extraImages 由客户端 loadAuthedState() 在已登录时填充,未登录为空不渲染 */}
)} {descParas.length > 0 && (
{descParas.map((p) => (

{p}

))}
)}
) : (

{error === "missing" ? t('No article ID specified') : t('Failed to load article. Please refresh or try later.')}

← {t('Back to Index')}
) } {article && previewImages.length > 0 && ( )} {/* ===== 细节图灯箱:点大图灯箱里的细节缩略图打开,单张细节图放大查看 ===== */} {article && previewImages.length > 0 && ( )}