feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

298
web/src/utils/request.ts Normal file
View File

@@ -0,0 +1,298 @@
import axios from 'axios';
import type { AxiosRequestConfig } from 'axios';
import { clearAuthData } from './auth';
import { message } from 'antd';
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
import i18n from '@/i18n'
export interface ResponseData {
code: number;
msg: string;
data: data | Record<string, string | number | boolean | object | null | undefined>[] | object | any[];
error: string;
time: number;
}
interface data {
"items": Record<string, string | number | boolean | object | null | undefined>[];
"page": {
"page": number;
"pagesize": number;
"total": number;
"hasnext": boolean;
}
}
// 创建axios实例
const service = axios.create({
baseURL: '/api', // 与vite.config.ts中的代理配置对应
// timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json'
},
});
// 是否正在刷新token
let isRefreshing = false;
// 存储待重试的请求队列
interface RequestQueueItem {
config: AxiosRequestConfig;
resolve: (token: string) => void;
reject: (error: Error) => void;
}
let requests: RequestQueueItem[] = [];
// 请求拦截器
service.interceptors.request.use(
(config) => {
if (!config.headers.Authorization) {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => {
// 对请求错误做些什么
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 刷新token的函数
const tokenRefresh = async (): Promise<string> => {
try {
const refresh_token = localStorage.getItem('refresh_token');
if (window.location.hash.includes('#/invite-register')) {
throw new Error(i18n.t('common.refreshTokenNotExist'));
}
if (!refresh_token) {
throw new Error(i18n.t('common.refreshTokenNotExist'));
}
// 使用原生axios调用refresh接口避免触发拦截器导致的循环调用
const response: any = await refreshToken();
const newToken = response.access_token;
localStorage.setItem('token', newToken);
return newToken;
} catch (error) {
// 如果refresh接口也返回401则退出登录
clearAuthData();
message.warning(i18n.t('common.loginExpired'));
// 这里可以添加重定向到登录页的逻辑
if (!window.location.hash.includes('#/login')) {
window.location.href = `/#/login`;
}
throw error;
}
};
// 响应拦截器
service.interceptors.response.use(
(response) => {
// 对响应数据做点什么
const { data: responseData } = response;
// 如果响应数据不是对象,直接返回
if (!responseData || typeof responseData !== 'object') {
return responseData;
}
const { data, code } = responseData;
switch (code) {
case 0:
case 200:
return data !== undefined ? data : responseData;
case 401:
// 处理未授权情况
return handle401Error(response.config);
default:
if (code === undefined) {
return responseData;
}
if (responseData.error || responseData.msg) {
message.warning(responseData.error || responseData.msg)
}
return Promise.reject(responseData);
}
},
(error) => {
// 处理网络错误、超时等
let msg = error.response?.data?.error || error.response?.error;
const status = error?.response ? error.response.status : error;
// 服务器响应了但状态码不在2xx范围
switch (status) {
case 401:
// 处理未授权情况
return handle401Error(error.config);
case 403:
msg = i18n.t('common.permissionDenied');
break;
case 404:
msg = i18n.t('common.apiNotFound');
break;
case 429:
msg = i18n.t('common.tooManyRequests');
break;
case 500:
case 502:
msg = msg || i18n.t('common.serviceUpgrading');
break;
case 504:
msg = msg || i18n.t('common.serverError');
break;
default:
if (!msg && Array.isArray(error.response?.data?.detail)) {
msg = error.response?.data?.detail?.map(item => item.msg).join(';')
} else {
msg = msg || i18n.t('common.unknownError');
}
break;
}
message.warning(msg);
return Promise.reject(error);
}
);
// 处理401错误的函数
const handle401Error = async (config: AxiosRequestConfig): Promise<unknown> => {
// 如果是refresh接口本身返回401则直接退出登录
if (config.url === refreshTokenUrl) {
clearAuthData();
message.warning(i18n.t('common.loginExpired'));
return Promise.reject(new Error(i18n.t('common.loginExpired')));
}
if (config.url === loginUrl) {
return Promise.reject(new Error(i18n.t('common.loginApiCannotRefreshToken')));
}
if (config.url === logoutUrl) {
window.location.href = `/#/login`;
return Promise.reject(new Error(i18n.t('common.logoutApiCannotRefreshToken')));
}
if (config.url?.includes('/public')) {
return Promise.reject(new Error(i18n.t('common.publicApiCannotRefreshToken')));
}
// 如果正在刷新token则将当前请求加入队列
if (isRefreshing) {
return new Promise((resolve, reject) => {
requests.push({ config, resolve, reject });
}).then((token) => {
// 使用新token重新发送请求
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
return service(config);
});
}
// 开始刷新token
isRefreshing = true;
try {
const newToken = await tokenRefresh();
// 更新队列中所有请求的token并重新发送
requests.forEach(({ config, resolve }) => {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${newToken}`;
resolve(newToken);
});
// 清空队列
requests = [];
// 使用新token重新发送当前请求
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${newToken}`;
return service(config);
} catch (error) {
// 刷新token失败清空队列并拒绝所有请求
requests.forEach(({ reject }) => {
reject(error as Error);
});
requests = [];
return Promise.reject(error);
} finally {
isRefreshing = false;
}
};
interface ObjectWithPush {
_push?: boolean;
[key: string]: string | number | boolean | object | null | undefined;
}
function paramFilter(params: Record<string, string | number | boolean | ObjectWithPush | null | undefined> = {}) {
Object.keys(params).forEach(key => {
const val = params[key];
if (val && typeof(val) === 'object'){
const objVal = val as ObjectWithPush;
if(objVal._push){
delete objVal._push;
}else{
delete params[key];
}
} else if(val || val === 0 || val === false){
if(typeof(val) === 'string'){
params[key] = val.trim();
}
}else{
delete params[key];
}
});
return params;
}
// 封装请求方法
export const request = {
get<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return service.get(url, {
params: paramFilter(data as Record<string, string | number | boolean | ObjectWithPush | null | undefined>),
...config || {}
});
},
post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return service.post(url, data, config);
},
put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return service.put(url, data, config);
},
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return service.delete(url, config);
},
patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
return service.patch(url, data, config);
},
uploadFile<T>(url: string, formData?: unknown, config?: AxiosRequestConfig): Promise<T> {
return service.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
...config
});
},
downloadFile(url: string, fileName: string, data?: unknown) {
service.post(url, data, {
responseType: "blob",
})
.then(res =>{
const link = document.createElement("a");
const blob = new Blob([res.data], { type: "application/vnd.ms-excel" });
link.style.display = "none";
link.href = URL.createObjectURL(blob);
link.setAttribute("download", decodeURI(res.headers['filename'] || fileName));
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
};
export default service;