128 lines
4.7 KiB
Plaintext
128 lines
4.7 KiB
Plaintext
---
|
||
import Layout from "@/layouts/Layout.astro"
|
||
import Item from "@/components/Item.astro"
|
||
import StreetSnap from "@/components/StreetSnap.astro"
|
||
import { getSsrArticle, getSsrStreetSnap } from "@/lib/api"
|
||
import { getI18n } from "@/i18n/utils"
|
||
|
||
// 按需 SSR:走秀(runway)与街拍(street snap)详情统一收口到 /item/[id]。
|
||
// 对外 id 带类型前缀(r=runway / s=snap,见后端 hashid.EncodeTyped),据此直接查对应表,无歧义、无需回落。
|
||
export const prerender = false
|
||
|
||
const { id } = Astro.params
|
||
const isSnap = !!id && id[0] === "s"
|
||
|
||
const { t } = getI18n(Astro)
|
||
|
||
let article = null
|
||
let articleErr = null
|
||
let snap = null
|
||
let snapErr = null
|
||
let preview = false
|
||
let imageTotal = 0
|
||
|
||
// await 必须位于 frontmatter 顶层(Astro 不支持块内顶层 await)
|
||
const data = isSnap
|
||
? await getSsrStreetSnap(id, Astro.currentLocale)
|
||
: await getSsrArticle(id, Astro.currentLocale)
|
||
|
||
if (isSnap) {
|
||
snap = data.snap
|
||
snapErr = data.error
|
||
} else {
|
||
article = data.article
|
||
articleErr = data.error
|
||
}
|
||
preview = data.preview ?? false
|
||
imageTotal = data.imageTotal ?? 0
|
||
|
||
// 供客户端「收藏 / 历史」使用的精简元信息(服务端已拿到,直接透传,避免前端再发请求)。
|
||
const meta = isSnap
|
||
? { id: id ?? "", type: "street", title: snap?.title ?? "", cover: snap?.cover ?? "", brand: "" }
|
||
: { id: id ?? "", type: "runway", title: article?.title ?? "", cover: article?.cover ?? "", brand: article?.brand_name ?? "" }
|
||
const saveLabel = t("save")
|
||
const savedLabel = t("saved")
|
||
---
|
||
|
||
<Layout>
|
||
{article ? <Item article={article} error={articleErr} preview={preview} imageTotal={imageTotal} /> : <StreetSnap snap={snap} error={snapErr} preview={preview} imageTotal={imageTotal} />}
|
||
</Layout>
|
||
|
||
<style>
|
||
#fav-fab.is-active .fav-heart { fill: currentColor; }
|
||
</style>
|
||
|
||
{(article || snap) && (
|
||
<button id="fav-fab" type="button"
|
||
data-id={meta.id}
|
||
data-type={meta.type}
|
||
data-title={meta.title}
|
||
data-cover={meta.cover}
|
||
data-brand={meta.brand}
|
||
data-save-label={saveLabel}
|
||
data-saved-label={savedLabel}
|
||
class="fixed bottom-6 right-6 z-50 inline-flex items-center gap-2 rounded-full border border-black/40 bg-white/90 px-5 py-3 text-[11px] font-bold uppercase tracking-[0.2em] text-black shadow-sm backdrop-blur transition-colors hover:bg-black hover:text-white">
|
||
<svg class="fav-heart h-4 w-4" viewBox="0 0 24 24" fill="none" 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>
|
||
<span id="fav-label">{saveLabel}</span>
|
||
</button>
|
||
)}
|
||
|
||
{/* 注意:不能用 define:vars 传 meta —— 它会把脚本包成内联 IIFE 并把 import 塞进函数体内,
|
||
导致 SyntaxError(import 只能在模块顶层)。故改由 data-* 属性透传,脚本自行读取。 */}
|
||
<script>
|
||
import { getUser } from "@/lib/api"
|
||
import { isFavorited, toggleFavorite, checkFavorited, hydrateFavorited } from "@/lib/favorites"
|
||
import { recordHistory } from "@/lib/history"
|
||
|
||
const btn = document.getElementById("fav-fab")
|
||
const label = document.getElementById("fav-label")
|
||
if (btn) {
|
||
const meta = {
|
||
id: btn.dataset.id || "",
|
||
type: btn.dataset.type || "",
|
||
title: btn.dataset.title || "",
|
||
cover: btn.dataset.cover || "",
|
||
brand: btn.dataset.brand || "",
|
||
}
|
||
const saveLabel = btn.dataset.saveLabel || "save"
|
||
const savedLabel = btn.dataset.savedLabel || "saved"
|
||
|
||
if (meta.id) {
|
||
// 浏览历史:点开文章详情即记一条(登录态;列表页不再记)。同一篇反复打开,后端 upsert 刷新时间。
|
||
if (getUser()) recordHistory(meta.id)
|
||
|
||
const sync = () => {
|
||
const fav = isFavorited(meta.id)
|
||
if (label) label.textContent = fav ? savedLabel : saveLabel
|
||
btn.classList.toggle("is-active", fav)
|
||
}
|
||
sync()
|
||
|
||
// 登录态:异步问服务端当前篇是否已收藏并写回本地(换浏览器/设备后,爱心状态与服务端一致)
|
||
if (getUser()) {
|
||
checkFavorited([meta.id])
|
||
.then((set) => {
|
||
if (set.has(meta.id)) hydrateFavorited(meta.id, meta.type as "runway" | "street", meta.title, meta.cover, meta.brand)
|
||
sync()
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
|
||
btn.addEventListener("click", () => {
|
||
if (!getUser()) {
|
||
window.location.href = "/login"
|
||
return
|
||
}
|
||
toggleFavorite({
|
||
id: meta.id,
|
||
type: meta.type,
|
||
title: meta.title,
|
||
cover: meta.cover,
|
||
brand: meta.brand,
|
||
} as any)
|
||
sync()
|
||
})
|
||
}
|
||
}
|
||
</script>
|