Files
frontend_v2/src/lib/api.ts
toom1996 5a8dbf41cb
Some checks failed
deploy / deploy (push) Has been cancelled
update
2026-08-11 22:29:47 +08:00

116 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/lib/api.ts — 后端 API 统一客户端
// 集中所有 /api 调用base URL 解析、错误处理、端点封装。
//
// base URL 解析规则:
// - DEVastro dev前端 4345 + 后端 8090一律回落 http://localhost:8090CORS=*)。
// - 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<T>(path: string, params?: URLSearchParams): Promise<T> {
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<BrandEntry[]> {
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<number[]> {
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<ArticlePage> {
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) }
}