67 lines
2.2 KiB
TypeScript
67 lines
2.2 KiB
TypeScript
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<string, string | number | boolean | undefined>
|
||
}
|
||
|
||
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<never> {
|
||
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<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
||
const auth = useAuthStore()
|
||
const headers: Record<string, string> = { '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<T>(data: T, ms = 200): Promise<T> {
|
||
return new Promise((resolve) => setTimeout(() => resolve(structuredClone(data)), ms))
|
||
}
|