326 lines
9.9 KiB
TypeScript
326 lines
9.9 KiB
TypeScript
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<T = any>(config: ExtendedRequestConfig): Promise<T> {
|
||
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<T>(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<T>(config: ExtendedRequestConfig): Promise<T> {
|
||
return request({ ...config, method: 'GET' }) // GET 请求
|
||
},
|
||
post<T>(config: ExtendedRequestConfig): Promise<T> {
|
||
return request({ ...config, method: 'POST' }) // POST 请求
|
||
},
|
||
put<T>(config: ExtendedRequestConfig): Promise<T> {
|
||
return request({ ...config, method: 'PUT' }) // PUT 请求
|
||
},
|
||
patch<T>(config: ExtendedRequestConfig): Promise<T> {
|
||
return request({ ...config, method: 'PATCH' }) // PATCH 请求
|
||
},
|
||
del<T>(config: ExtendedRequestConfig): Promise<T> {
|
||
return request({ ...config, method: 'DELETE' }) // DELETE 请求
|
||
},
|
||
request<T>(config: ExtendedRequestConfig): Promise<T> {
|
||
return request({ ...config }) // 通用请求
|
||
}
|
||
}
|
||
|
||
// 仅清理本地状态(不调用接口)- 用于401等情况
|
||
const clearLocalStateAndRedirect = () => {
|
||
if (isHandlingAuthExpiry) return
|
||
isHandlingAuthExpiry = true
|
||
|
||
ElMessage.error('登录已过期,请重新登录')
|
||
setTimeout(() => {
|
||
useUserStore().clearLocalState()
|
||
isHandlingAuthExpiry = false
|
||
}, 1000)
|
||
}
|
||
|
||
export default api
|