211 lines
6.4 KiB
TypeScript
211 lines
6.4 KiB
TypeScript
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
|
||
import {toast} from '@/components/ui/use-toast';
|
||
import { API_BASE_URL, isDev } from '@/config/env';
|
||
import { maintenanceDetector } from '@/services/maintenanceDetector';
|
||
import store from '../store';
|
||
import { logout } from '../store/userSlice';
|
||
|
||
export interface Response<T = any> {
|
||
code: number;
|
||
message: string;
|
||
data: T;
|
||
success: boolean;
|
||
}
|
||
|
||
export interface RequestOptions extends AxiosRequestConfig {
|
||
retryCount?: number;
|
||
retryDelay?: number;
|
||
}
|
||
|
||
// 默认请求配置
|
||
const defaultConfig: Partial<RequestOptions> = {
|
||
retryCount: 3,
|
||
retryDelay: 1000,
|
||
timeout: 30000
|
||
};
|
||
|
||
// 创建请求实例
|
||
const request = axios.create({
|
||
baseURL: API_BASE_URL,
|
||
timeout: 30000,
|
||
withCredentials: true,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
// 开发环境打印请求配置
|
||
if (isDev) {
|
||
console.log('📡 API Base URL:', API_BASE_URL || '使用代理 (proxy)');
|
||
}
|
||
|
||
const defaultErrorMessage = '操作失败';
|
||
|
||
// 创建请求配置
|
||
const createRequestConfig = (options?: Partial<RequestOptions>): RequestOptions => {
|
||
return {
|
||
...defaultConfig,
|
||
...options
|
||
};
|
||
};
|
||
|
||
const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
||
// 请求成功,记录到维护检测器
|
||
maintenanceDetector.recordSuccess();
|
||
|
||
const result = response.data;
|
||
if (result.success && result.code === 200) {
|
||
return result.data;
|
||
} else {
|
||
if (result.message != undefined) {
|
||
toast({
|
||
title: '操作失败',
|
||
description: result.message || defaultErrorMessage,
|
||
variant: 'destructive'
|
||
});
|
||
return Promise.reject(response);
|
||
}
|
||
return Promise.reject(response);
|
||
}
|
||
};
|
||
|
||
|
||
const errorHandler = (error: any) => {
|
||
// 检测网络错误(后端完全不可达)
|
||
if (!error.response) {
|
||
// 记录失败到维护检测器
|
||
maintenanceDetector.recordFailure();
|
||
|
||
// 不显示toast,因为会跳转到维护页面
|
||
return Promise.reject(error);
|
||
}
|
||
|
||
if (axios.isCancel(error)) {
|
||
return Promise.reject(error);
|
||
}
|
||
|
||
const status = error.response.status;
|
||
|
||
// 检测严重的服务器错误(500+),可能是服务器故障或维护
|
||
if (status >= 500) {
|
||
maintenanceDetector.recordFailure();
|
||
// 继续执行下面的错误处理,显示具体的错误信息
|
||
}
|
||
let errorMessage = '';
|
||
switch (status) {
|
||
case 401:
|
||
// 登录已过期,清除所有本地存储并跳转到登录页
|
||
errorMessage = '登录已过期,请重新登录';
|
||
toast({
|
||
title: '登录已过期',
|
||
description: errorMessage,
|
||
variant: 'destructive'
|
||
});
|
||
|
||
// 使用统一的 logout action 清除所有用户数据
|
||
store.dispatch(logout());
|
||
|
||
// 延迟跳转,确保提示能显示出来
|
||
setTimeout(() => {
|
||
window.location.href = '/login';
|
||
}, 1000);
|
||
break;
|
||
case 403:
|
||
errorMessage = '拒绝访问';
|
||
toast({
|
||
title: '访问被拒绝',
|
||
description: errorMessage,
|
||
variant: 'destructive'
|
||
});
|
||
break;
|
||
case 404:
|
||
errorMessage = '请求错误,未找到该资源';
|
||
toast({
|
||
title: '请求错误',
|
||
description: errorMessage,
|
||
variant: 'destructive'
|
||
});
|
||
break;
|
||
case 500:
|
||
errorMessage = '服务异常,请稍后再试';
|
||
toast({
|
||
title: '服务器错误',
|
||
description: errorMessage,
|
||
variant: 'destructive'
|
||
});
|
||
break;
|
||
default:
|
||
errorMessage = '服务器异常,请稍后再试!';
|
||
toast({
|
||
title: '请求失败',
|
||
description: errorMessage,
|
||
variant: 'destructive'
|
||
});
|
||
}
|
||
|
||
return Promise.reject(error);
|
||
};
|
||
|
||
request.interceptors.request.use(
|
||
(config) => {
|
||
// 检查是否处于维护模式
|
||
if (maintenanceDetector.isInMaintenanceMode()) {
|
||
// 如果在维护模式且当前不在维护页面,跳转
|
||
if (window.location.pathname !== '/maintenance') {
|
||
window.location.replace('/maintenance');
|
||
}
|
||
// 取消请求,避免继续发送
|
||
return Promise.reject(new Error('System in maintenance mode'));
|
||
}
|
||
|
||
const token = localStorage.getItem('token');
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`;
|
||
}
|
||
return config;
|
||
},
|
||
(error) => Promise.reject(error)
|
||
);
|
||
|
||
request.interceptors.response.use(responseHandler, errorHandler);
|
||
|
||
const http = {
|
||
get: <T = any>(url: string, config?: RequestOptions) =>
|
||
request.get<any, T>(url, createRequestConfig(config)),
|
||
|
||
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||
request.post<any, T>(url, data, createRequestConfig(config)),
|
||
|
||
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||
request.put<any, T>(url, data, createRequestConfig(config)),
|
||
|
||
delete: <T = any>(url: string, config?: RequestOptions) =>
|
||
request.delete<any, T>(url, createRequestConfig(config)),
|
||
|
||
upload: <T = any>(url: string, file: File, config?: RequestOptions) => {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
return request.post<any, T>(url, formData, createRequestConfig({
|
||
headers: {'Content-Type': 'multipart/form-data'},
|
||
...config
|
||
}));
|
||
},
|
||
|
||
download: (url: string, filename?: string, config?: RequestOptions) =>
|
||
request.get(url, createRequestConfig({
|
||
responseType: 'blob',
|
||
...config
|
||
})).then(response => {
|
||
const blob = new Blob([response.data]);
|
||
const downloadUrl = window.URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = downloadUrl;
|
||
link.download = filename || '';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(downloadUrl);
|
||
})
|
||
};
|
||
|
||
export default http;
|