@ -8,9 +8,11 @@ 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 } = Astro.props
|
||||
const { article, error = null, preview = false, imageTotal = 0 } = Astro.props
|
||||
const { locale, t } = getI18n(Astro)
|
||||
|
||||
const full = (u: string): string => {
|
||||
@ -28,6 +30,15 @@ const images = (article?.images ?? [])
|
||||
}))
|
||||
.filter((im) => im.url)
|
||||
|
||||
// 未登录预览上限:详情页(图集)未登录最多展示前 PREVIEW_LIMIT 张图片;其余由登录门禁提示,
|
||||
// 已登录(客户端 loadFull 拉全量)或图片本就不超上限时全部展示。前 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())
|
||||
@ -58,6 +69,7 @@ if (article?.brand_id) {
|
||||
data-gallery-uid={article?.id ?? ""}
|
||||
data-gallery-title={article?.title ?? ""}
|
||||
data-gallery-brand={article?.brand_name ?? ""}
|
||||
data-preview={preview ? "true" : "false"}
|
||||
class="min-h-screen bg-white text-black font-sans selection:bg-black selection:text-white"
|
||||
>
|
||||
{
|
||||
@ -73,13 +85,13 @@ if (article?.brand_id) {
|
||||
{article.title}
|
||||
</h1>
|
||||
|
||||
{images.length > 0 && (
|
||||
{previewImages.length > 0 && (
|
||||
<section class="mt-16">
|
||||
<div
|
||||
id="article-gallery"
|
||||
class="grid grid-cols-2 md:grid-cols-3 gap-4"
|
||||
>
|
||||
{images.map((im, i) => (
|
||||
{previewImages.map((im, i) => (
|
||||
<figure
|
||||
data-image-index={i}
|
||||
data-image-url={im.url}
|
||||
@ -118,7 +130,71 @@ if (article?.brand_id) {
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
))}
|
||||
) )}
|
||||
|
||||
{/* 未登录预览占位遮罩:补满「剩余 N 张」空图,提示用户登录后可见,
|
||||
避免误以为图集只有预览上限那几张。已登录(x-show false)自动隐藏,由 extraImages 真实图替补。 */}
|
||||
{lockedCount > 0 &&
|
||||
Array.from({ length: lockedCount }).map(() => (
|
||||
<figure
|
||||
x-show="!isAuthed && preview"
|
||||
x-cloak
|
||||
@click="goLogin()"
|
||||
class="relative bg-[#f2f2f2] cursor-pointer group overflow-hidden"
|
||||
>
|
||||
{/* 与真实图片等高的 3:4 图位:锁图标 + 提示,点击跳登录 */}
|
||||
<div class="w-full aspect-[3/4] flex items-center justify-center text-[#c4c4c4] group-hover:text-[#9a9a9a] 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] font-mono uppercase tracking-[0.18em]">{t('login to view')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 与真实图 figcaption 同高的 caption 行(正常图显示 look01 等小字):
|
||||
占位显示 +N photos,保证遮罩格子与真实图片格子总高一致 */}
|
||||
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#cfcfcf] mt-2">+{lockedCount} {t('photos')}</figcaption>
|
||||
</figure>
|
||||
))
|
||||
}
|
||||
|
||||
{/* 登录后追加的额外图片:extraImages 由客户端 loadFull() 在已登录时填充,未登录为空不渲染 */}
|
||||
<template x-for="(im, i) in extraImages" :key="im.url">
|
||||
<figure
|
||||
:data-image-index="5 + i"
|
||||
: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-[#f2f2f2] 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
|
||||
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>
|
||||
<template x-if="im.name">
|
||||
<figcaption class="text-[11px] font-mono uppercase tracking-widest text-[#999] mt-2" x-text="im.name"></figcaption>
|
||||
</template>
|
||||
</figure>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@ -147,7 +223,7 @@ if (article?.brand_id) {
|
||||
)
|
||||
}
|
||||
|
||||
{article && images.length > 0 && (
|
||||
{article && previewImages.length > 0 && (
|
||||
<div
|
||||
x-show="lbOpen"
|
||||
x-cloak
|
||||
@ -188,7 +264,7 @@ if (article?.brand_id) {
|
||||
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 bg-gradient-to-b from-black/5 to-transparent no-scrollbar"
|
||||
>
|
||||
{images.map((im, i) => (
|
||||
{previewImages.map((im, i) => (
|
||||
<button
|
||||
type="button"
|
||||
data-lb-index={i}
|
||||
@ -205,6 +281,22 @@ if (article?.brand_id) {
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<template x-for="(im, i) in extraImages" :key="im.url">
|
||||
<button
|
||||
type="button"
|
||||
:data-lb-index="5 + i"
|
||||
:data-thumb-url="im.url"
|
||||
@click="showLb(5 + i)"
|
||||
class="lb-thumb relative block w-full aspect-[3/4] mb-2 rounded-[2px] overflow-hidden cursor-pointer transition-all duration-200 opacity-40 hover:opacity-85 bg-[#eee] bg-cover bg-center"
|
||||
x-bind:class="lbIndex === Number($el.dataset.lbIndex)
|
||||
? 'opacity-100 scale-[1.03] ring-2 ring-[#111]'
|
||||
: ''"
|
||||
:aria-label="String(5 + i + 1)"
|
||||
>
|
||||
<span class="absolute left-1 top-1 z-10 font-mono text-[8px] sm:text-[9px] font-semibold tracking-widest text-white px-1 py-0.5 rounded-[3px] bg-black/55 backdrop-blur-sm" x-text="String(5 + i + 1).padStart(2, '0')"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@ -316,7 +408,7 @@ if (article?.brand_id) {
|
||||
// handleTouchEnd(event: TouchEvent): void
|
||||
// }
|
||||
|
||||
import { getUser } from "@/lib/api"
|
||||
import { getUser, getArticleDetailAuthed, toAbs } from "@/lib/api"
|
||||
import { isFavorited, toggleImageFavorite, checkFavorited } from "@/lib/favorites"
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
@ -336,11 +428,22 @@ if (article?.brand_id) {
|
||||
galleryTitle: "",
|
||||
galleryBrand: "",
|
||||
|
||||
// 登录态快照:用响应式属性暴露给 x-show 表达式(Alpine 表达式作用域访问不到模块 import 的 getUser,
|
||||
// 故在此用 getUser() 初始化,避免 x-show="!getUser()..." 求值报错导致遮罩卡在 x-cloak 隐藏)。
|
||||
isAuthed: false,
|
||||
// 未登录门禁:后端截断图片集时为 true(详情页最多看 PREVIEW_LIMIT 张)
|
||||
preview: false,
|
||||
// 已登录后客户端补拉的全量图片(前 PREVIEW_LIMIT 张已由 SSR 服务端渲染)
|
||||
extraImages: [] as { id: string; url: string; raw: string; name: string }[],
|
||||
showAll: false,
|
||||
|
||||
init() {
|
||||
this.favType = this.$el.dataset.favType || "runway_image"
|
||||
this.galleryUid = this.$el.dataset.galleryUid || ""
|
||||
this.galleryTitle = this.$el.dataset.galleryTitle || ""
|
||||
this.galleryBrand = this.$el.dataset.galleryBrand || ""
|
||||
this.preview = this.$el.dataset.preview === "true"
|
||||
this.isAuthed = !!getUser()
|
||||
|
||||
this.imageCount = document.querySelectorAll(
|
||||
"#article-gallery [data-image-index]"
|
||||
@ -349,6 +452,45 @@ if (article?.brand_id) {
|
||||
this.initThumbLazyLoading()
|
||||
this.initImageFavs()
|
||||
this.maybeLocateImageFromUrl()
|
||||
|
||||
// 已登录且后端未登录截断(preview):客户端拉全量图片集并追加渲染
|
||||
if (getUser() && this.preview) this.loadFull()
|
||||
// 登录后(如从门禁跳登录回跳)自动补全全量
|
||||
window.addEventListener("auth:login", () => { this.isAuthed = true; if (this.preview) this.loadFull() })
|
||||
},
|
||||
|
||||
// 已登录用户:拉取完整图片集,把第 PREVIEW_LIMIT+1 张起的图片追加进 extraImages 渲染。
|
||||
// 后端详情接口对已登录(带 Bearer)返回完整图片集,故此处能拿到全量。
|
||||
async loadFull() {
|
||||
if (!this.preview) return
|
||||
try {
|
||||
const full = await getArticleDetailAuthed(this.galleryUid)
|
||||
if (!full || !Array.isArray(full.images)) return
|
||||
const mapped = (full.images || []).slice(5).map((im: any) => ({
|
||||
id: im.id || "",
|
||||
url: toAbs(im.image || ""),
|
||||
raw: im.image || "",
|
||||
name: im.name || "",
|
||||
}))
|
||||
this.extraImages = mapped
|
||||
this.showAll = true
|
||||
await this.$nextTick()
|
||||
this.imageCount = document.querySelectorAll(
|
||||
"#article-gallery [data-image-index]"
|
||||
).length
|
||||
this.initImageFavs()
|
||||
this.initThumbLazyLoading()
|
||||
this.maybeLocateImageFromUrl()
|
||||
} catch {
|
||||
/* 失败维持截断态,门禁仍在 */
|
||||
}
|
||||
},
|
||||
|
||||
// 未登录门禁:跳登录并回跳本篇详情
|
||||
goLogin() {
|
||||
const back = window.location.pathname + window.location.search
|
||||
const loc = window.location.pathname.startsWith("/cn") ? "cn" : "en"
|
||||
window.location.href = `/${loc}/login?redirect=${encodeURIComponent(back)}`
|
||||
},
|
||||
|
||||
// ?img=<图片uid> 深链:个人中心点单图收藏 → 打开所属图集并滚动定位到该图。
|
||||
|
||||
Reference in New Issue
Block a user