import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig, } from 'axios'; /** 后端统一响应体 */ export interface ApiResult { code: number; msg: string; data: T; path?: string; timestamp?: string; } /** 分页结果 */ export interface Paginated { list: T[]; total: number; page: number; pageSize: number; } const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001/api'; const TOKEN_KEY = process.env.NEXT_PUBLIC_TOKEN_KEY ?? 'admin_token'; /** ---------- Token 存取(仅浏览器端) ---------- */ export const tokenStorage = { get(): string | null { if (typeof window === 'undefined') return null; return window.localStorage.getItem(TOKEN_KEY); }, set(token: string): void { if (typeof window === 'undefined') return; window.localStorage.setItem(TOKEN_KEY, token); }, clear(): void { if (typeof window === 'undefined') return; window.localStorage.removeItem(TOKEN_KEY); }, }; /** ---------- axios 实例 ---------- */ export const http: AxiosInstance = axios.create({ baseURL: API_URL, timeout: 15000, }); // 请求拦截:自动注入 JWT http.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = tokenStorage.get(); if (token) { config.headers.set('Authorization', `Bearer ${token}`); } return config; }); // 响应拦截:拆包 + 401 跳登录 // 注:onFulfilled 在拦截器中返回业务数据(拆包后的 data)而非 AxiosResponse, // 这是项目约定的解包模式;axios 1.x 的类型签名不灵活,必须 cast 为 any http.interceptors.response.use( // eslint-disable-next-line @typescript-eslint/no-explicit-any (response): any => { const body = response.data as ApiResult; if (body && typeof body.code === 'number') { if (body.code === 200) { return body.data; } // 业务错误:抛出,UI 层 try/catch 处理 return Promise.reject(new Error(body.msg || '请求失败')); } return body; }, (error: AxiosError>) => { const status = error.response?.status; const msg = error.response?.data?.msg ?? error.message ?? '网络异常,请稍后重试'; if (status === 401) { tokenStorage.clear(); // 避免循环跳转:仅在 /admin 下跳登录 if (typeof window !== 'undefined') { const path = window.location.pathname; if (path.startsWith('/admin') && path !== '/admin/login') { window.location.href = '/admin/login?expired=1'; } } } return Promise.reject(new Error(msg)); }, ); /** ---------- SWR 通用 fetcher ---------- */ export const fetcher = async (url: string): Promise => { return (await http.get(url)) as T; }; /** ---------- 工具:表单提交包装 ---------- */ export async function submitForm(fn: () => Promise): Promise { return fn(); }