2026-06-22 15:49:58 +08:00

102 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios, {
AxiosError,
AxiosInstance,
InternalAxiosRequestConfig,
} from 'axios';
/** 后端统一响应体 */
export interface ApiResult<T> {
code: number;
msg: string;
data: T;
path?: string;
timestamp?: string;
}
/** 分页结果 */
export interface Paginated<T> {
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<unknown>;
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<ApiResult<unknown>>) => {
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 <T>(url: string): Promise<T> => {
return (await http.get<unknown, T>(url)) as T;
};
/** ---------- 工具:表单提交包装 ---------- */
export async function submitForm<T>(fn: () => Promise<T>): Promise<T> {
return fn();
}