Files
one-pipe-system/src/utils/http/index.ts
luo 2c8524485b
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m24s
fix: 资产信息字段显示隐藏根据角色
2026-07-13 16:40:35 +08:00

326 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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