/** * 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) => { 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( config: ExtendedAxiosRequestConfig, retries: number = MAX_RETRIES ): Promise { try { return await request(config) } catch (error) { if (retries > 0 && error instanceof HttpError && shouldRetry(error.code)) { await delay(RETRY_DELAY) return retryRequest(config, retries - 1) } throw error } } /** 延迟函数 */ function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } /** 请求函数 */ async function request(config: ExtendedAxiosRequestConfig): Promise { // 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>(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(config: ExtendedAxiosRequestConfig) { return retryRequest({ ...config, method: 'GET' }) }, post(config: ExtendedAxiosRequestConfig) { return retryRequest({ ...config, method: 'POST' }) }, put(config: ExtendedAxiosRequestConfig) { return retryRequest({ ...config, method: 'PUT' }) }, del(config: ExtendedAxiosRequestConfig) { return retryRequest({ ...config, method: 'DELETE' }) }, request(config: ExtendedAxiosRequestConfig) { return retryRequest(config) } } export default api