import { useAuthStore } from '@/stores/auth' const BASE = '/api/v1' export class ApiError extends Error { status: number constructor(status: number, message: string) { super(message) this.status = status } } interface RequestOptions { method?: string body?: unknown query?: Record } function buildUrl(path: string, query?: RequestOptions['query']): string { if (!query) return BASE + path const qs = new URLSearchParams() for (const [k, v] of Object.entries(query)) { if (v !== undefined && v !== '') qs.set(k, String(v)) } const s = qs.toString() return s ? `${BASE}${path}?${s}` : BASE + path } async function parseError(resp: Response): Promise { let message = `请求失败(${resp.status})` try { const body = (await resp.json()) as { error?: string; hint?: string } if (body.error) message = body.hint ? `${body.hint}|${body.error}` : body.error } catch { /* 非 JSON 响应体,保留默认消息 */ } if (resp.status === 401) useAuthStore().logout() // 全局限速(429)整页拦截;登录接口的 429 是账号锁定,留在表单内提示 if (resp.status === 429 && !resp.url.includes('/auth/login') && location.pathname !== '/blocked') { location.assign('/blocked') } throw new ApiError(resp.status, message) } /** 统一请求封装:注入 JWT、401 登出、错误转 ApiError */ export async function request(path: string, opts: RequestOptions = {}): Promise { const auth = useAuthStore() const headers: Record = { 'Content-Type': 'application/json' } if (auth.token) headers.Authorization = `Bearer ${auth.token}` const resp = await fetch(buildUrl(path, opts.query), { method: opts.method ?? 'GET', headers, body: opts.body === undefined ? undefined : JSON.stringify(opts.body), }) if (!resp.ok) await parseError(resp) if (resp.status === 204) return undefined as T return (await resp.json()) as T } export const mockOn = import.meta.env.VITE_MOCK === '1' /** mock 数据延时返回,模拟网络延迟 */ export function mocked(data: T, ms = 200): Promise { return new Promise((resolve) => setTimeout(() => resolve(structuredClone(data)), ms)) }