From f8ff5b86c7bd21f4ce043204717036d0ea71a521 Mon Sep 17 00:00:00 2001 From: toom1996 <23cm.cn@gmail.com> Date: Fri, 25 Sep 2026 11:32:11 +0800 Subject: [PATCH] update --- .gitignore | 2 + src/components/DetailCountBadge.astro | 12 ++--- src/components/DetailGalleryView.astro | 15 +++--- src/components/GalleryLightbox.astro | 75 +++----------------------- src/components/RunwayLookCard.astro | 7 ++- src/components/StreetSnapCard.astro | 7 ++- src/layouts/Layout.astro | 2 +- src/lib/api.ts | 7 ++- src/lib/endpoints.ts | 2 +- src/lib/gallery.ts | 48 +---------------- src/lib/galleryDetail.ts | 60 ++++++++++----------- src/lib/i18n/index.ts | 15 +++--- src/lib/request.ts | 16 +++--- src/lib/ssg.ts | 3 +- src/lib/ssr.ts | 12 ++--- 15 files changed, 86 insertions(+), 197 deletions(-) diff --git a/.gitignore b/.gitignore index 6fc517e..f8f7e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ _tmp* # jetbrains setting folder .idea/ +.playwright-cli +.workbuddy \ No newline at end of file diff --git a/src/components/DetailCountBadge.astro b/src/components/DetailCountBadge.astro index 8208560..91618e3 100644 --- a/src/components/DetailCountBadge.astro +++ b/src/components/DetailCountBadge.astro @@ -3,20 +3,20 @@ // 纯主图(无细节图)不渲染。数量走 Alpine 的 detailCount()(响应式)—— 未登录取前 PREVIEW_LIMIT 张 // 的细节子树,登录后由 gallery:full 重建映射,两态共用同一条渲染路径。 // -// look:Alpine 表达式,表示该主图的 look_index。SSR 首屏传字面量(如 "3"), -// 客户端补拉的 x-for 里传表达式(如 "im.lookIndex")。 +// mainId:Alpine 表达式,表示该主图自身 id。SSR 首屏传字面量(如 "i0008CZYI"), +// 客户端补拉的 x-for 里传表达式(如 "im.id")。 interface Props { - look: string + mainId: string } -const { look } = Astro.props -const count = `detailCount(${look})` +const { mainId } = Astro.props +const count = `detailCount(${mainId})` --- 0`} x-cloak - @click.stop={`openDetailLb(${look})`} + @click.stop={`openDetailLb(${mainId})`} :title={`${count} + ' detail' + (${count} > 1 ? 's' : '')`} :aria-label={`${count} + ' detail' + (${count} > 1 ? 's' : '')`} class="absolute left-2 bottom-2 z-10 flex items-center gap-1 px-1.5 py-1 bg-white/85 backdrop-blur-sm border border-black/15 text-[#111] text-[10px] uppercase tracking-[0.14em] cursor-pointer hover:bg-black hover:text-white transition-colors" diff --git a/src/components/DetailGalleryView.astro b/src/components/DetailGalleryView.astro index a90227f..d41a9ca 100644 --- a/src/components/DetailGalleryView.astro +++ b/src/components/DetailGalleryView.astro @@ -32,7 +32,6 @@ interface GalleryImage { thumb: string raw: string name: string - lookIndex: number } interface Props { @@ -68,7 +67,7 @@ interface Props { preview: boolean /** 占位遮罩格数(补足「剩余 N 张」) */ lockedCount: number - /** 递给客户端的副图投影(look_index + detail),与登录后完整 images 同构解析 */ + /** 递给客户端的副图投影(id + detail),与登录后完整 images 同构解析 */ detailPayload: unknown /** 图集元信息:写进 data-*,供收藏 / 灯箱使用 */ galleryUid: string @@ -186,7 +185,7 @@ const hasImages = previewImages.length > 0 {previewImages.map((im, i) => ( 0 {/* 副图数量角标(共享组件):有副图的主图左下角标「张数」,点它直接开副图灯箱 */} - + {showCaptions && im.name && ( @@ -264,7 +263,7 @@ const hasImages = previewImages.length > 0 0 - + {showCaptions && ( @@ -325,9 +324,9 @@ const hasImages = previewImages.length > 0 ) } - {/* 大图灯箱(共享):当前主图的副图经 details 插槽注入。 */} + {/* 大图灯箱(共享):当前主图的副图经 details 插槽注入。左栏缩略图条已移除(见 GalleryLightbox)。 */} {hasImages && ( - + )} diff --git a/src/components/GalleryLightbox.astro b/src/components/GalleryLightbox.astro index 9438ab2..a8439d5 100644 --- a/src/components/GalleryLightbox.astro +++ b/src/components/GalleryLightbox.astro @@ -1,5 +1,9 @@ --- -// 走秀 / 街拍详情页共享的全屏大图灯箱:左栏缩略图条 + 大图 + 上/下·左/右翻页 + 单图收藏。 +// 走秀 / 街拍详情页共享的全屏大图灯箱:大图 + 左/右翻页 + 单图收藏。 +// +// 左栏「所有图片缩略图条」已移除:翻页/定位已由顶部计数 + 左右箭头 + 键盘 ←/→ + ?img= 深链完整覆盖, +// 缩略条属冗余导航;去掉后主图可用面积更大。主图翻页实际读的是图集网格里 [data-image-index] 的 +// figure(第 6 张起的 extraImages 也已在网格里渲染),与左栏无关,故移除不影响翻页。 // // 视觉遵循全站「黑白编辑风」:直角细框、小字 + 宽字距、零填充计数、hover 反色(bg-black + 白字)。 // 交互逻辑全部在 src/lib/gallery.ts(createGalleryView);本组件只负责标记。 @@ -7,17 +11,7 @@ // 各页独有的附加层通过具名插槽 details 注入(走秀页塞「当前 look 的细节图簇」,街拍页不传)。 import { getI18n } from "@/lib/i18n" -interface Props { - /** SSR 首屏图片(前 PREVIEW_LIMIT 张),需带 thumb 供左栏缩略图条使用 */ - images: { thumb: string }[] - /** 缩略图比例。后端素材统一 2:3(说明见 DetailGalleryView 的 aspect prop),默认值即可 */ - aspect?: "2/3" | "3/4" -} -const { images, aspect = "2/3" } = Astro.props const { t } = getI18n(Astro) - -// 两个字面量都出现在本文件源码里,保证 Tailwind v4 JIT 能扫描到 -const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]" --- - - {images.map((im, i) => ( - - - {String(i + 1).padStart(2, "0")} - - - ))} - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/RunwayLookCard.astro b/src/components/RunwayLookCard.astro index 85e3ad7..b36e007 100644 --- a/src/components/RunwayLookCard.astro +++ b/src/components/RunwayLookCard.astro @@ -21,7 +21,7 @@ interface Props { const { article, error = null, preview = false, imageTotal = 0 } = Astro.props const { locale, t } = getI18n(Astro) -// 后端详情接口 images 仅返回主图(look 图),每张主图的细节图嵌套在它自己的 detail 子数组下。 +// 后端详情接口 images 仅返回主图(走秀主图),每张主图的细节图嵌套在它自己的 detail 子数组下。 // 故此处 images 即为主图集合,默认只渲染它们;detail 随主图一起带上,供「细节入口」展开。 const images = (article?.images ?? []) .map((im) => ({ @@ -32,16 +32,15 @@ const images = (article?.images ?? []) thumb: ssrAbs(im.thumb || im.image || ""), raw: im.image || "", // 相对路径:收藏快照存相对地址,不把宿主/端口写死进库 name: im.name || "", - lookIndex: Number(im.look_index ?? 0), })) .filter((im) => im.url) // 递给浏览器端 Alpine 的细节图数据:只序列化「带细节图的主图」投影(不带主图自身 URL, -// 避免把整份 images 重复塞进 HTML)。元素形状与 images 元素一致(look_index + detail), +// 避免把整份 images 重复塞进 HTML)。元素形状与 images 元素一致(id + detail), // 故 Alpine 的 buildDetailMapFrom() 能与「登录后拿回的完整 images」共用同一套解析逻辑。 const detailPayload = (article?.images ?? []) .filter((im) => (im.detail?.length ?? 0) > 0) - .map((im) => ({ look_index: Number(im.look_index ?? 0), detail: im.detail })) + .map((im) => ({ id: im.id ?? "", detail: im.detail })) // 未登录预览上限:详情页(图集)未登录最多展示前 PREVIEW_LIMIT 张图片;其余由登录门禁提示, // 已登录(客户端 loadAuthedState 拉全量)或图片本就不超上限时全部展示。前 PREVIEW_LIMIT 张服务端已渲染(SEO/无 JS 友好)。 diff --git a/src/components/StreetSnapCard.astro b/src/components/StreetSnapCard.astro index e6a23b8..8f7ff16 100644 --- a/src/components/StreetSnapCard.astro +++ b/src/components/StreetSnapCard.astro @@ -30,15 +30,14 @@ const images = (snap?.images ?? []) thumb: ssrAbs(im.thumb || im.image || ""), raw: im.image || "", // 相对路径:收藏快照存相对地址,不把宿主/端口写死进库 name: im.name || "", - lookIndex: Number(im.look_index ?? 0), })) .filter((im) => im.url) -// 递给浏览器端 Alpine 的副图数据:只序列化「带副图的主图」投影(look_index + detail), -// 供 lib/galleryDetail.ts 构建 look_index → 副图 映射(与登录后完整 images 同构解析)。 +// 递给浏览器端 Alpine 的副图数据:只序列化「带副图的主图」投影(id + detail), +// 供 lib/galleryDetail.ts 构建 主图id → 副图 映射(与登录后完整 images 同构解析)。 const detailPayload = (snap?.images ?? []) .filter((im) => (im.detail?.length ?? 0) > 0) - .map((im) => ({ look_index: Number(im.look_index ?? 0), detail: im.detail })) + .map((im) => ({ id: im.id ?? "", detail: im.detail })) // 未登录预览上限:详情页(图集)未登录最多展示前 PREVIEW_LIMIT 张图片;其余由登录门禁提示, // 已登录(客户端 loadAuthedState 拉全量)或图片本就不超上限时全部展示。前 PREVIEW_LIMIT 张服务端已渲染(SEO/无 JS 友好)。 diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index aafe2ff..dced358 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -36,7 +36,7 @@ const defaultKeywords = const metaDescription = description ?? defaultDescription const metaKeywords = keywords ?? defaultKeywords -// 把当前语言注入 API 请求层:所有服务端(SSG/SSR)取数统一带上 ?locale=, +// 把当前语言注入 API 请求层:所有服务端(SSG/SSR)取数统一走 X-Locale 头, // 后端据此选 *_en / *_cn 列。客户端 fetch 则自行按 URL path 推断,无需此处。 setApiLocale(locale) diff --git a/src/lib/api.ts b/src/lib/api.ts index 2320149..1915cb3 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -69,10 +69,9 @@ export interface RawArticleImage { name?: string // 主图 / 细节图不再靠 is_detail 区分:详情接口 images 只含主图,细节图嵌套在所属主图的 detail 下, // 层级本身已表达类型(前端无任何处读取 is_detail,故不再声明)。 - look_index?: number // 该图归属的主图序号;细节图与所属主图同值 favorited?: boolean // 已登录时该图片是否已收藏(随详情接口返回) - // 仅详情接口填充:该主图(look)下的细节图子集,元素形状与本类型递归同构。 - // 前端「细节入口」直接读它,无需再按 look_index 分组查表。 + // 仅详情接口填充:该主图下的细节图子集,元素形状与本类型递归同构。 + // 前端「细节入口」直接读它,无需再按主图 id 分组查表(主图主键即 im.id)。 detail?: RawArticleImage[] } @@ -122,7 +121,7 @@ export interface RawStreetSnap { title: string cover: string // 详情接口 images 仅含主图(is_detail=0),每张主图的副图(细节图)嵌套在它自己的 detail 子数组下。 - // 复用 RawArticleImage(与走秀详情同形):look_index 标识主图序号,detail 为副图。 + // 复用 RawArticleImage(与走秀详情同形):detail 为副图,主图以自身 id 为归并键(不再有 look_index)。 images?: RawArticleImage[] favorited?: boolean // 已登录时该图集是否已收藏(随详情接口返回) } diff --git a/src/lib/endpoints.ts b/src/lib/endpoints.ts index 6af58d5..a67f6f1 100644 --- a/src/lib/endpoints.ts +++ b/src/lib/endpoints.ts @@ -2,7 +2,7 @@ // // 与 src/lib/routes.ts(ROUTES,前端页面跳转 URL)严格区分: // - routes.ts 的 ROUTES = 浏览器访问的「页面路由」(带 /en /cn 语言前缀,走 /navigate) -// - 本文件的 endpoints = fetch 调用的「后端接口路径」(locale 走 ?locale= 查询参数,不在路径里) +// - 本文件的 endpoints = fetch 调用的「后端接口路径」(语言经 X-Locale 头透传,不在路径/查询里) // 版本前缀 V1 与构建期内部端口前缀 SSG 只在此定义一次; // 将来升 /api/v2 或调整 SSG 路径,改这一处即可,不用全局搜。 diff --git a/src/lib/gallery.ts b/src/lib/gallery.ts index 2d90efa..a274da1 100644 --- a/src/lib/gallery.ts +++ b/src/lib/gallery.ts @@ -21,9 +21,8 @@ export interface GalleryImageRaw { image?: string thumb?: string name?: string - look_index?: number favorited?: boolean - // 详情接口专有:该主图下的细节图(递归同构)。走秀页据此重建「细节入口」映射。 + // 详情接口专有:该主图下的细节图(递归同构)。走秀/街拍页据此重建「细节入口」映射。 detail?: GalleryImageRaw[] } @@ -50,13 +49,8 @@ export interface GalleryExtraImage { thumb: string raw: string name: string - lookIndex?: number } -// 缩略图懒加载 observer:同一页面同时只有一个图集灯箱,用模块级单例持有; -// 重建前先断开上一个(loadAuthedState 之后会再调一次 initThumbLazyLoading)。 -let thumbObserver: IntersectionObserver | null = null - // 等 Alpine 完成一次 DOM 刷新。比 $nextTick 稳:rAF 必然晚于 Alpine 的微任务 flush。 const afterRender = () => new Promise((resolve) => requestAnimationFrame(() => resolve())) @@ -103,7 +97,6 @@ export function createGalleryView(cfg: GalleryConfig) { this.isAuthed = !!getUser() this.imageCount = this.countImages() - this.initThumbLazyLoading() this.initImageFavs() this.maybeLocateImageFromUrl() @@ -163,12 +156,10 @@ export function createGalleryView(cfg: GalleryConfig) { thumb: toAbs(im.thumb || im.image || ""), raw: im.image || "", name: im.name || "", - lookIndex: Number(im.look_index ?? 0), })) await afterRender() this.imageCount = this.countImages() all.slice(5).forEach(mark) - this.initThumbLazyLoading() this.maybeLocateImageFromUrl() } all.slice(0, 5).forEach(mark) @@ -269,7 +260,6 @@ export function createGalleryView(cfg: GalleryConfig) { document.body.style.overflow = "hidden" this.updateLightboxImage() requestAnimationFrame(() => { - this.scrollThumbIntoView() this.preloadAround() }) }, @@ -286,7 +276,6 @@ export function createGalleryView(cfg: GalleryConfig) { const main = document.getElementById("lb-main") if (main) main.scrollTop = 0 requestAnimationFrame(() => { - this.scrollThumbIntoView() this.preloadAround() }) }, @@ -314,14 +303,6 @@ export function createGalleryView(cfg: GalleryConfig) { const main = document.getElementById("lb-main") if (main) main.scrollTop = 0 }, - scrollThumbIntoView() { - const thumbs = document.getElementById("lb-thumbs") as HTMLElement | null - if (!thumbs) return - const active = thumbs.querySelector(`[data-lb-index="${this.lbIndex}"]`) - if (!active) return - const target = active.offsetTop - (thumbs.clientHeight - active.clientHeight) / 2 - thumbs.scrollTo({ top: Math.max(0, target), behavior: "smooth" }) - }, preloadAround() { const indexes = [ this.lbIndex, @@ -336,32 +317,5 @@ export function createGalleryView(cfg: GalleryConfig) { image.src = url }) }, - /** 缩略图条带:用 IntersectionObserver 懒加载背景图(未进入视口不设 background-image)。 */ - initThumbLazyLoading() { - const thumbs = document.querySelectorAll("#lb-thumbs [data-thumb-url]") - if (!thumbs.length) return - if (!("IntersectionObserver" in window)) { - thumbs.forEach((thumb) => { - thumb.style.backgroundImage = `url("${thumb.dataset.thumbUrl}")` - }) - return - } - // 先断开上一个实例,避免重复调用时泄漏 - thumbObserver?.disconnect() - const observer = new IntersectionObserver( - (entries, obs) => { - entries.forEach((entry) => { - if (!entry.isIntersecting) return - const element = entry.target as HTMLElement - const url = element.dataset.thumbUrl - if (url) element.style.backgroundImage = `url("${url}")` - obs.unobserve(element) - }) - }, - { root: document.getElementById("lb-thumbs"), rootMargin: "300px" }, - ) - thumbObserver = observer - thumbs.forEach((thumb) => observer.observe(thumb)) - }, } } diff --git a/src/lib/galleryDetail.ts b/src/lib/galleryDetail.ts index 864a527..5069498 100644 --- a/src/lib/galleryDetail.ts +++ b/src/lib/galleryDetail.ts @@ -1,7 +1,7 @@ // src/lib/galleryDetail.ts —— 详情页的「细节图 / 副图」扩展(走秀与街拍共用)。 // // 叠加在 lib/gallery.ts 的 createGalleryView 之上,提供: -// - 由 SSR 载荷(根节点 data-details)或登录后完整 images 构建 look_index → 细节图 映射 +// - 由 SSR 载荷(根节点 data-details)或登录后完整 images 构建 主图id → 细节图 映射 // - 网格角标用的 detailCount() // - 细节图灯箱(打开 / 翻页 / 缩放 / 收藏 / 退回主图) // @@ -35,8 +35,8 @@ export interface DetailGalleryExtras { detailLbIndex: number detailLbZoomed: boolean detailImages: DetailImage[] - /** look_index → 该主图的细节图数组(init 由 data-details 构建,登录后由 gallery:full 重建)。 */ - detailImagesByLook: Record + /** 主图 id → 该主图的细节图数组(init 由 data-details 构建,登录后由 gallery:full 重建)。 */ + detailImagesByMain: Record /** 细节灯箱是否由主图大图灯箱打开(true 时关闭要退回主图,而不是掉回网格)。 */ detailLbFromLb: boolean lbReturnIndex: number @@ -50,12 +50,12 @@ export interface DetailGalleryExtras { currentDetailFavUrl(): string init(): void - getImageLookIndex(index: number): number + getImageMainId(index: number): string openDetailLbAt(idx: number): void buildDetailMapFrom(images: any[]): void buildDetailMap(): void - detailCount(li: number): number - openDetailLb(li: number, startIndex?: number, fromLb?: boolean): void + detailCount(mainId: string): number + openDetailLb(mainId: string, startIndex?: number, fromLb?: boolean): void closeDetailLb(): void showDetailLb(index: number): void nextDetailLb(): void @@ -84,12 +84,12 @@ export function createDetailGallery, + detailImagesByMain: {} as Record, detailLbFromLb: false, lbReturnIndex: 0, init() { - // 本扩展独有:从 SSR 载荷构建「细节入口」用的 look_index → 细节图 映射。 + // 本扩展独有:从 SSR 载荷构建「细节入口」用的 主图id → 细节图 映射。 this.buildDetailMap() // 未登录时后端只返回前 PREVIEW_LIMIT 张主图的细节子树;登录后 loadAuthedState 拿回完整图集, // 需据其重建映射,否则第 6 张起的角标 / 细节灯箱仍为空(详见 lib/gallery.ts 的 gallery:full)。 @@ -102,10 +102,10 @@ export function createDetailGallery = {} + const map: Record = {} for (const im of images || []) { - const li = Number(im?.look_index ?? 0) + const mainId = String(im?.id ?? "") const kids: any[] = Array.isArray(im?.detail) ? im.detail : [] - if (!li || !kids.length) continue - map[li] = kids.map((d: any) => ({ + if (!mainId || !kids.length) continue + map[mainId] = kids.map((d: any) => ({ id: d.id || "", url: toAbs(d.image || ""), thumb: toAbs(d.thumb || d.image || ""), @@ -145,7 +145,7 @@ export function createDetailGallery 里的 fetch):从 URL path 推断(/cn/ → cn,其余 → en)。 @@ -91,12 +93,11 @@ export function currentLocale(): string { return _ssrLocale || DEFAULT_LOCALE; } -// 把当前语言追加进 query(locale 非空才加,避免污染 URL;DEFAULT_LOCALE 也显式带,语义清晰)。 -export function withLocale(params?: URLSearchParams, override?: string): URLSearchParams { - const p = params ? new URLSearchParams(params) : new URLSearchParams(); - const loc = override || currentLocale(); - if (loc) p.set('locale', loc); - return p; +// 当前语言对应的请求头:{ "X-Locale": "cn" | "en" };取不到则返回空对象(后端回退默认 en)。 +// 所有传输层(浏览器 request / SSG requestSsg / SSR 取数)统一经此透传,避免各处手写 locale。 +export function localeHeader(): Record { + const loc = currentLocale(); + return loc ? { "X-Locale": loc } : {}; } // ── 记住语言偏好 / Remembered locale ── diff --git a/src/lib/request.ts b/src/lib/request.ts index 3bc19f0..9dcd2da 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -6,7 +6,7 @@ // 设计:**全站只有一个公开请求入口 `request()`**,统一附带: // - 前端反爬签名(X-Sign / X-Sign-Ts / X-Sign-Nonce)—— 所有请求一律带; // 后端只在「/public 列表」路由校验,其余路由忽略该头即可。 -// - ?locale=(当前语言)。 +// - X-Locale 头(当前语言)。 // - 已登录时的 Bearer(opts.auth !== false 时经 fetchWithToken 附),遇 401 自动续期一次后重试。 // base 地址全部来自 src/lib/config(dev/prod 两份,缺则抛);密钥不进 config。 // @@ -22,7 +22,7 @@ import { config } from "./config" // dev/prod 配置(baseApi / baseApiSsr / ba import { endpoints } from "./endpoints" // 后端端点路径(见 endpoints.ts) import { clientSign } from "./crypto" // 公开请求签名(X-Sign* 头) import { getAccessToken, getRefreshToken, getUser, saveSession, clearSession, isAccessExpiring } from "./auth" // 令牌读取 / 写入 -import { withLocale, currentLocale } from "./i18n" // 请求语言透传 +import { localeHeader } from "./i18n" // 请求语言透传(X-Locale 头) // ===================== base 解析(全部来自 config) ===================== @@ -86,7 +86,7 @@ async function ensureFreshToken(): Promise { // ===================== 唯一请求入口 ===================== // 全站统一请求:走 baseUrl() + path(path 为含 /api/v1 前缀的完整路径)。 -// 一律附带「反爬签名 + ?locale=(当前语言)」。 +// 一律附带「反爬签名 + X-Locale 头(当前语言)」。 // opts.auth 默认 true:附 Bearer + 遇 401 续期重试;显式 false 时走裸 fetch // (用于登录/刷新/登出等 auth 端点:它们本就不需要 Bearer,且可避免「刷新请求触发刷新」的递归)。 // 查询参数走 opts.params(URLSearchParams),不要拼进 path。 @@ -96,7 +96,7 @@ export async function request( path: string, opts?: { params?: URLSearchParams; body?: unknown; auth?: boolean }, ): Promise { - const params = withLocale(opts?.params) + const params = opts?.params ?? new URLSearchParams() const query = params.toString() const fullPath = path const { sig, ts, nonce } = await clientSign(method, fullPath, query) @@ -105,6 +105,7 @@ export async function request( "X-Sign": sig, "X-Sign-Ts": ts, "X-Sign-Nonce": nonce, + ...localeHeader(), } const body = opts?.body if (body !== undefined) headers["Content-Type"] = "application/json" @@ -183,16 +184,15 @@ function ssgToken(): string { } export async function requestSsg(path: string): Promise { - // 组合 SSG_TOKEN 与 ?locale=(构建期按当前语言选列,后端 parseLocale 已支持)。 + // SSG_TOKEN 经 query 传入(密钥不进客户端 bundle,仅构建期从 env 读取); + // 语言经 X-Locale 头透传(后端 parseLocale 优先读此头,按当前构建语言选列)。 const t = ssgToken() const parts: string[] = [] if (t) parts.push("token=" + encodeURIComponent(t)) - const loc = currentLocale() - if (loc) parts.push("locale=" + encodeURIComponent(loc)) const base = config.baseApiSsg.replace(/\/$/, "") const url = base + path + (parts.length ? (path.includes("?") ? "&" : "?") + parts.join("&") : "") try { - const res = await fetch(url) + const res = await fetch(url, { headers: localeHeader() }) if (!res.ok) return null return (await res.json()) as T } catch { diff --git a/src/lib/ssg.ts b/src/lib/ssg.ts index c1ec97f..6ba48b6 100644 --- a/src/lib/ssg.ts +++ b/src/lib/ssg.ts @@ -2,7 +2,7 @@ // // 本文件只装「构建期 SSG 取数」:所有函数调用 /api/internal/ssg/* 内部端口 // (仅 127.0.0.1,nginx 不反代,由 astro build 时的 Node 进程调用), -// 走 request.ts 的 requestSsg()(自动带 SSG_TOKEN + ?locale=)。 +// 走 request.ts 的 requestSsg()(自动带 SSG_TOKEN + X-Locale 头)。 // // 与 api.ts 的「浏览器运行期公开业务 + 鉴权 + 收藏 + 历史」隔离, // 避免「仅构建期才有意义」的代码混进客户端逻辑(这些函数从不进客户端 bundle)。 @@ -44,6 +44,7 @@ export async function getSsgHotBrands(): Promise { // 接口:GET /api/internal/ssg/street-snaps/popular(SSG 内部端口,构建期 Node 调用) // 首页「Street Style」热门街拍(按图片数前 N,返回完整对象含封面)。 +// locale 透传:后端按 locale 返回中/英标题(中文为空回落英文)。 export async function getSsgStreetSnapPopular(limit = 12): Promise { const j = await requestSsg<{ data?: any[] }>(`${endpoints.ssg.streetSnapPopular}?limit=${limit}`) return (j?.data ?? []).map((b: any) => ({ diff --git a/src/lib/ssr.ts b/src/lib/ssr.ts index 9582caa..e63bc8e 100644 --- a/src/lib/ssr.ts +++ b/src/lib/ssr.ts @@ -12,7 +12,7 @@ import { ssrBase } from "./request" import { endpoints } from "./endpoints" -import { withLocale } from "./i18n" +import { currentLocale } from "./i18n" import type { RawArticle, RawStreetSnap } from "./api" export type SsrArticleResult = { @@ -32,10 +32,9 @@ export async function getSsrArticle( if (!id) return { article: null, error: "missing", preview: false, imageTotal: 0 } const b = ssrBase().replace(/\/$/, "") if (!b) return { article: null, error: "failed", preview: false, imageTotal: 0 } - const p = withLocale(undefined, locale) - const qs = p.toString() ? "?" + p.toString() : "" + const headers = locale ? { "X-Locale": locale } : { "X-Locale": currentLocale() } try { - const res = await fetch(`${b}${endpoints.public.runwayLook(id)}${qs}`) + const res = await fetch(`${b}${endpoints.public.runwayLook(id)}`, { headers }) if (!res.ok) return { article: null, error: "failed", preview: false, imageTotal: 0 } const json: any = await res.json() return { @@ -65,10 +64,9 @@ export async function getSsrStreetSnap( if (!id) return { snap: null, error: "missing", preview: false, imageTotal: 0 } const b = ssrBase().replace(/\/$/, "") if (!b) return { snap: null, error: "failed", preview: false, imageTotal: 0 } - const p = withLocale(undefined, locale) - const qs = p.toString() ? "?" + p.toString() : "" + const headers = locale ? { "X-Locale": locale } : { "X-Locale": currentLocale() } try { - const res = await fetch(`${b}${endpoints.public.streetSnap(id)}${qs}`) + const res = await fetch(`${b}${endpoints.public.streetSnap(id)}`, { headers }) if (!res.ok) return { snap: null, error: "failed", preview: false, imageTotal: 0 } const json: any = await res.json() return {