105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import axios, { type AxiosInstance, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
|
||
import type { ApiResponse } from './types'
|
||
|
||
/**
|
||
* 扩展 AxiosInstance 类型,修正返回值类型
|
||
* 响应拦截器直接返回 res.result,所以实际返回 T 而非 AxiosResponse<T>
|
||
*/
|
||
interface CustomAxiosInstance extends Omit<AxiosInstance, 'get' | 'post' | 'put' | 'delete' | 'patch'> {
|
||
get<T = any>(url: string, config?: InternalAxiosRequestConfig): Promise<T>
|
||
post<T = any>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<T>
|
||
put<T = any>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<T>
|
||
delete<T = any>(url: string, config?: InternalAxiosRequestConfig): Promise<T>
|
||
patch<T = any>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<T>
|
||
}
|
||
|
||
// 创建axios实例
|
||
const axiosInstance: AxiosInstance = axios.create({
|
||
baseURL: '/api',
|
||
timeout: 30000,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
})
|
||
|
||
// 请求拦截器
|
||
axiosInstance.interceptors.request.use(
|
||
(config) => {
|
||
// 从localStorage获取token,统一注入 Authorization 头
|
||
// 说明:后端已强制从 JWT 解析用户身份,X-User-ID 头不再被读取,故不再发送
|
||
const token = localStorage.getItem('token')
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`
|
||
}
|
||
|
||
return config
|
||
},
|
||
(error) => {
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
// 401 兜底处理:会话过期时统一登出并跳登录页。
|
||
// 用 debounce 标记 + 懒加载 store/router,避免并发 401 反复跳转、以及与 auth/router 形成循环依赖
|
||
let handlingUnauthorized = false
|
||
async function handleUnauthorized() {
|
||
if (handlingUnauthorized) return
|
||
handlingUnauthorized = true
|
||
try {
|
||
const [authModule, routerModule] = await Promise.all([
|
||
import('@/stores/auth'),
|
||
import('@/router'),
|
||
])
|
||
try {
|
||
authModule.useAuthStore().logout()
|
||
} catch (e) {
|
||
console.warn('handleUnauthorized: logout 失败', e)
|
||
}
|
||
const router = routerModule.default
|
||
if (router.currentRoute.value.path !== '/login') {
|
||
router.push('/login').catch(() => {})
|
||
}
|
||
} finally {
|
||
// 留一点时间让路由跳转完成,避免这段时间内的并发 401 又触发一次
|
||
setTimeout(() => { handlingUnauthorized = false }, 1500)
|
||
}
|
||
}
|
||
|
||
// 响应拦截器
|
||
axiosInstance.interceptors.response.use(
|
||
(response: AxiosResponse<ApiResponse>) => {
|
||
const res = response.data
|
||
|
||
// 统一响应格式处理
|
||
// 后端所有接口返回HTTP 200,错误通过code字段标识
|
||
if (res.code !== 0) {
|
||
// 兼容部分接口在 200 体里返回 code=401 的情况
|
||
if (res.code === 401) {
|
||
handleUnauthorized()
|
||
return Promise.reject(new Error(res.message || '登录已过期,请重新登录'))
|
||
}
|
||
// 错误处理
|
||
console.error('API Error:', res.message)
|
||
return Promise.reject(new Error(res.message || '请求失败'))
|
||
}
|
||
|
||
// 返回result字段的数据
|
||
return res.result as any
|
||
},
|
||
(error) => {
|
||
// 鉴权中间件返回 HTTP 401(携带统一响应体):会走到这里,触发登出兜底。
|
||
// 原来注释里说"由 401 拦截器兜底",但实际并没有实现,导致会话过期后所有请求静默失败、用户被困在界面里
|
||
if (error?.response?.status === 401) {
|
||
handleUnauthorized()
|
||
}
|
||
console.error('Request Error:', error)
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
// 导出类型正确的实例
|
||
const request = axiosInstance as CustomAxiosInstance
|
||
|
||
export default request
|
||
|