deploy-ease-platform/frontend/src/utils/request.ts
2025-12-06 17:01:42 +08:00

211 lines
6.4 KiB
TypeScript
Raw 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, {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;