import axios, { type AxiosError, type AxiosRequestConfig, type AxiosResponse } from 'axios' import { ElMessage } from 'element-plus' import { clearToken, getToken } from '@/utils/storage' /** 后端统一响应信封(见 PROJECT_STANDARDS 第 3 节) */ export interface ApiResult { code: number message: string data: T } const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, }) // 请求拦截:注入 Bearer 令牌 service.interceptors.request.use((config) => { const token = getToken() if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) /** * 令牌失效时的跳转去重标志。 * * 页面初始化时常常并发多个请求,令牌过期会同时触发多次 401; * 没有这个标志就会弹出多条提示并反复跳转。 */ let redirecting = false function handleUnauthorized(message: string): void { if (redirecting) return redirecting = true clearToken() ElMessage.error(message) // 用整页跳转而非 router.replace:既避免 request → router → store → request // 的循环依赖,也能顺带清空内存中的应用状态 const current = window.location.pathname + window.location.search window.location.href = `/login?redirect=${encodeURIComponent(current)}` window.setTimeout(() => { redirecting = false }, 1000) } service.interceptors.response.use( (response: AxiosResponse) => { // 文件流(导出下载)不是统一信封,直通给调用方处理 if (response.config.responseType === 'blob') { return response } const body = response.data if (body.code !== 200) { // 防御性分支:后端非 2xx 走 reject 拦截器,这里是 2xx 但业务码异常的情况 ElMessage.error(body.message || '请求失败') return Promise.reject(new Error(body.message || '请求失败')) } return response }, async (error: AxiosError) => { const status = error.response?.status const raw = error.response?.data let message = (raw as ApiResult | undefined)?.message ?? '网络异常,请稍后重试' // blob 请求失败时 data 是 Blob(如导出接口返回的 JSON 信封错误): // 先解析出真实文案再统一提示 —— 先弹兜底再异步补弹会造成双提示, // 401 时跳转提示也会与实际原因错位 if (raw instanceof Blob) { message = '导出失败,请稍后重试' try { const body = JSON.parse(await raw.text()) as { message?: string } if (body.message) message = body.message } catch { /* 非 JSON 响应,保持兜底提示 */ } } if (status === 401) { // 401 严格表示「受保护接口的鉴权失败」。登录接口自身失败返回 400/403, // 因此不会出现「用户输错一次密码就被弹回登录页」的循环。 handleUnauthorized(message) } else { ElMessage.error(message) } return Promise.reject(error) }, ) /** * 发起请求并按统一信封解包。 * * 成功时直接返回 `data` 字段,调用方不必层层 `.data`; * 失败时已由拦截器提示并 reject,调用方通常只需 try/catch 做状态回滚。 */ export async function request(config: AxiosRequestConfig): Promise { const response = await service.request>(config) return response.data.data } export default service