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