99 lines
2.6 KiB
TypeScript
99 lines
2.6 KiB
TypeScript
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 跳登录
|
||
http.interceptors.response.use(
|
||
(response) => {
|
||
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();
|
||
}
|