Files
nl-um-vue-ts/src/api/request/index.ts
2026-08-12 13:14:32 +08:00

105 lines
3.6 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, { 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