227 lines
6.4 KiB
TypeScript
227 lines
6.4 KiB
TypeScript
/**
|
||
* HTTP 请求封装模块
|
||
* 基于 Axios 封装的 HTTP 请求工具,提供统一的请求/响应处理
|
||
*
|
||
* ## 主要功能
|
||
*
|
||
* - 请求/响应拦截器(自动添加 Token、统一错误处理)
|
||
* - 401 未授权自动登出(带防抖机制)
|
||
* - 请求失败自动重试(可配置)
|
||
* - 统一的成功/错误消息提示
|
||
* - 支持 GET/POST/PUT/DELETE 等常用方法
|
||
*
|
||
* @module utils/http
|
||
* @author Art Design Pro Team
|
||
*/
|
||
|
||
import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||
import { useUserStore } from '@/store/modules/user'
|
||
import i18n from '@/locales'
|
||
import { ApiStatus } from './status'
|
||
import { HttpError, handleError, showError, showSuccess } from './error'
|
||
import { $t } from '@/locales'
|
||
import { BaseResponse } from '@/types'
|
||
|
||
/** 当前语言(zh/en),供请求头 lang 使用,无 header 时后端按后台语言回包 */
|
||
function getRequestLang(): string {
|
||
const locale = i18n.global.locale as string | { value: string }
|
||
const lang = typeof locale === 'string' ? locale : (locale && 'value' in locale ? locale.value : 'zh')
|
||
return lang === 'en' ? 'en' : 'zh'
|
||
}
|
||
|
||
/** 请求配置常量(超时时间 30s) */
|
||
const REQUEST_TIMEOUT = 30000
|
||
const LOGOUT_DELAY = 500
|
||
const MAX_RETRIES = 0
|
||
const RETRY_DELAY = 1000
|
||
const UNAUTHORIZED_DEBOUNCE_TIME = 3000
|
||
|
||
/** 401防抖状态 */
|
||
let isUnauthorizedErrorShown = false
|
||
let unauthorizedTimer: NodeJS.Timeout | null = null
|
||
|
||
/** 扩展 AxiosRequestConfig */
|
||
interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
|
||
showErrorMessage?: boolean
|
||
showSuccessMessage?: boolean
|
||
}
|
||
|
||
const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env
|
||
|
||
/** Axios实例 */
|
||
const axiosInstance = axios.create({
|
||
timeout: REQUEST_TIMEOUT,
|
||
baseURL: VITE_API_URL,
|
||
withCredentials: VITE_WITH_CREDENTIALS === 'true',
|
||
validateStatus: (status) => status >= 200 && status < 300,
|
||
transformResponse: [
|
||
(data, headers) => {
|
||
const contentType = headers['content-type']
|
||
if (contentType?.includes('application/json')) {
|
||
try {
|
||
return JSON.parse(data)
|
||
} catch {
|
||
return data
|
||
}
|
||
}
|
||
return data
|
||
}
|
||
]
|
||
})
|
||
|
||
/** 请求拦截器 */
|
||
axiosInstance.interceptors.request.use(
|
||
(request: InternalAxiosRequestConfig) => {
|
||
const { accessToken } = useUserStore()
|
||
if (accessToken) request.headers.set('Authorization', `Bearer ` + accessToken)
|
||
request.headers.set('lang', getRequestLang())
|
||
|
||
if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) {
|
||
request.headers.set('Content-Type', 'application/json')
|
||
request.data = JSON.stringify(request.data)
|
||
}
|
||
|
||
return request
|
||
},
|
||
(error) => {
|
||
showError(createHttpError($t('httpMsg.requestConfigError'), ApiStatus.error))
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
/** 响应拦截器 */
|
||
axiosInstance.interceptors.response.use(
|
||
(response: AxiosResponse<BaseResponse>) => {
|
||
if (response.config.responseType === 'blob') return response
|
||
const { code, message } = response.data
|
||
if (code === ApiStatus.success) return response
|
||
if (code === ApiStatus.unauthorized) handleUnauthorizedError(message)
|
||
throw createHttpError(message || $t('httpMsg.requestFailed'), code)
|
||
},
|
||
(error) => {
|
||
if (error.response?.status === ApiStatus.unauthorized) handleUnauthorizedError()
|
||
return Promise.reject(handleError(error))
|
||
}
|
||
)
|
||
|
||
/** 统一创建HttpError */
|
||
function createHttpError(message: string, code: number) {
|
||
return new HttpError(message, code)
|
||
}
|
||
|
||
/** 处理401错误(带防抖) */
|
||
function handleUnauthorizedError(message?: string): never {
|
||
const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
|
||
|
||
if (!isUnauthorizedErrorShown) {
|
||
isUnauthorizedErrorShown = true
|
||
logOut()
|
||
|
||
unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME)
|
||
|
||
showError(error, true)
|
||
throw error
|
||
}
|
||
|
||
throw error
|
||
}
|
||
|
||
/** 重置401防抖状态 */
|
||
function resetUnauthorizedError() {
|
||
isUnauthorizedErrorShown = false
|
||
if (unauthorizedTimer) clearTimeout(unauthorizedTimer)
|
||
unauthorizedTimer = null
|
||
}
|
||
|
||
/** 退出登录函数 */
|
||
function logOut() {
|
||
setTimeout(() => {
|
||
useUserStore().logOut()
|
||
}, LOGOUT_DELAY)
|
||
}
|
||
|
||
/** 是否需要重试 */
|
||
function shouldRetry(statusCode: number) {
|
||
return [
|
||
ApiStatus.requestTimeout,
|
||
ApiStatus.internalServerError,
|
||
ApiStatus.badGateway,
|
||
ApiStatus.serviceUnavailable,
|
||
ApiStatus.gatewayTimeout
|
||
].includes(statusCode)
|
||
}
|
||
|
||
/** 请求重试逻辑 */
|
||
async function retryRequest<T>(
|
||
config: ExtendedAxiosRequestConfig,
|
||
retries: number = MAX_RETRIES
|
||
): Promise<T> {
|
||
try {
|
||
return await request<T>(config)
|
||
} catch (error) {
|
||
if (retries > 0 && error instanceof HttpError && shouldRetry(error.code)) {
|
||
await delay(RETRY_DELAY)
|
||
return retryRequest<T>(config, retries - 1)
|
||
}
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/** 延迟函数 */
|
||
function delay(ms: number) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||
}
|
||
|
||
/** 请求函数 */
|
||
async function request<T = any>(config: ExtendedAxiosRequestConfig): Promise<T> {
|
||
// POST | PUT 参数自动填充
|
||
if (
|
||
['POST', 'PUT'].includes(config.method?.toUpperCase() || '') &&
|
||
config.params &&
|
||
!config.data
|
||
) {
|
||
config.data = config.params
|
||
config.params = undefined
|
||
}
|
||
|
||
try {
|
||
const res = await axiosInstance.request<BaseResponse<T>>(config)
|
||
|
||
// 显示成功消息
|
||
if (config.showSuccessMessage && res.data.message) {
|
||
showSuccess(res.data.message)
|
||
}
|
||
|
||
if (config.responseType === 'blob') return res.data as T
|
||
|
||
return res.data.data as T
|
||
} catch (error) {
|
||
if (error instanceof HttpError && error.code !== ApiStatus.unauthorized) {
|
||
const showMsg = config.showErrorMessage !== false
|
||
showError(error, showMsg)
|
||
}
|
||
return Promise.reject(error)
|
||
}
|
||
}
|
||
|
||
/** API方法集合 */
|
||
const api = {
|
||
get<T>(config: ExtendedAxiosRequestConfig) {
|
||
return retryRequest<T>({ ...config, method: 'GET' })
|
||
},
|
||
post<T>(config: ExtendedAxiosRequestConfig) {
|
||
return retryRequest<T>({ ...config, method: 'POST' })
|
||
},
|
||
put<T>(config: ExtendedAxiosRequestConfig) {
|
||
return retryRequest<T>({ ...config, method: 'PUT' })
|
||
},
|
||
del<T>(config: ExtendedAxiosRequestConfig) {
|
||
return retryRequest<T>({ ...config, method: 'DELETE' })
|
||
},
|
||
request<T>(config: ExtendedAxiosRequestConfig) {
|
||
return retryRequest<T>(config)
|
||
}
|
||
}
|
||
|
||
export default api
|