@ -1,18 +1,18 @@
|
||||
// src/lib/api.ts — 后端 API 统一客户端(所有接口的唯一出入口)
|
||||
//
|
||||
// 集中封装全部 /api/v1 调用:公开查询(/api/v1/public/*)、账号体系(/api/v1/auth/*)、
|
||||
// 以及构建期专用的 SSG 接口(/api/v1/ssg/*,仅在 Node 构建期使用)。
|
||||
// 以及构建期专用的 SSG 接口(/api/internal/ssg/*,内部契约,仅 Node 构建期使用)。
|
||||
// 任何模块需要后端数据,都只从这里取,不要再自己写 fetch。
|
||||
//
|
||||
// base URL 解析规则(全项目统一两个基准地址,命名见下):
|
||||
// BASE_API —— 公开/运行期 API 基址(浏览器运行期 + 按需渲染 SSR)。
|
||||
// 空值 = 同源相对 /api;DEV 构建期固定走线上域名。
|
||||
// BASE_API_SSG —— 构建期 Node 调 /api/ssg/* 的「内部端口」(默认 8091),
|
||||
// BASE_API_SSG —— 构建期 Node 调 /api/internal/ssg/* 的「内部端口」(默认 8091),
|
||||
// 与 BASE_API 完全分离,绝不回落到公开/运行期端口(避免内部全量数据外泄)。
|
||||
//
|
||||
// - getBase():公开接口 base。DEV(astro dev)→ 线上域名;其余 → BASE_API(空则同源相对 /api)。
|
||||
// - ssrBase():按需渲染(on-demand)详情取数 base,BASE_API > 本地 8090 回落。
|
||||
// - ssgBase():构建期 Node 调 /api/ssg/*,只读 BASE_API_SSG。
|
||||
// - ssgBase():构建期 Node 调 /api/internal/ssg/*,只读 BASE_API_SSG。
|
||||
|
||||
// 加载即安装全局 fetch 拦截器:把 API 请求/响应记录到调试日志(控制台/文件)。
|
||||
import "@/lib/http"
|
||||
@ -76,6 +76,8 @@ export interface AuthUser {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
/** 用户等级:free(普通)| vip(高级,图集详情页可见高清原图)。 */
|
||||
tier: 'free' | 'vip'
|
||||
}
|
||||
|
||||
// SSG 构建专用类型
|
||||
@ -110,7 +112,7 @@ export function ssrBase(): string {
|
||||
}
|
||||
|
||||
// ===================== i18n:请求语言透传 =====================
|
||||
// 后端 /api/v1/public/* 与 /api/v1/ssg/* 都通过 ?locale=cn|en(优先级最高,高于 Accept-Language)
|
||||
// 后端 /api/v1/public/* 与 /api/internal/ssg/* 都通过 ?locale=cn|en(优先级最高,高于 Accept-Language)
|
||||
// 决定 name / title / brand_name 选用 *_en 还是 *_cn 列(service.pickLocale 选列并缺翻译回落),
|
||||
// 出参字段名保持不变,故前端契约无需改动,仅需在请求上带上当前语言。
|
||||
//
|
||||
@ -420,7 +422,7 @@ export async function fetchMe(): Promise<AuthUser | null> {
|
||||
if (typeof localStorage === "undefined") return null
|
||||
if (!getAccessToken()) return null
|
||||
try {
|
||||
const res = await authedFetch(`${BASE_API_URL}/api/v1/auth/me?locale=${encodeURIComponent(currentLocale())}`)
|
||||
const res = await authedFetch(`${BASE_API_URL}/api/v1/me?locale=${encodeURIComponent(currentLocale())}`)
|
||||
if (!res.ok) {
|
||||
clearSession()
|
||||
return null
|
||||
@ -462,8 +464,9 @@ export async function logout(): Promise<void> {
|
||||
const refresh = getRefreshToken()
|
||||
if (refresh) {
|
||||
try {
|
||||
await fetch(`${BASE_API_URL}/api/v1/auth/logout`, {
|
||||
method: "POST",
|
||||
// 会话即 refresh token:单设备登出 = 吊销当前会话
|
||||
await fetch(`${BASE_API_URL}/api/v1/auth/sessions/current`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refresh_token: refresh }),
|
||||
})
|
||||
@ -510,7 +513,7 @@ export async function getHistoryMeta(id: string): Promise<HistoryMeta | null> {
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== SSG 构建专用(/api/ssg/*,仅 Node 构建期) =====================
|
||||
// ===================== SSG 构建专用(/api/internal/ssg/*,仅 Node 构建期) =====================
|
||||
|
||||
function ssgEnv(name: string): string | undefined {
|
||||
const e = import.meta.env as any
|
||||
@ -546,18 +549,18 @@ async function ssgRequest<T>(path: string): Promise<T | null> {
|
||||
}
|
||||
}
|
||||
|
||||
// 接口:GET /api/v1/ssg/index/runway(SSG 内部端口,构建期 Node 调用)
|
||||
// 接口:GET /api/internal/ssg/home/runway(SSG 内部端口,构建期 Node 调用)
|
||||
// 首页(/index/)runway 区块数据,含多个模块;hotBrand 为其中之一(当前仅此模块)。
|
||||
export async function getSsgIndexRunway(): Promise<{ hotBrand: SsgPopularBrand[] }> {
|
||||
const j = await ssgRequest<{ data?: { hotBrand?: SsgPopularBrand[] } }>(`/api/v1/ssg/index/runway`)
|
||||
const j = await ssgRequest<{ data?: { hotBrand?: SsgPopularBrand[] } }>(`/api/internal/ssg/home/runway`)
|
||||
return j?.data ?? { hotBrand: [] }
|
||||
}
|
||||
|
||||
// 接口:GET /api/ssg/brands(SSG 内部端口,构建期 Node 调用)
|
||||
// 接口:GET /api/internal/ssg/brands/popular(SSG 内部端口,构建期 Node 调用)
|
||||
// SSG 构建期:走秀页热门品牌(按图片总数排名,结果按热度顺序)。
|
||||
// 后端固定返回前 30 个;侧栏 filter 展示前 10,品牌弹窗 HOT 标签展示全部。
|
||||
export async function getSsgHotBrands(): Promise<BrandEntry[]> {
|
||||
const j = await ssgRequest<{ data?: any[] }>("/api/v1/ssg/brands/hot")
|
||||
const j = await ssgRequest<{ data?: any[] }>("/api/internal/ssg/brands/popular")
|
||||
return (j?.data ?? []).map((b: any) => {
|
||||
const nameEn = (b.name_en || b.name || "").toString()
|
||||
return {
|
||||
@ -670,10 +673,10 @@ export async function getStreetSnapDetailAuthed(
|
||||
}
|
||||
}
|
||||
|
||||
// 接口:GET /api/v1/ssg/street-snaps/popular(SSG 内部端口,构建期 Node 调用)
|
||||
// 接口:GET /api/internal/ssg/street-snaps/popular(SSG 内部端口,构建期 Node 调用)
|
||||
// 首页「Street Style」热门街拍(按图片数前 N,返回完整对象含封面)。
|
||||
export async function getSsgStreetSnapPopular(limit = 12): Promise<StreetSnapItem[]> {
|
||||
const j = await ssgRequest<{ data?: any[] }>(`/api/v1/ssg/street-snaps/popular?limit=${limit}`)
|
||||
const j = await ssgRequest<{ data?: any[] }>(`/api/internal/ssg/street-snaps/popular?limit=${limit}`)
|
||||
return (j?.data ?? []).map((b: any) => ({
|
||||
id: String(b.id),
|
||||
title: b.title ?? "",
|
||||
|
||||
Reference in New Issue
Block a user