| import axios, { AxiosError, type AxiosRequestConfig } from 'axios' |
|
|
| export type ApiError = { |
| status: number |
| message: string |
| data?: unknown |
| } |
|
|
| export type ApiResponse<T> = { |
| code: number |
| msg: string |
| data?: T | null |
| } |
|
|
| const defaultBaseUrl = import.meta.env.PROD ? '/api/v1' : 'http://localhost:8000/api/v1' |
| const baseURL = import.meta.env.VITE_API_BASE_URL || defaultBaseUrl |
|
|
| export const http = axios.create({ |
| baseURL, |
| timeout: 30000, |
| }) |
|
|
| function normalizePath(path: string) { |
| if (path.startsWith('http://') || path.startsWith('https://')) return path |
| return path.replace(/^\/+/, '') |
| } |
|
|
| function extractWrappedErrorMessage(data: unknown) { |
| if (!data || typeof data !== 'object') return undefined |
| const obj = data as Record<string, unknown> |
| const msg = obj.msg |
| const code = obj.code |
| if (typeof msg === 'string' && typeof code === 'number') return `API ${code}: ${msg}` |
| if (typeof msg === 'string') return msg |
| return undefined |
| } |
|
|
| function toApiError(error: unknown): ApiError { |
| if (axios.isAxiosError(error)) { |
| const axiosError = error as AxiosError |
| const status = axiosError.response?.status ?? 0 |
| const wrappedMsg = extractWrappedErrorMessage(axiosError.response?.data) |
| const message = |
| wrappedMsg || |
| (typeof axiosError.response?.data === 'string' |
| ? axiosError.response.data |
| : axiosError.message) |
|
|
| return { |
| status, |
| message, |
| data: axiosError.response?.data, |
| } |
| } |
|
|
| return { |
| status: 0, |
| message: error instanceof Error ? error.message : String(error), |
| } |
| } |
|
|
| export async function apiGet<T>(path: string, config?: AxiosRequestConfig) { |
| try { |
| const res = await http.get<T>(normalizePath(path), config) |
| return res.data |
| } catch (error) { |
| throw toApiError(error) |
| } |
| } |
|
|
| export function unwrapApiResponse<T>(value: unknown) { |
| if (!value || typeof value !== 'object') return { ok: false as const, error: '响应不是对象' } |
| const obj = value as Record<string, unknown> |
| if (typeof obj.code !== 'number' || typeof obj.msg !== 'string') { |
| return { ok: false as const, error: '响应缺少 code/msg 字段' } |
| } |
| const code = obj.code |
| const msg = obj.msg |
| const data = obj.data as T | null | undefined |
| if (code !== 200) { |
| return { ok: false as const, error: `API ${code}: ${msg}`, code, msg, data } |
| } |
| return { ok: true as const, data } |
| } |
|
|
| export async function apiGetWrapped<T>(path: string, config?: AxiosRequestConfig) { |
| const raw = await apiGet<unknown>(path, config) |
| const parsed = unwrapApiResponse<T>(raw) |
| if (parsed.ok) return parsed.data as T |
| if (parsed.error === '响应缺少 code/msg 字段') return raw as T |
| const apiError: ApiError = { |
| status: 0, |
| message: parsed.error, |
| data: raw, |
| } |
| throw apiError |
| } |
|
|
| export async function apiPost<TRes, TReq>( |
| path: string, |
| data: TReq, |
| config?: AxiosRequestConfig, |
| ) { |
| try { |
| const res = await http.post<TRes>(normalizePath(path), data, config) |
| return res.data |
| } catch (error) { |
| throw toApiError(error) |
| } |
| } |
|
|
| export async function apiPostWrapped<TRes, TReq>( |
| path: string, |
| data: TReq, |
| config?: AxiosRequestConfig, |
| ) { |
| const raw = await apiPost<unknown, TReq>(path, data, config) |
| const parsed = unwrapApiResponse<TRes>(raw) |
| if (parsed.ok) return parsed.data as TRes |
| if (parsed.error === '响应缺少 code/msg 字段') return raw as TRes |
| const apiError: ApiError = { |
| status: 0, |
| message: parsed.error, |
| data: raw, |
| } |
| throw apiError |
| } |
|
|
| export async function apiGetText(path: string, config?: AxiosRequestConfig) { |
| try { |
| const res = await http.get<string>(normalizePath(path), { |
| ...config, |
| responseType: 'text', |
| }) |
| return res.data |
| } catch (error) { |
| throw toApiError(error) |
| } |
| } |
|
|