32
.workbuddy/memory/2026-08-22.md
Normal file
32
.workbuddy/memory/2026-08-22.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# 2026-08-22 工作记忆
|
||||||
|
|
||||||
|
## 重构:把 SSR / SSG 请求 base URL 从 astro 文件抽离到 lib
|
||||||
|
用户要求:重新封装 SSR 和 SSG 请求,baseurl 不要放在 .astro 文件里。
|
||||||
|
|
||||||
|
### 改动清单
|
||||||
|
- **新建 `src/lib/ssr.ts`**:集中 SSR(按需渲染)数据客户端。
|
||||||
|
- `ssrBase()`:解析优先级 `API_SSR` > `PUBLIC_API_BASE` > `http://localhost:8090`(替代之前散布在 article 页面的内联写法)。
|
||||||
|
- `getSsrArticle(id)`:包揽「缺 id / 取数失败 / 成功」三态,返回 `{article, error}`。
|
||||||
|
- **`src/pages/en/article.astro` / `cn/article.astro`**:删除 `const API = (import.meta.env.API_SSR || PUBLIC_API_BASE || "http://localhost:8090")...`,改用 `getSsrArticle(id)`;不再给组件传 `apiBase`。
|
||||||
|
- **`src/pages/en/items/[id].astro`**:删掉**硬编码** `const API = "http://localhost:8090"`(最严重的一处)和调试 `console.log(id/article)`,改用 `getSsrArticle(id)`。
|
||||||
|
- **`src/components/pages/article.astro` 与 `RunwayLooksDetail.astro`**:移除 `apiBase` prop,改用 `ssrBase()` 内部解析图片绝对地址(原来 `apiBase` 只用于服务端 frontmatter 的 `full()`,浏览器 `<script>` 用的是服务端注入的 `images` 数据,不依赖该 prop)。
|
||||||
|
- **`src/lib/api.ts`** 新增供客户端脚本复用:
|
||||||
|
- `publicBase()`:仅 `PUBLIC_API_BASE`(空→同源相对 `/api`),与 `getBase()`(含 SSR/DEV 覆盖)区分。
|
||||||
|
- `toAbsPublic(url)`:基于 `publicBase()` 的相对路径补全。
|
||||||
|
- `fetchArticlesList({page,size,collection_type,season,brand_id})`:替代 articles.astro 内联的 `fetch('/api/public/articles?...')`。
|
||||||
|
- **`src/components/pages/articles.astro` 客户端脚本**:删除内联 `const API_BASE = import.meta.env.PUBLIC_API_BASE...` 与 `toAbs` 闭包,改用 `fetchArticlesList` + `toAbsPublic`;并补声明原本**未定义**却被引用的 `let fType = ""` / `let fSeason = ""`(之前因 `!API_BASE` 提前 return 才不报错,属潜在 bug)。
|
||||||
|
- 顺手清理调试日志:`data.ts` 的 `fetchArticleById` 内 `console.log`、 `ssg.ts` 的 `getSsgPopularBrands` 内 `console.log(j)`。
|
||||||
|
|
||||||
|
### 关于 SSG
|
||||||
|
SSG 请求本身**已封装在 `src/lib/ssg.ts`**(`ssgBase()` / `getSsgBrands` / `getSsgPopularBrands` / `getSsgArticleIds`,基 `SSG_API`> `API_SSR` > `http://localhost:8091`),`brands.astro` 与 `Index.astro` 在用,本次未改动其结构,仅清掉一处调试日志。
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
- `npm run build` 通过(Complete!)。
|
||||||
|
- Lint 0 报错。
|
||||||
|
- `astro dev` 下 `/en/article?id=1` → 200、`/en/items/1` → 200(后端离线时走 error 态,不影响编译/路由)。
|
||||||
|
|
||||||
|
### 文件职责(请求层)
|
||||||
|
- `ssr.ts`:运行期按需 SSR,调 `/api/public/*`,base=API_SSR。
|
||||||
|
- `ssg.ts`:构建期 Node,调 `/api/ssg/*`,base=SSG_API(内部端口 8091)。
|
||||||
|
- `api.ts`:客户端运行期通用 API(RunwayLooks 等用 `getBase()`;articles 用 `publicBase()`)。
|
||||||
|
- `data.ts`:网站统一数据层(首页/列表 getter),`API_BASE` 仍基于 `PUBLIC_API_BASE`,属 lib 内逻辑、不在 astro 文件。
|
||||||
@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
|
import { getI18n } from '@/i18n/utils'
|
||||||
|
import { href, PATH } from '@/lib/routes'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
module?: string
|
module?: string
|
||||||
moduleZh?: string
|
moduleZh?: string
|
||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
const { module = "This module", moduleZh = "", note = "" } = Astro.props
|
const { module = "This module", moduleZh = "", note = "" } = Astro.props
|
||||||
|
const { locale } = getI18n(Astro)
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="cs-wrap">
|
<section class="cs-wrap">
|
||||||
@ -18,8 +22,8 @@ const { module = "This module", moduleZh = "", note = "" } = Astro.props
|
|||||||
{note || "该模块正在筹备中,尚未对外开放。站点正按模块逐个完善,你可以先浏览已上线的走秀档案。"}
|
{note || "该模块正在筹备中,尚未对外开放。站点正按模块逐个完善,你可以先浏览已上线的走秀档案。"}
|
||||||
</p>
|
</p>
|
||||||
<div class="cs-actions">
|
<div class="cs-actions">
|
||||||
<a class="cs-btn" href="/articles">浏览走秀档案 →</a>
|
<a class="cs-btn" href={href(locale, PATH.articles)}>浏览走秀档案 →</a>
|
||||||
<a class="cs-btn-ghost" href="/">返回首页</a>
|
<a class="cs-btn-ghost" href={href(locale, PATH.home)}>返回首页</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import ComingSoon from "@/components/ComingSoon.astro"
|
import ComingSoon from "@/components/ComingSoon.astro"
|
||||||
import { toAbs } from "@/lib/api"
|
import { toAbs } from "@/lib/api"
|
||||||
import { getI18n } from "@/i18n/utils"
|
import { getI18n } from "@/i18n/utils"
|
||||||
import { getRelativeLocaleUrl } from 'astro:i18n'
|
import { href, PATH } from '@/lib/routes'
|
||||||
import { getSsgPopularBrands } from "@/lib/ssg"
|
import { getSsgPopularBrands } from "@/lib/ssg"
|
||||||
|
|
||||||
// 国际化:一行取当前语言 + 翻译函数
|
// 国际化:一行取当前语言 + 翻译函数
|
||||||
@ -66,7 +66,7 @@ const galleryItems: GalleryItem[] = popular
|
|||||||
D
|
D
|
||||||
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
||||||
</li>
|
</li>
|
||||||
<a href="/brands" class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap hover:opacity-80">E</a>
|
<a href={href(locale, PATH.brands)} class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap hover:opacity-80">E</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ const galleryItems: GalleryItem[] = popular
|
|||||||
{
|
{
|
||||||
galleryItems.map((g, i) => (
|
galleryItems.map((g, i) => (
|
||||||
<div class="swiper-slide hs-slide">
|
<div class="swiper-slide hs-slide">
|
||||||
<a class="hs-item" href={getRelativeLocaleUrl(locale, `/items/${g.articleId}`)} data-idx={i}>
|
<a class="hs-item" href={href(locale, PATH.item(g.articleId))} data-idx={i}>
|
||||||
<!-- Alpine 管理单张图片的加载/失败状态 -->
|
<!-- Alpine 管理单张图片的加载/失败状态 -->
|
||||||
<div
|
<div
|
||||||
class="hs-img-box"
|
class="hs-img-box"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
||||||
|
import { clientHref, PATH } from '@/lib/routes'
|
||||||
import { getI18n } from '@/i18n/utils'
|
import { getI18n } from '@/i18n/utils'
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +24,6 @@ try {
|
|||||||
if (!yearChips.length) yearChips = yearSeed
|
if (!yearChips.length) yearChips = yearSeed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="showsroot w-full" x-data="showsPage()" data-locale={locale}>
|
<div class="showsroot w-full" x-data="showsPage()" data-locale={locale}>
|
||||||
@ -185,7 +185,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
<!-- Card List -->
|
<!-- Card List -->
|
||||||
<template x-for="it in items" :key="it.id">
|
<template x-for="it in items" :key="it.id">
|
||||||
<a class="showscard" :href="`/${locale}/runway-looks/${it.id}`" :aria-label="`${it.brand_name} ${it.title}`">
|
<a class="showscard" :href="itemHref(it)" :aria-label="`${it.brand_name} ${it.title}`">
|
||||||
<div class="showscard-left">
|
<div class="showscard-left">
|
||||||
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||||
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||||
@ -270,7 +270,6 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script id="showsbrands-data" type="application/json" set:html={JSON.stringify(sidebarBrands)}></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@reference "tailwindcss";
|
@reference "tailwindcss";
|
||||||
@ -585,7 +584,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
reqToken: 0,
|
reqToken: 0,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.locale = this.$el.dataset.locale || 'zh-cn';
|
this.locale = this.$el.dataset.locale || 'en';
|
||||||
this.loadPage(1, false)
|
this.loadPage(1, false)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -717,6 +716,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
if (kw) {
|
if (kw) {
|
||||||
this.modalLetter = ""
|
this.modalLetter = ""
|
||||||
this.modalLoading = true
|
this.modalLoading = true
|
||||||
|
this.modalResults = []
|
||||||
try {
|
try {
|
||||||
this.modalResults = await getBrands({ keyword: kw, size: 60 })
|
this.modalResults = await getBrands({ keyword: kw, size: 60 })
|
||||||
} catch {
|
} catch {
|
||||||
@ -809,6 +809,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
// 辅助辅助工具函数
|
// 辅助辅助工具函数
|
||||||
toAbsUrl(url: string) { return toAbs(url) },
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
|
itemHref(it: { id: string | number }) { return clientHref(this.locale, PATH.runwayLooks(it.id)) },
|
||||||
ctLabel(ct?: string) {
|
ctLabel(ct?: string) {
|
||||||
const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall" }
|
const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall" }
|
||||||
return ct && map[ct] ? map[ct] : (ct || "")
|
return ct && map[ct] ? map[ct] : (ct || "")
|
||||||
|
|||||||
@ -2,20 +2,21 @@
|
|||||||
import type { RawArticle } from "@/lib/data"
|
import type { RawArticle } from "@/lib/data"
|
||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getI18n } from "@/i18n/utils"
|
import { getI18n } from "@/i18n/utils"
|
||||||
|
import { href, PATH } from '@/lib/routes'
|
||||||
|
import { ssrBase } from "@/lib/ssr"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
article: RawArticle | null
|
article: RawArticle | null
|
||||||
apiBase?: string
|
|
||||||
error?: "missing" | "failed" | null
|
error?: "missing" | "failed" | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { article, apiBase = "", error = null } = Astro.props
|
const { article, error = null } = Astro.props
|
||||||
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
|
const { locale, t } = getI18n(Astro)
|
||||||
|
|
||||||
const full = (u: string): string => {
|
const full = (u: string): string => {
|
||||||
if (!u) return ""
|
if (!u) return ""
|
||||||
if (/^https?:\/\//i.test(u)) return u
|
if (/^https?:\/\//i.test(u)) return u
|
||||||
return apiBase.replace(/\/$/, "") + u
|
return ssrBase().replace(/\/$/, "") + u
|
||||||
}
|
}
|
||||||
|
|
||||||
const images = (article?.images ?? [])
|
const images = (article?.images ?? [])
|
||||||
@ -31,8 +32,8 @@ const descParas = (article?.description || "")
|
|||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
const homeHref = getRelativeLocaleUrl(locale, "/")
|
const homeHref = href(locale, PATH.home)
|
||||||
const archiveHref = getRelativeLocaleUrl(locale, "/shows")
|
const archiveHref = href(locale, PATH.shows)
|
||||||
|
|
||||||
const crumbs: { label: string; href: string }[] = [
|
const crumbs: { label: string; href: string }[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
---
|
---
|
||||||
import type { RawArticle } from "@/lib/data"
|
import type { RawArticle } from "@/lib/data"
|
||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
|
import { ssrBase } from "@/lib/ssr"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
article: RawArticle | null
|
article: RawArticle | null
|
||||||
locale?: "en" | "cn"
|
locale?: "en" | "cn"
|
||||||
apiBase?: string
|
|
||||||
error?: "missing" | "failed" | null
|
error?: "missing" | "failed" | null
|
||||||
}
|
}
|
||||||
const { article, locale = "en", apiBase = "", error = null } = Astro.props
|
const { article, locale = "en", error = null } = Astro.props
|
||||||
|
|
||||||
// 把相对图片地址补全为绝对地址(服务端完成,避免浏览器再拼)
|
// 把相对图片地址补全为绝对地址(服务端完成,避免浏览器再拼;base 由 ssr.ts 统一解析)
|
||||||
const full = (u: string): string =>
|
const full = (u: string): string =>
|
||||||
!u ? "" : /^https?:\/\//i.test(u) ? u : apiBase.replace(/\/$/, "") + u
|
!u ? "" : /^https?:\/\//i.test(u) ? u : ssrBase().replace(/\/$/, "") + u
|
||||||
|
|
||||||
// 图片数组:服务端预处理为灯箱可用的 {id,url,name}
|
// 图片数组:服务端预处理为灯箱可用的 {id,url,name}
|
||||||
const images = (article?.images ?? []).map((im) => ({
|
const images = (article?.images ?? []).map((im) => ({
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { getReports, fmtDate } from "@/lib/data"
|
import { getReports, fmtDate } from "@/lib/data"
|
||||||
|
import { href, PATH } from '@/lib/routes'
|
||||||
import { getI18n } from "@/i18n/utils"
|
import { getI18n } from "@/i18n/utils"
|
||||||
|
|
||||||
// 文章展示创意布局页:复用你现有的 Layout 外壳。
|
// 文章展示创意布局页:复用你现有的 Layout 外壳。
|
||||||
@ -8,8 +9,8 @@ import { getI18n } from "@/i18n/utils"
|
|||||||
|
|
||||||
const reports = await getReports(30)
|
const reports = await getReports(30)
|
||||||
const fmt = fmtDate
|
const fmt = fmtDate
|
||||||
const { locale, getRelativeLocaleUrl } = getI18n(Astro)
|
const { locale } = getI18n(Astro)
|
||||||
const itemBase = getRelativeLocaleUrl(locale, "/items/").replace(/\/$/, "")
|
const itemBase = href(locale, "/items")
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="archive" data-item-base={itemBase}>
|
<div class="archive" data-item-base={itemBase}>
|
||||||
@ -24,7 +25,7 @@ const itemBase = getRelativeLocaleUrl(locale, "/items/").replace(/\/$/, "")
|
|||||||
<p>
|
<p>
|
||||||
走秀趋势报告档案库。支持网格 / 杂志 / 时间轴三种创意视图自由切换,彩色配图由后端接口动态渲染。
|
走秀趋势报告档案库。支持网格 / 杂志 / 时间轴三种创意视图自由切换,彩色配图由后端接口动态渲染。
|
||||||
</p>
|
</p>
|
||||||
<a href="/" class="archive-back">← Index</a>
|
<a href={href(locale, PATH.home)} class="archive-back">← Index</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ const itemBase = getRelativeLocaleUrl(locale, "/items/").replace(/\/$/, "")
|
|||||||
style={`--ratio:${s.ratio}`}
|
style={`--ratio:${s.ratio}`}
|
||||||
>
|
>
|
||||||
<div class="card-datecol"><span class="card-date">{fmt(s.created_at)}</span></div>
|
<div class="card-datecol"><span class="card-date">{fmt(s.created_at)}</span></div>
|
||||||
<a class="card-media" href={`${itemBase}/${s.id}`} aria-label={s.title}>
|
<a class="card-media" href={href(locale, PATH.item(s.id))} aria-label={s.title}>
|
||||||
<img src={s.cover} alt={s.title} loading="lazy" />
|
<img src={s.cover} alt={s.title} loading="lazy" />
|
||||||
{s.featured && <span class="card-flag">Featured</span>}
|
{s.featured && <span class="card-flag">Featured</span>}
|
||||||
</a>
|
</a>
|
||||||
@ -266,18 +267,17 @@ const itemBase = getRelativeLocaleUrl(locale, "/items/").replace(/\/$/, "")
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { fetchArticlesList, toAbsPublic } from "@/lib/api"
|
||||||
|
|
||||||
const archive = document.querySelector<HTMLElement>(".archive")
|
const archive = document.querySelector<HTMLElement>(".archive")
|
||||||
const gallery = document.getElementById("gallery") as HTMLElement | null
|
const gallery = document.getElementById("gallery") as HTMLElement | null
|
||||||
const countEl = document.getElementById("count")
|
const countEl = document.getElementById("count")
|
||||||
const filterbar = document.getElementById("filterbar")
|
const filterbar = document.getElementById("filterbar")
|
||||||
const viewBtns = Array.from(document.querySelectorAll<HTMLButtonElement>(".viewswitch button"))
|
const viewBtns = Array.from(document.querySelectorAll<HTMLButtonElement>(".viewswitch button"))
|
||||||
const API_BASE = (import.meta.env.PUBLIC_API_BASE ?? "").replace(/\/$/, "")
|
// 系列类型 / 季节筛选状态(对应 #typefilter / #seasonfilter 按钮;仅后端接口分支使用)
|
||||||
|
let fType = ""
|
||||||
|
let fSeason = ""
|
||||||
const itemBase = archive?.dataset.itemBase || "/items"
|
const itemBase = archive?.dataset.itemBase || "/items"
|
||||||
// 封面可能是完整外链(https://...),也可能是相对路径(/uploads/...),统一处理
|
|
||||||
const toAbs = (url: string) => {
|
|
||||||
if (!url) return ""
|
|
||||||
return /^https?:\/\//i.test(url) ? url : API_BASE + url
|
|
||||||
}
|
|
||||||
|
|
||||||
// 来自品牌索引的聚焦筛选(/brands 的品牌链接:?brand_id= 或 ?brand=)
|
// 来自品牌索引的聚焦筛选(/brands 的品牌链接:?brand_id= 或 ?brand=)
|
||||||
const _urlParams = new URLSearchParams(location.search)
|
const _urlParams = new URLSearchParams(location.search)
|
||||||
@ -420,18 +420,13 @@ const itemBase = getRelativeLocaleUrl(locale, "/items/").replace(/\/$/, "")
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadBackend = async () => {
|
const loadBackend = async () => {
|
||||||
if (!API_BASE) return
|
|
||||||
try {
|
try {
|
||||||
const size = _brandId ? 100 : 30
|
const rows = await fetchArticlesList({
|
||||||
const p = new URLSearchParams({ page: "1", size: String(size) })
|
size: _brandId ? 100 : 30,
|
||||||
if (fType) p.set("collection_type", fType)
|
collection_type: fType || undefined,
|
||||||
if (fSeason) p.set("season", fSeason)
|
season: fSeason || undefined,
|
||||||
if (_brandId) p.set("brand_id", _brandId)
|
brand_id: _brandId || undefined,
|
||||||
const url = `${API_BASE}/api/public/articles?${p.toString()}`
|
})
|
||||||
const res = await fetch(url)
|
|
||||||
if (!res.ok) throw new Error("bad status")
|
|
||||||
const json = await res.json()
|
|
||||||
const rows = json?.data ?? []
|
|
||||||
if (!Array.isArray(rows) || rows.length === 0) return
|
if (!Array.isArray(rows) || rows.length === 0) return
|
||||||
let items: Item[] = rows.map((r: any, i: number) => ({
|
let items: Item[] = rows.map((r: any, i: number) => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
@ -439,7 +434,7 @@ const itemBase = getRelativeLocaleUrl(locale, "/items/").replace(/\/$/, "")
|
|||||||
brand: r.brand_name || "Unknown",
|
brand: r.brand_name || "Unknown",
|
||||||
year: r.year || "",
|
year: r.year || "",
|
||||||
created_at: r.published_at,
|
created_at: r.published_at,
|
||||||
cover: toAbs(r.cover || ""),
|
cover: toAbsPublic(r.cover || ""),
|
||||||
excerpt: r.summary || "",
|
excerpt: r.summary || "",
|
||||||
image_count: Number(r.image_count || 0),
|
image_count: Number(r.image_count || 0),
|
||||||
featured: false,
|
featured: false,
|
||||||
|
|||||||
@ -12,6 +12,7 @@ type Brand = SsgBrand
|
|||||||
// · 无副标题、无数字 badge、无大字标题、无悬停黑底、无搜索框
|
// · 无副标题、无数字 badge、无大字标题、无悬停黑底、无搜索框
|
||||||
// 构建期 SSR 拉数据走 SSG 内部接口 /api/ssg/brands(不对外暴露,位于独立内部端口 8091);
|
// 构建期 SSR 拉数据走 SSG 内部接口 /api/ssg/brands(不对外暴露,位于独立内部端口 8091);
|
||||||
// base 地址 + token 拼接已在 @/lib/ssg 内封装。后端不可达则回落空数组(页面显示空索引)。
|
// base 地址 + token 拼接已在 @/lib/ssg 内封装。后端不可达则回落空数组(页面显示空索引)。
|
||||||
|
const { locale } = getI18n(Astro)
|
||||||
const brands = await getSsgBrands({ size: 300, only_with_articles: 1, featured: 1 })
|
const brands = await getSsgBrands({ size: 300, only_with_articles: 1, featured: 1 })
|
||||||
const loadError = brands.length === 0
|
const loadError = brands.length === 0
|
||||||
|
|
||||||
@ -92,7 +93,7 @@ const total = orderedColumns.reduce((s, c) => s + c.items.length, 0)
|
|||||||
<ul class="brow-items">
|
<ul class="brow-items">
|
||||||
{col.items.map((b) => (
|
{col.items.map((b) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/articles?brand_id=${b.id}`}>{primaryName(b)}</a>
|
<a href={href(locale, PATH.articlesByBrand(b.id))}>{primaryName(b)}</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -8,13 +8,6 @@ export const DEFAULT_LOCALE = 'en' as const;
|
|||||||
export const LOCALES = ['en', 'cn'] as const;
|
export const LOCALES = ['en', 'cn'] as const;
|
||||||
export type Locale = (typeof LOCALES)[number];
|
export type Locale = (typeof LOCALES)[number];
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认语言是否带 URL 前缀:
|
|
||||||
* - false → 英文在根路径(/shows),中文在 /cn/shows(本项目采用)
|
|
||||||
* - true → 所有语言都带前缀(/en/shows、/cn/shows)
|
|
||||||
*/
|
|
||||||
export const PREFIX_DEFAULT_LOCALE = false;
|
|
||||||
|
|
||||||
/** 语言显示名(用于语言切换器等 UI)。 */
|
/** 语言显示名(用于语言切换器等 UI)。 */
|
||||||
export const LOCALE_NAMES: Record<Locale, string> = {
|
export const LOCALE_NAMES: Record<Locale, string> = {
|
||||||
en: 'English',
|
en: 'English',
|
||||||
@ -29,8 +22,3 @@ export const BROWSER_LOCALE_MAP: Record<string, Locale> = {
|
|||||||
'zh-hk': 'cn',
|
'zh-hk': 'cn',
|
||||||
'en': 'en',
|
'en': 'en',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 判断一个路径段是不是已支持的语言前缀。 */
|
|
||||||
export function isLocaleSegment(seg: string | undefined): seg is Locale {
|
|
||||||
return !!seg && (LOCALES as readonly string[]).includes(seg);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
import '@/styles/global.css'
|
import '@/styles/global.css'
|
||||||
import { getRelativeLocaleUrl } from 'astro:i18n'
|
import { href, PATH } from '@/lib/routes'
|
||||||
import { getI18n } from '@/i18n/utils'
|
import { getI18n } from '@/i18n/utils'
|
||||||
import { LOCALES, LOCALE_NAMES, isLocaleSegment } from '@/i18n/config'
|
import { LOCALES, LOCALE_NAMES } from '@/i18n/config'
|
||||||
import LanguageDetector from '@/components/LanguageDetector.astro'
|
import LanguageDetector from '@/components/LanguageDetector.astro'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -39,13 +39,13 @@ const queryString = Astro.url.search;
|
|||||||
<!-- 顶部主导航栏 -->
|
<!-- 顶部主导航栏 -->
|
||||||
<nav class="flex justify-between items-center text-[13px] font-bold tracking-[0.5px] uppercase">
|
<nav class="flex justify-between items-center text-[13px] font-bold tracking-[0.5px] uppercase">
|
||||||
<div class="flex flex-wrap items-center gap-6 md:gap-8">
|
<div class="flex flex-wrap items-center gap-6 md:gap-8">
|
||||||
<a href={getRelativeLocaleUrl(locale, '/portfolio')} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent">{t('Portfolio')}<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full font-mono tracking-tight leading-none">
|
<a href={href(locale, PATH.portfolio)} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent">{t('Portfolio')}<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full font-mono tracking-tight leading-none">
|
||||||
2026
|
2026
|
||||||
</span></a>
|
</span></a>
|
||||||
<a href={getRelativeLocaleUrl(locale, '/shows')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('All Shows')}</a>
|
<a href={href(locale, PATH.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('All Shows')}</a>
|
||||||
<a href={getRelativeLocaleUrl(locale, '/latest-projects')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
|
<a href={href(locale, PATH.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
|
||||||
<a href={getRelativeLocaleUrl(locale, '/about')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
|
<a href={href(locale, PATH.about)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
|
||||||
<a href={getRelativeLocaleUrl(locale, '/contact')} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a>
|
<a href={href(locale, PATH.contact)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@ -88,7 +88,7 @@ const queryString = Astro.url.search;
|
|||||||
{LOCALES.map((l) => (
|
{LOCALES.map((l) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={getRelativeLocaleUrl(l, currentPath) + queryString}
|
href={href(l, currentPath) + queryString}
|
||||||
@click="open = false"
|
@click="open = false"
|
||||||
class:list={[
|
class:list={[
|
||||||
'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
|
||||||
@ -100,8 +100,8 @@ const queryString = Astro.url.search;
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2" id="auth-slot">
|
<div class="flex items-center gap-2" id="auth-slot">
|
||||||
<a href={getRelativeLocaleUrl(locale, '/login')} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
<a href={href(locale, PATH.login)} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
||||||
<a href={getRelativeLocaleUrl(locale, '/register')} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
<a href={href(locale, PATH.register)} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -208,13 +208,13 @@ const queryString = Astro.url.search;
|
|||||||
|
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
<a
|
<a
|
||||||
href={getRelativeLocaleUrl(locale, '/about')}
|
href={href(locale, PATH.about)}
|
||||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||||
>
|
>
|
||||||
{t('About')}
|
{t('About')}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={getRelativeLocaleUrl(locale, '/contact')}
|
href={href(locale, PATH.contact)}
|
||||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||||
>
|
>
|
||||||
{t('Contact')}
|
{t('Contact')}
|
||||||
|
|||||||
@ -57,6 +57,42 @@ export function toAbs(url: string): string {
|
|||||||
return /^https?:\/\//i.test(url) ? url : getBase() + url
|
return /^https?:\/\//i.test(url) ? url : getBase() + url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 客户端运行期 base:仅 PUBLIC_API_BASE(空 → 同源相对 /api),
|
||||||
|
// 与 getBase()(含 SSR / DEV 覆盖,用于 RunwayLooks 等)区分,保持 articles 页既有行为。
|
||||||
|
export function publicBase(): string {
|
||||||
|
return ((import.meta.env as any).PUBLIC_API_BASE ?? "").replace(/\/$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相对路径补成绝对(基于 PUBLIC_API_BASE);供 articles.astro 客户端脚本复用,
|
||||||
|
// 避免在每个 .astro 文件的 <script> 里重复写 base URL 解析。
|
||||||
|
export function toAbsPublic(url: string): string {
|
||||||
|
if (!url) return ""
|
||||||
|
if (/^https?:\/\//i.test(url)) return url
|
||||||
|
return publicBase() + url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 走秀档案列表(articles.astro 客户端按需拉取):保留该页独有的
|
||||||
|
// 「单值 brand_id / collection_type / season」参数形态与 summary 字段,
|
||||||
|
// 与 getArticles(RunwayLooks 用,参数形态不同)分开维护。
|
||||||
|
export async function fetchArticlesList(opts?: {
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
collection_type?: string
|
||||||
|
season?: string
|
||||||
|
brand_id?: string
|
||||||
|
}): Promise<any[]> {
|
||||||
|
const base = publicBase()
|
||||||
|
if (!base) return []
|
||||||
|
const p = new URLSearchParams({ page: String(opts?.page ?? 1), size: String(opts?.size ?? 30) })
|
||||||
|
if (opts?.collection_type) p.set("collection_type", opts.collection_type)
|
||||||
|
if (opts?.season) p.set("season", opts.season)
|
||||||
|
if (opts?.brand_id) p.set("brand_id", opts.brand_id)
|
||||||
|
const res = await fetch(`${base}/api/public/articles?${p.toString()}`)
|
||||||
|
if (!res.ok) throw new Error(`list ${res.status}`)
|
||||||
|
const json = await res.json()
|
||||||
|
return json?.data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, params?: URLSearchParams): Promise<T> {
|
async function request<T>(path: string, params?: URLSearchParams): Promise<T> {
|
||||||
const qs = params && params.toString() ? "?" + params.toString() : ""
|
const qs = params && params.toString() ? "?" + params.toString() : ""
|
||||||
const url = getBase() + "/api" + path + qs
|
const url = getBase() + "/api" + path + qs
|
||||||
|
|||||||
@ -120,7 +120,6 @@ export async function fetchArticleById(id: number | string, apiBase: string = AP
|
|||||||
if (!base) return null
|
if (!base) return null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}/api/public/articles/${id}`)
|
const res = await fetch(`${base}/api/public/articles/${id}`)
|
||||||
console.log(`${base}/api/public/articles/${id}`)
|
|
||||||
if (!res.ok) return null
|
if (!res.ok) return null
|
||||||
const json: any = await res.json()
|
const json: any = await res.json()
|
||||||
return json?.data ?? null
|
return json?.data ?? null
|
||||||
|
|||||||
39
src/lib/routes.ts
Normal file
39
src/lib/routes.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 全站 URL 路径集中管理
|
||||||
|
// - PATH:逻辑路径(不带 locale 前缀)。静态用字符串,动态用工厂函数。
|
||||||
|
// - href:服务端(.astro frontmatter)使用,跟随 astro.config 的 prefixDefaultLocale 规则拼前缀。
|
||||||
|
// - clientHref:客户端脚本(Alpine/Vue)使用,astro:i18n 在浏览器不可用,故自行拼接。
|
||||||
|
|
||||||
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
||||||
|
|
||||||
|
/** 逻辑路径片段(不含 locale 前缀)。 */
|
||||||
|
export const PATH = {
|
||||||
|
home: '/',
|
||||||
|
shows: '/shows',
|
||||||
|
brands: '/brands',
|
||||||
|
portfolio: '/portfolio',
|
||||||
|
latestProjects: '/latest-projects',
|
||||||
|
about: '/about',
|
||||||
|
contact: '/contact',
|
||||||
|
login: '/login',
|
||||||
|
register: '/register',
|
||||||
|
// 动态路径(工厂函数)
|
||||||
|
item: (id: string | number) => `/items/${id}`,
|
||||||
|
article: (id: string | number) => `/article?id=${id}`,
|
||||||
|
articlesByBrand: (id: string | number) => `/articles?brand_id=${id}`,
|
||||||
|
runwayLooks: (id: string | number) => `/runway-looks/${id}`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */
|
||||||
|
export function href(locale: string, path: string): string {
|
||||||
|
return getRelativeLocaleUrl(locale, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端:生成带 locale 前缀的完整 URL。
|
||||||
|
* 跟随 astro.config.mjs 的 prefixDefaultLocale: true(en 也带 /en 前缀)。
|
||||||
|
* 浏览器内无法使用 astro:i18n,故此处自行拼接,规则须与 astro.config 保持一致。
|
||||||
|
*/
|
||||||
|
export function clientHref(locale: string, path: string): string {
|
||||||
|
const base = locale === 'en' ? '/en' : `/${locale}`;
|
||||||
|
return path === '/' ? base : `${base}${path}`;
|
||||||
|
}
|
||||||
@ -72,7 +72,6 @@ export interface SsgBrand {
|
|||||||
// 首页「热门品牌」区块:按图片数排名前 limit 个品牌,每个附带代表走秀的封面与标题。
|
// 首页「热门品牌」区块:按图片数排名前 limit 个品牌,每个附带代表走秀的封面与标题。
|
||||||
export async function getSsgPopularBrands(limit = 20): Promise<SsgPopularBrand[]> {
|
export async function getSsgPopularBrands(limit = 20): Promise<SsgPopularBrand[]> {
|
||||||
const j = await ssgRequest<{ data?: SsgPopularBrand[] }>(`/api/ssg/brands/popular?limit=${limit}`)
|
const j = await ssgRequest<{ data?: SsgPopularBrand[] }>(`/api/ssg/brands/popular?limit=${limit}`)
|
||||||
console.log(j)
|
|
||||||
return j?.data ?? []
|
return j?.data ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
src/lib/ssr.ts
Normal file
34
src/lib/ssr.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// src/lib/ssr.ts — 服务端(按需 SSR / on-demand render)数据客户端
|
||||||
|
//
|
||||||
|
// 集中封装「服务端取数 + base URL 解析」,让 .astro 页面只调用业务函数,
|
||||||
|
// 不再自己拼 API_SSR / PUBLIC_API_BASE / http://localhost:8090 这类 base URL。
|
||||||
|
//
|
||||||
|
// 与 ssg.ts 的区别:
|
||||||
|
// - ssg.ts → 构建期 Node 调 /api/ssg/*(内部端口 8091,不对外暴露)
|
||||||
|
// - ssr.ts → 运行期(按需渲染)调 /api/public/*,base = API_SSR(回落 PUBLIC_API_BASE)
|
||||||
|
|
||||||
|
import { fetchArticleById, type RawArticle } from "./data"
|
||||||
|
|
||||||
|
// SSR base 解析:API_SSR(部署注入)> PUBLIC_API_BASE > 本地后端回落
|
||||||
|
export function ssrBase(): string {
|
||||||
|
const e = import.meta.env as any
|
||||||
|
return (e.API_SSR || e.PUBLIC_API_BASE || "http://localhost:8090").replace(/\/$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SsrArticleResult = {
|
||||||
|
article: RawArticle | null
|
||||||
|
error: "missing" | "failed" | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 id 取文章:包揽「缺 id / 取数失败 / 成功」三态,astro 页面只调这一行
|
||||||
|
export async function getSsrArticle(
|
||||||
|
id: string | null | undefined
|
||||||
|
): Promise<SsrArticleResult> {
|
||||||
|
if (!id) return { article: null, error: "missing" }
|
||||||
|
try {
|
||||||
|
const article = await fetchArticleById(id, ssrBase())
|
||||||
|
return { article, error: article ? null : "failed" }
|
||||||
|
} catch {
|
||||||
|
return { article: null, error: "failed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,27 +1,15 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import Article from "@/components/pages/article.astro"
|
import Article from "@/components/pages/article.astro"
|
||||||
import { fetchArticleById } from "@/lib/data"
|
import { getSsrArticle } from "@/lib/ssr"
|
||||||
|
|
||||||
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /cn/article?id= 链接,无需改动其他页面)
|
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /cn/article?id= 链接,无需改动其他页面)
|
||||||
export const prerender = false
|
export const prerender = false
|
||||||
|
|
||||||
const API = (import.meta.env.API_SSR || import.meta.env.PUBLIC_API_BASE || "http://localhost:8090").replace(/\/$/, "")
|
|
||||||
const id = Astro.url.searchParams.get("id")
|
const id = Astro.url.searchParams.get("id")
|
||||||
let article: import("@/lib/data").RawArticle | null = null
|
const { article, error } = await getSsrArticle(id)
|
||||||
let error: "missing" | "failed" | null = null
|
|
||||||
if (!id) {
|
|
||||||
error = "missing"
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
article = await fetchArticleById(id, API)
|
|
||||||
} catch {
|
|
||||||
article = null
|
|
||||||
}
|
|
||||||
error = article ? null : "failed"
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Article article={article} locale="cn" apiBase={API} error={error} />
|
<Article article={article} locale="cn" error={error} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
||||||
|
import { getI18n } from '@/i18n/utils'
|
||||||
|
import { clientHref, PATH, href } from '@/lib/routes'
|
||||||
|
|
||||||
|
const { locale } = getI18n(Astro)
|
||||||
let sidebarBrands: BrandEntry[] = []
|
let sidebarBrands: BrandEntry[] = []
|
||||||
let yearChips: number[] = []
|
let yearChips: number[] = []
|
||||||
|
|
||||||
@ -21,14 +24,14 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="All Shows — Portfolio Studio">
|
<Layout title="All Shows — Portfolio Studio">
|
||||||
<div class="showsroot w-full" x-data="showsPage()">
|
<div class="showsroot w-full" x-data="showsPage()" data-locale={locale}>
|
||||||
<!-- 顶部:极简眉头 -->
|
<!-- 顶部:极简眉头 -->
|
||||||
<header class="showshead">
|
<header class="showshead">
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Home', href: '/' },
|
{ label: 'Home', href: href(locale, PATH.home) },
|
||||||
{ label: 'Runway Archive', href: '/shows' },
|
{ label: 'Runway Archive', href: href(locale, PATH.shows) },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
||||||
@ -187,7 +190,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
<!-- Card List -->
|
<!-- Card List -->
|
||||||
<template x-for="it in items" :key="it.id">
|
<template x-for="it in items" :key="it.id">
|
||||||
<a class="showscard" :href="`/article?id=${it.id}`" :aria-label="`${it.brand_name} ${it.title}`">
|
<a class="showscard" :href="articleHref(it)" :aria-label="`${it.brand_name} ${it.title}`">
|
||||||
<div class="showscard-left">
|
<div class="showscard-left">
|
||||||
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||||
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||||
@ -557,6 +560,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
allBrands,
|
allBrands,
|
||||||
|
locale: '',
|
||||||
ICON_USER,
|
ICON_USER,
|
||||||
ICON_DATE,
|
ICON_DATE,
|
||||||
SPIN_SVG,
|
SPIN_SVG,
|
||||||
@ -594,6 +598,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
reqToken: 0,
|
reqToken: 0,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.locale = this.$el.dataset.locale || 'en'
|
||||||
this.loadPage(1, false)
|
this.loadPage(1, false)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -826,6 +831,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
// 辅助辅助工具函数
|
// 辅助辅助工具函数
|
||||||
toAbsUrl(url: string) { return toAbs(url) },
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
|
articleHref(it: { id: string | number }) { return clientHref(this.locale, PATH.article(it.id)) },
|
||||||
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
||||||
fmtDate(ts: number) {
|
fmtDate(ts: number) {
|
||||||
if (!ts) return ""
|
if (!ts) return ""
|
||||||
|
|||||||
@ -1,27 +1,15 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import Article from "@/components/pages/article.astro"
|
import Article from "@/components/pages/article.astro"
|
||||||
import { fetchArticleById } from "@/lib/data"
|
import { getSsrArticle } from "@/lib/ssr"
|
||||||
|
|
||||||
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/article?id= 链接,无需改动其他页面)
|
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/article?id= 链接,无需改动其他页面)
|
||||||
export const prerender = false
|
export const prerender = false
|
||||||
|
|
||||||
const API = (import.meta.env.API_SSR || import.meta.env.PUBLIC_API_BASE || "http://localhost:8090").replace(/\/$/, "")
|
|
||||||
const id = Astro.url.searchParams.get("id")
|
const id = Astro.url.searchParams.get("id")
|
||||||
let article: import("@/lib/data").RawArticle | null = null
|
const { article, error } = await getSsrArticle(id)
|
||||||
let error: "missing" | "failed" | null = null
|
|
||||||
if (!id) {
|
|
||||||
error = "missing"
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
article = await fetchArticleById(id, API)
|
|
||||||
} catch {
|
|
||||||
article = null
|
|
||||||
}
|
|
||||||
error = article ? null : "failed"
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Article article={article} locale="en" apiBase={API} error={error} />
|
<Article article={article} locale="en" error={error} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@ -1,29 +1,15 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import RunwayLookDetail from "@/components/RunwayLooksDetail.astro"
|
import RunwayLookDetail from "@/components/RunwayLooksDetail.astro"
|
||||||
import { fetchArticleById } from "@/lib/data"
|
import { getSsrArticle } from "@/lib/ssr"
|
||||||
|
|
||||||
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/article?id= 链接,无需改动其他页面)
|
// 按需 SSR:服务端按 ?id= 取数并渲染(保留既有 /en/items/:id 链接,无需改动其他页面)
|
||||||
export const prerender = false
|
export const prerender = false
|
||||||
|
|
||||||
const API = ("http://localhost:8090").replace(/\/$/, "")
|
const { id } = Astro.params
|
||||||
const {id} = Astro.params
|
const { article, error } = await getSsrArticle(id)
|
||||||
let article: import("@/lib/data").RawArticle | null = null
|
|
||||||
let error: "missing" | "failed" | null = null
|
|
||||||
console.log(id)
|
|
||||||
if (!id) {
|
|
||||||
error = "missing"
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
article = await fetchArticleById(id, API)
|
|
||||||
console.log(article)
|
|
||||||
} catch {
|
|
||||||
article = null
|
|
||||||
}
|
|
||||||
error = article ? null : "failed"
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<RunwayLookDetail article={article} apiBase={API} error={error} />
|
<RunwayLookDetail article={article} error={error} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
||||||
|
import { getI18n } from '@/i18n/utils'
|
||||||
|
import { clientHref, PATH, href } from '@/lib/routes'
|
||||||
|
|
||||||
|
const { locale } = getI18n(Astro)
|
||||||
let sidebarBrands: BrandEntry[] = []
|
let sidebarBrands: BrandEntry[] = []
|
||||||
let yearChips: number[] = []
|
let yearChips: number[] = []
|
||||||
|
|
||||||
@ -30,14 +33,14 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="showsroot w-full" x-data="showsPage()">
|
<div class="showsroot w-full" x-data="showsPage()" data-locale={locale}>
|
||||||
<!-- 顶部:极简眉头 -->
|
<!-- 顶部:极简眉头 -->
|
||||||
<header class="showshead">
|
<header class="showshead">
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Home', href: '/' },
|
{ label: 'Home', href: href(locale, PATH.home) },
|
||||||
{ label: 'Runway Archive', href: '/shows' },
|
{ label: 'Runway Archive', href: href(locale, PATH.shows) },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
||||||
@ -201,7 +204,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
<!-- Card List -->
|
<!-- Card List -->
|
||||||
<template x-for="it in items" :key="it.id">
|
<template x-for="it in items" :key="it.id">
|
||||||
<a class="showscard" :href="`/article?id=${it.id}`" :aria-label="`${it.brand_name} ${it.title}`">
|
<a class="showscard" :href="articleHref(it)" :aria-label="`${it.brand_name} ${it.title}`">
|
||||||
<div class="showscard-left">
|
<div class="showscard-left">
|
||||||
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||||
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||||
@ -486,6 +489,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
allBrands,
|
allBrands,
|
||||||
|
locale: '',
|
||||||
ICON_USER,
|
ICON_USER,
|
||||||
ICON_DATE,
|
ICON_DATE,
|
||||||
SPIN_SVG,
|
SPIN_SVG,
|
||||||
@ -513,6 +517,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
reqToken: 0,
|
reqToken: 0,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.locale = this.$el.dataset.locale || 'en'
|
||||||
this.loadPage(1, false)
|
this.loadPage(1, false)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -684,6 +689,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
// 辅助辅助工具函数
|
// 辅助辅助工具函数
|
||||||
toAbsUrl(url: string) { return toAbs(url) },
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
|
articleHref(it: { id: string | number }) { return clientHref(this.locale, PATH.article(it.id)) },
|
||||||
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
||||||
fmtDate(ts: number) {
|
fmtDate(ts: number) {
|
||||||
if (!ts) return ""
|
if (!ts) return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user