125 lines
3.8 KiB
JavaScript
125 lines
3.8 KiB
JavaScript
import axios from 'axios'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { Message } from '@/components/ui'
|
||
|
||
const AUTH_ERROR_CODE = 401
|
||
const SESSION_REPLACED_CODE = 40101
|
||
|
||
const http = axios.create({
|
||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||
timeout: 15000,
|
||
headers: {
|
||
Accept: 'application/json',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
})
|
||
|
||
let redirectingToLogin = false
|
||
|
||
function redirectToLogin(reason) {
|
||
const currentPath = window.location.pathname.replace(/\/+$/, '') || '/'
|
||
if (currentPath === '/login' || redirectingToLogin) return
|
||
|
||
redirectingToLogin = true
|
||
const redirect = `${window.location.pathname}${window.location.search}${window.location.hash}`
|
||
const query = new URLSearchParams({ redirect })
|
||
if (reason) query.set('reason', reason)
|
||
window.location.assign(`/login?${query.toString()}`)
|
||
}
|
||
|
||
function isAuthenticationError(code) {
|
||
return code === AUTH_ERROR_CODE || code === SESSION_REPLACED_CODE
|
||
}
|
||
|
||
function handleUnauthorized(code) {
|
||
const authStore = useAuthStore()
|
||
authStore.clearSession()
|
||
redirectToLogin(code === SESSION_REPLACED_CODE ? 'session-replaced' : 'expired')
|
||
}
|
||
|
||
// 请求拦截器:注入 Bearer token(调用时读取 store,保证 pinia 已激活)
|
||
http.interceptors.request.use((config) => {
|
||
const authStore = useAuthStore()
|
||
if (authStore.loginInfo.token) {
|
||
config.headers.Authorization = `Bearer ${authStore.loginInfo.token}`
|
||
}
|
||
return config
|
||
})
|
||
|
||
// 响应拦截器:统一按 ApiResult 结构处理
|
||
http.interceptors.response.use(
|
||
(response) => {
|
||
const payload = response.data
|
||
|
||
// 文件下载(导出等):blob 响应没有 code 字段,原样透传响应本身,
|
||
// 调用方才能读到 headers(Content-Disposition 里的文件名、content-type 用于区分错误 JSON)
|
||
if (typeof Blob !== 'undefined' && payload instanceof Blob) {
|
||
return response
|
||
}
|
||
|
||
// 无 code 字段时按成功透传
|
||
if (payload?.code === undefined) {
|
||
return {
|
||
success: true,
|
||
code: 0,
|
||
message: '',
|
||
data: payload,
|
||
}
|
||
}
|
||
|
||
payload.success = payload.code === 0
|
||
|
||
if (payload.success) return response
|
||
|
||
if (isAuthenticationError(payload.code)) handleUnauthorized(payload.code)
|
||
|
||
const err = new Error(payload.message || '请求失败')
|
||
err.code = payload.code
|
||
err.data = payload.data
|
||
err.success = payload.success
|
||
throw err
|
||
},
|
||
(error) => {
|
||
const status = error.response?.status
|
||
const payload = error.response?.data
|
||
|
||
// 仅鉴权类错误(401 / 40101)才清会话并跳登录页;
|
||
// 网络异常(无 status)、服务器 5xx 等普通错误不应把用户登出
|
||
const errorCode = payload?.code ?? status ?? 0
|
||
if (isAuthenticationError(errorCode)) handleUnauthorized(errorCode)
|
||
|
||
const message =
|
||
payload?.message ||
|
||
(error.code === 'ECONNABORTED' ? '请求超时,请稍后重试' : '网络异常,请检查服务是否正常')
|
||
|
||
const requestError = new Error(message)
|
||
requestError.code = payload?.code ?? status ?? -1
|
||
requestError.data = payload?.data
|
||
requestError.success = false
|
||
|
||
// 服务端不可达(无 status)/ 5xx 属全局性问题,统一弹 Message 提示;
|
||
// 4xx 等业务错误由调用方自行决定提示方式。
|
||
// 打 toasted 标记:登录、上传/删除等已有自身 Message 的调用方据此去重,避免重复弹窗
|
||
if (!status || status >= 500) {
|
||
Message.error(message)
|
||
requestError.toasted = true
|
||
}
|
||
|
||
throw requestError
|
||
},
|
||
)
|
||
|
||
export function post(path, data, config) {
|
||
return http.post(path, data, config).then((r) => r.data)
|
||
}
|
||
|
||
export function put(path, data, config) {
|
||
return http.put(path, data, config).then((r) => r.data)
|
||
}
|
||
|
||
export function get(path, params, config) {
|
||
return http.get(path, { params, ...config }).then((r) => r.data)
|
||
}
|
||
|
||
export default http
|