// REQ-USR-004: 统一 Axios 实例 + Result 拆包 + 错误拦截(docs/04 § 2.3 / § 2.4) import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig, } from 'axios'; /** token 持久化键名(D6,跨 task 引用此常量,不写字面量) */ export const TOKEN_STORAGE_KEY = 'xly_erp_token'; /** 网络 / 超时 / 5xx 无可用业务码时的统一标识 */ export const NETWORK_ERROR_CODE = -1; /** HTTP 未授权状态码(BR10 / D11,被动登录失效统一登出) */ export const HTTP_UNAUTHORIZED = 401; /** * 401 统一登出回调单例(D11)。拦截器内无法使用 React hooks(useNavigate/message), * 故由外壳挂载时通过 registerUnauthorizedHandler 注册回调,拦截器捕获 HTTP 401 时调用之。 */ let onUnauthorized: (() => void) | null = null; /** 注册(或传 null 清除)被动 401 统一登出回调 */ export function registerUnauthorizedHandler(fn: (() => void) | null): void { onUnauthorized = fn; } /** 后端统一响应体 Result(docs/04 § 1.4) */ export interface Result { code: number; message: string; data: T; } /** 业务 / 网络错误,携带后端业务码供页面分流文案 */ export class ApiError extends Error { code: number; constructor(code: number, message: string) { super(message); this.name = 'ApiError'; this.code = code; } } const request: AxiosInstance = axios.create({ baseURL: '/api', timeout: 15000, headers: { 'Content-Type': 'application/json' }, }); // 请求拦截器:已登录态注入 Authorization;登录 / 版本端点无 token 时自然跳过 request.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = localStorage.getItem(TOKEN_STORAGE_KEY); if (token) { config.headers.set('Authorization', `Bearer ${token}`); } return config; }); // 响应拦截器:拆 Result。code=0 取 data;非 0 抛 ApiError;无响应(网络/超时/5xx)兜底 request.interceptors.response.use( (response) => { const body = response.data as Result; if (body && typeof body === 'object' && 'code' in body) { if (body.code === 0) { return body.data; } return Promise.reject(new ApiError(body.code, body.message || '请求失败')); } // 非 Result 结构(理论上不应出现),原样返回 data return response.data; }, (error: AxiosError) => { // 已是 ApiError 直接透传(极少数情况) if (error instanceof ApiError) { return Promise.reject(error); } // 被动 401:触发统一登出回调(若已注册),再走原 ApiError 映射(BR10 / D11) if (error.response?.status === HTTP_UNAUTHORIZED && onUnauthorized) { onUnauthorized(); } const body = error.response?.data as Result | undefined; if (body && typeof body === 'object' && 'code' in body) { return Promise.reject(new ApiError(body.code, body.message || '请求失败')); } // 无响应:网络异常 / 超时 / 5xx 无 Result 体 return Promise.reject(new ApiError(NETWORK_ERROR_CODE, '网络异常,请稍后重试')); }, ); export default request;