// src/lib/api.ts — 后端 API 统一客户端 // 集中所有 /api 调用:base URL 解析、错误处理、端点封装。 // // base URL 解析规则: // - DEV(astro dev,前端 4345 + 后端 8090):一律回落 http://localhost:8090(CORS=*)。 // - SSR(构建/预渲染期):走 server-only 的 API_SSR,回落 PUBLIC_API_BASE。 // - 客户端(浏览器运行期):走 PUBLIC_API_BASE,置空时同源相对 /api(由 nginx 反代到 backend)。 export interface BrandEntry { id: number name: string article_count: number } export interface ArticleItem { id: number brand_id: number title: string cover: string brand_name: string year: number image_count: number published_at: number collection_type?: string season?: string season_code?: string gallery: string[] } export interface ArticleQuery { page?: number size?: number sort?: string brand_ids?: number[] collection_type?: string[] season?: string[] year?: string[] with_images?: number } export interface ArticlePage { data: ArticleItem[] total: number last_page: number } export function getBase(): string { const env = import.meta.env as any if (env.DEV) return "http://localhost:8090" if (env.SSR) return (env.API_SSR || env.PUBLIC_API_BASE || "").replace(/\/$/, "") return (env.PUBLIC_API_BASE || "").replace(/\/$/, "") } // 把相对图片路径补成绝对(仅客户端渲染卡片时用;空 base 时保留相对路径走同源) export function toAbs(url: string): string { if (!url) return "" return /^https?:\/\//i.test(url) ? url : getBase() + url } async function request(path: string, params?: URLSearchParams): Promise { const qs = params && params.toString() ? "?" + params.toString() : "" const url = getBase() + "/api" + path + qs const res = await fetch(url) if (!res.ok) throw new Error(`API ${res.status} ${url}`) return (await res.json()) as T } // 侧边栏品牌清单(含 article_count 计数) export async function getBrands(opts?: { size?: number only_with_articles?: number featured?: number }): Promise { const qs = new URLSearchParams() if (opts?.size) qs.set("size", String(opts.size)) if (opts?.only_with_articles) qs.set("only_with_articles", String(opts.only_with_articles)) if (opts?.featured) qs.set("featured", String(opts.featured)) const j = await request<{ data?: any[] }>("/public/brands", qs) return (j.data ?? []).map((b: any) => ({ id: Number(b.id), name: (b.name || b.show_name || "").toString(), article_count: Number(b.article_count ?? 0), })) } // 最近若干条秀场中提取的独特年份(降序去重,最多 6 个),用于筛选年份 chips export async function getYears(sample = 30): Promise { const qs = new URLSearchParams({ size: String(sample), sort: "year_desc" }) const j = await request<{ data?: any[] }>("/public/articles", qs) const ys = Array.from( new Set((j.data ?? []).map((it: any) => Number(it.year)).filter((y: number) => y > 0)) ) as number[] return ys.slice(0, 6) } // 文章/秀场列表:统一构造 QueryArray 参数(多选用 repeated params), // 并把后端 images[] 映射成 gallery 字符串数组(缩略图条用)。 export async function getArticles(q: ArticleQuery): Promise { const p = new URLSearchParams() p.set("page", String(q.page ?? 1)) p.set("size", String(q.size ?? 24)) if (q.sort) p.set("sort", q.sort) if (q.with_images) p.set("with_images", String(q.with_images)) if (q.brand_ids?.length) p.set("brand_ids", q.brand_ids.join(",")) // 多选:同一字段用 repeated params(后端 QueryArray 收到数组后 1 个走 = / 多个走 IN) for (const v of q.collection_type ?? []) p.append("collection_type", v) for (const v of q.season ?? []) p.append("season", v) for (const v of q.year ?? []) p.append("year", v) const j = await request<{ data?: any[]; total?: number; last_page?: number }>("/public/articles", p) const data: ArticleItem[] = (j.data ?? []).map((it: any) => ({ ...it, gallery: Array.isArray(it.images) ? it.images.map((im: any) => im.image || im) : [], })) return { data, total: Number(j.total ?? data.length), last_page: Number(j.last_page ?? 1) } }