update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-08-11 22:29:47 +08:00
commit 5a8dbf41cb
62 changed files with 9514 additions and 0 deletions

115
src/lib/api.ts Normal file
View File

@ -0,0 +1,115 @@
// 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) }
}