import axios, { InternalAxiosRequestConfig, AxiosRequestConfig, AxiosResponse } from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/store/modules/user' import { ApiStatus } from './status' import type { RequestOptions, ErrorMessageMode } from '@/types/api' const axiosInstance = axios.create({ timeout: 15000, // 请求超时时间(毫秒) baseURL: import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL, // 开发服务器使用代理,其他模式使用完整URL withCredentials: false, // 异步请求携带cookie transformRequest: [ (data) => { // 如果是 FormData,不进行转换 if (data instanceof FormData) { return data } // 其他数据转换为 JSON 字符串 return JSON.stringify(data) } ], validateStatus: (status) => status >= 200 && status < 300, // 只接受 2xx 的状态码 headers: { get: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' }, post: { 'Content-Type': 'application/json;charset=utf-8' } }, transformResponse: [ (data, headers) => { const contentType = headers['content-type'] if (contentType && contentType.includes('application/json')) { try { return JSON.parse(data) } catch { return data } } return data } ] }) // 请求拦截器 axiosInstance.interceptors.request.use( (request: InternalAxiosRequestConfig) => { const { accessToken } = useUserStore() // 如果 token 存在,则设置请求头 if (accessToken) { // 如果是 FormData,不要覆盖 Content-Type (让浏览器自动设置 boundary) if (request.data instanceof FormData) { request.headers.set('Authorization', `Bearer ${accessToken}`) // 删除 Content-Type,让浏览器自动设置 delete request.headers['Content-Type'] } else { request.headers.set({ 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}` }) } } return request // 返回修改后的配置 }, (error) => { ElMessage.error('服务器异常!') // 显示错误消息 return Promise.reject(error) // 返回拒绝的 Promise } ) // Token 刷新相关 let isRefreshing = false // 是否正在刷新 token let failedQueue: Array<{ resolve: (value?: any) => void reject: (reason?: any) => void }> = [] // 失败队列,存储因 token 过期而失败的请求 let isHandlingAuthExpiry = false // 处理失败队列 const processQueue = (error: any, token: string | null = null) => { failedQueue.forEach((prom) => { if (error) { prom.reject(error) } else { prom.resolve(token) } }) failedQueue = [] } // 响应拦截器 axiosInstance.interceptors.response.use( (response: AxiosResponse) => { // 401 未授权 - 尝试刷新 token if (response.data.code === ApiStatus.unauthorized) { const userStore = useUserStore() const originalRequest = response.config as any // 如果没有 refreshToken,直接清理本地状态(不调用退出接口) if (!userStore.refreshToken) { clearLocalStateAndRedirect() return Promise.reject(response) } // 如果正在刷新 token,将请求加入队列 if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }) }) .then((token) => { originalRequest.headers['Authorization'] = `Bearer ${token}` return axiosInstance.request(originalRequest) }) .catch((err) => { return Promise.reject(err) }) } // 标记正在刷新 originalRequest._retry = true isRefreshing = true // 调用刷新 token 接口 return new Promise((resolve, reject) => { axiosInstance .post('/api/auth/refresh', { refreshToken: userStore.refreshToken }) .then((res) => { const { data } = res // 刷新成功 if (data.code === ApiStatus.success && data.data?.access_token) { // 更新 token userStore.setToken(data.data.access_token, data.data.refresh_token) // 更新请求头 originalRequest.headers['Authorization'] = `Bearer ${data.data.access_token}` axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${data.data.access_token}` // 处理队列中的请求 processQueue(null, data.data.access_token) // 重试原请求 resolve(axiosInstance.request(originalRequest)) } else { // 刷新失败 - 清理本地状态(不调用退出接口) processQueue(new Error('Token refresh failed'), null) clearLocalStateAndRedirect() reject(res) } }) .catch((err) => { // 刷新失败 - 清理本地状态(不调用退出接口) processQueue(err, null) clearLocalStateAndRedirect() reject(err) }) .finally(() => { isRefreshing = false }) }) } return response }, (error) => { if (axios.isCancel(error)) { console.log('repeated request: ' + error.message) return Promise.reject(error) } // 处理 HTTP 401 状态码 - 直接清理本地状态(不调用退出接口) if (error.response && error.response.status === 401) { console.warn('HTTP 401 Unauthorized - 强制退出登录') clearLocalStateAndRedirect() return Promise.reject(error) } // 注意:错误处理现在在request函数中根据requestOptions处理 return Promise.reject(error) } ) // 扩展的请求配置接口 interface ExtendedRequestConfig extends AxiosRequestConfig { requestOptions?: RequestOptions } // 处理请求配置 function processRequestConfig(config: ExtendedRequestConfig): AxiosRequestConfig { const { requestOptions, ...axiosConfig } = config // 应用自定义请求选项 if (requestOptions) { // 处理是否携带token if (requestOptions.withToken === false) { axiosConfig.headers = { ...axiosConfig.headers } delete axiosConfig.headers?.Authorization } // 处理是否添加时间戳 if (requestOptions.joinTime) { const timestamp = Date.now() if (axiosConfig.method?.toUpperCase() === 'GET') { axiosConfig.params = { ...axiosConfig.params, _t: timestamp } } } // 处理API URL if (requestOptions.apiUrl) { axiosConfig.baseURL = requestOptions.apiUrl } } return axiosConfig } // 处理错误消息 function handleErrorMessage( error: any, mode: ErrorMessageMode = 'message', show404Error: boolean = true ) { if (mode === 'none') return const backendMessage = error.response?.data?.msg const httpStatus = error.response?.status // 401 过期类错误由 clearLocalStateAndRedirect 统一提示,避免重复弹出后端报错。 if (httpStatus === 401 || error.response?.data?.code === ApiStatus.unauthorized) { return } // 如果是404错误,且未配置显示404错误,则不显示任何提示 if (httpStatus === 404 && !show404Error) { return } // 如果后端返回了错误信息,使用后端信息 if (backendMessage) { if (mode === 'modal') { // TODO: 可以使用 ElMessageBox 显示模态框 ElMessage.error(backendMessage) } else { ElMessage.error(backendMessage) } return } // 其他错误显示通用提示 const message = '请求超时或服务器异常!' if (mode === 'modal') { // TODO: 可以使用 ElMessageBox 显示模态框 ElMessage.error(message) } else { ElMessage.error(message) } } // 请求 async function request(config: ExtendedRequestConfig): Promise { const processedConfig = processRequestConfig(config) // 对 POST | PUT | PATCH 请求特殊处理 if ( processedConfig.method?.toUpperCase() === 'POST' || processedConfig.method?.toUpperCase() === 'PUT' || processedConfig.method?.toUpperCase() === 'PATCH' ) { // 如果已经有 data,则保留原有的 data if (processedConfig.params && !processedConfig.data) { processedConfig.data = processedConfig.params processedConfig.params = undefined // 使用 undefined 而不是空对象 } } try { const res = await axiosInstance.request(processedConfig) return res.data } catch (e) { if (axios.isAxiosError(e)) { // 默认显示错误消息,除非明确设置为 'none' const errorMode = config.requestOptions?.errorMessageMode || 'message' const show404Error = config.requestOptions?.show404Error !== undefined ? config.requestOptions.show404Error : true handleErrorMessage(e, errorMode, show404Error) } return Promise.reject(e) } } // API 方法集合 const api = { get(config: ExtendedRequestConfig): Promise { return request({ ...config, method: 'GET' }) // GET 请求 }, post(config: ExtendedRequestConfig): Promise { return request({ ...config, method: 'POST' }) // POST 请求 }, put(config: ExtendedRequestConfig): Promise { return request({ ...config, method: 'PUT' }) // PUT 请求 }, patch(config: ExtendedRequestConfig): Promise { return request({ ...config, method: 'PATCH' }) // PATCH 请求 }, del(config: ExtendedRequestConfig): Promise { return request({ ...config, method: 'DELETE' }) // DELETE 请求 }, request(config: ExtendedRequestConfig): Promise { return request({ ...config }) // 通用请求 } } // 仅清理本地状态(不调用接口)- 用于401等情况 const clearLocalStateAndRedirect = () => { if (isHandlingAuthExpiry) return isHandlingAuthExpiry = true ElMessage.error('登录已过期,请重新登录') setTimeout(() => { useUserStore().clearLocalState() isHandlingAuthExpiry = false }, 1000) } export default api