Files
datahub/frontend/src/utils/request.ts
T

112 lines
3.4 KiB
TypeScript
Raw Normal View History

2026-03-18 13:53:36 +08:00
import { ApiError } from '@/types/api'
const DEFAULT_TIMEOUT = 15000
// 允许外部注入 token 获取函数,避免与 store 循环依赖
let tokenGetter: (() => string | null) | null = null
export function setTokenGetter(fn: () => string | null) {
tokenGetter = fn
}
2026-03-18 13:53:36 +08:00
interface RequestOptions extends RequestInit {
params?: Record<string, unknown>
timeout?: number
2026-03-18 13:53:36 +08:00
}
2026-03-23 09:46:34 +08:00
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''
2026-03-18 13:53:36 +08:00
async function request<T = unknown>(url: string, options: RequestOptions = {}): Promise<T> {
2026-03-23 09:46:34 +08:00
url = `${API_BASE_URL}${url}`
const token = tokenGetter?.() ?? localStorage.getItem('access_token')
2026-03-18 13:53:36 +08:00
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
// 拼接查询参数
if (options.params) {
const query = new URLSearchParams(
Object.entries(options.params)
.filter(([, v]) => v !== undefined && v !== null && v !== '')
.map(([k, v]) => [k, String(v)])
)
const separator = url.includes('?') ? '&' : '?'
url = `${url}${separator}${query}`
}
2026-03-18 20:08:04 +08:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { params: _params, timeout: _timeout, ...fetchOptions } = options
// 超时控制
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? DEFAULT_TIMEOUT)
try {
const response = await fetch(url, {
...fetchOptions,
headers,
signal: controller.signal,
})
2026-03-23 09:46:34 +08:00
// 401 未授权:跳过登录/注册接口(由页面自行处理错误提示),其余接口重定向到登录页
if (response.status === 401) {
2026-03-23 09:46:34 +08:00
const isAuthEndpoint = /\/api\/v1\/(login|register)$/.test(url)
if (!isAuthEndpoint) {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('user')
const redirect = encodeURIComponent(
window.location.pathname + window.location.search,
)
window.location.href = `/login?redirect=${redirect}`
}
throw new ApiError('登录已过期,请重新登录', 401)
}
// 防护非 JSON 响应(如网关 HTML 错误页)
let result
try {
result = await response.json()
} catch {
throw new ApiError('服务器返回了非预期的响应格式', response.status)
}
// 业务错误
if (result.code !== 0) {
throw new ApiError(result.message || '请求失败', result.code)
}
return result.data
} catch (error) {
if (error instanceof ApiError) throw error
if (error instanceof DOMException && error.name === 'AbortError') {
throw new ApiError('请求超时,请检查网络连接', 0)
}
throw error
} finally {
clearTimeout(timeoutId)
2026-03-18 13:53:36 +08:00
}
}
export const api = {
get: <T = unknown>(url: string, params?: Record<string, unknown>) =>
request<T>(url, { method: 'GET', params }),
post: <T = unknown>(url: string, data?: unknown) =>
request<T>(url, { method: 'POST', body: JSON.stringify(data) }),
put: <T = unknown>(url: string, data?: unknown) =>
request<T>(url, { method: 'PUT', body: JSON.stringify(data) }),
patch: <T = unknown>(url: string, data?: unknown) =>
request<T>(url, { method: 'PATCH', body: JSON.stringify(data) }),
delete: <T = unknown>(url: string) =>
request<T>(url, { method: 'DELETE' }),
}