可用版本
This commit is contained in:
parent
445766b458
commit
a7bc39c70f
@ -50,19 +50,19 @@ const BasicLayout: React.FC = () => {
|
||||
// 初始化用户数据
|
||||
useEffect(() => {
|
||||
const initializeUserData = async () => {
|
||||
try {
|
||||
// try {
|
||||
setLoading(true);
|
||||
const userData = await getCurrentUser();
|
||||
dispatch(setUserInfo(userData));
|
||||
const menuData = await getCurrentUserMenus();
|
||||
const menuData = await getCurrentUserMenus()
|
||||
dispatch(setMenus(menuData));
|
||||
} catch (error) {
|
||||
message.error('初始化用户数据失败');
|
||||
dispatch(logout());
|
||||
navigate('/login', {replace: true});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// message.error('初始化用户数据失败');
|
||||
// dispatch(logout());
|
||||
// navigate('/login', {replace: true});
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
};
|
||||
|
||||
if (!userInfo) {
|
||||
@ -137,7 +137,7 @@ const BasicLayout: React.FC = () => {
|
||||
|
||||
// 将菜单数据转换为antd Menu需要的格式
|
||||
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
|
||||
return menuList.map(menu => ({
|
||||
return menuList?.map(menu => ({
|
||||
key: menu.path || menu.id.toString(),
|
||||
icon: getIcon(menu.icon),
|
||||
label: menu.name,
|
||||
|
||||
@ -16,9 +16,7 @@ export const logout = async (): Promise<void> => {
|
||||
};
|
||||
|
||||
export const getCurrentUser = async (): Promise<LoginResponse> => {
|
||||
return request.get('/api/v1/user/current', {
|
||||
errorMessage: '获取用户信息失败,请重新登录'
|
||||
});
|
||||
return request.get('/api/v1/user/current');
|
||||
};
|
||||
|
||||
export const getUserMenus = async (): Promise<MenuResponse[]> => {
|
||||
|
||||
@ -33,7 +33,5 @@ export const getMenuTree = async () => {
|
||||
};
|
||||
|
||||
export const getCurrentUserMenus = async (): Promise<MenuResponse[]> => {
|
||||
return request.get('/api/v1/menu/current', {
|
||||
errorMessage: '获取菜单失败,请刷新页面重试'
|
||||
});
|
||||
return request.get('/api/v1/menu/current');
|
||||
};
|
||||
@ -6,6 +6,7 @@ import type {DepartmentDTO} from '../Department/types';
|
||||
import {getUsers, createUser, updateUser, deleteUser, resetPassword} from './service';
|
||||
import {useTableData} from '@/hooks/useTableData';
|
||||
import type {FixedType, AlignType} from 'rc-table/lib/interface';
|
||||
import {Response} from "@/utils/request.ts";
|
||||
|
||||
const UserPage: React.FC = () => {
|
||||
const {
|
||||
@ -80,23 +81,30 @@ const UserPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (editingUser) {
|
||||
await updateUser(editingUser.id, {
|
||||
const values = await form.validateFields()
|
||||
.catch(() => {
|
||||
// 表单验证失败,直接返回
|
||||
return Promise.reject();
|
||||
});
|
||||
|
||||
const request = editingUser
|
||||
? updateUser(editingUser.id, {
|
||||
...values,
|
||||
version: editingUser.version
|
||||
});
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await createUser(values);
|
||||
message.success('创建成功');
|
||||
}
|
||||
})
|
||||
: createUser(values);
|
||||
|
||||
return request
|
||||
.then(res => {
|
||||
message.success(editingUser ? '更新用户成功' : '新增用户成功');
|
||||
setModalVisible(false);
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.code) {
|
||||
message.error(error.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getTreeData = (deps: DepartmentDTO[]): any[] => {
|
||||
|
||||
@ -6,61 +6,38 @@ import type { Page } from '@/types/base/page';
|
||||
export const getUsers = async (params?: UserQuery) => {
|
||||
return request.get<Page<UserResponse>>('/api/v1/user/page', {
|
||||
params,
|
||||
errorMessage: '获取用户列表失败,请刷新重试'
|
||||
retryCount: 3,
|
||||
retryDelay: 1000,
|
||||
cancelPrevious: true,
|
||||
transform: true,
|
||||
customMessage: '获取用户列表失败',
|
||||
beforeRequest: () => {
|
||||
console.log('开始请求');
|
||||
},
|
||||
afterResponse: () => {
|
||||
console.log('请求完成');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserList = async (params?: UserQuery) => {
|
||||
return request.get<BaseResponse<UserResponse[]>>('/api/v1/user/list', {
|
||||
params,
|
||||
errorMessage: '获取用户列表失败,请刷新重试'
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
export const createUser = async (data: UserRequest) => {
|
||||
return request.post('/api/v1/user', data, {
|
||||
errorMessage: '创建用户失败,请稍后重试'
|
||||
});
|
||||
export const createUser = async (data: UserRequest): Promise<Response> => {
|
||||
return request.post('/api/v1/user', data, { hideMessage: true });
|
||||
};
|
||||
|
||||
export const updateUser = async (id: number, data: UserRequest) => {
|
||||
return request.put(`/api/v1/user/${id}`, data, {
|
||||
errorMessage: '更新用户信息失败,请稍后重试'
|
||||
});
|
||||
return request.put(`/api/v1/user/${id}`, data);
|
||||
};
|
||||
|
||||
export const deleteUser = async (id: number) => {
|
||||
return request.delete<void>(`/api/v1/user/${id}`, {
|
||||
errorMessage: '删除用户失败'
|
||||
});
|
||||
return request.delete<void>(`/api/v1/user/${id}`, {});
|
||||
};
|
||||
|
||||
export const assignRoles = async (userId: number, roleIds: number[]) => {
|
||||
return request.post(`/api/v1/user/${userId}/roles`, roleIds, {
|
||||
errorMessage: '分配角色失败,请稍后重试'
|
||||
});
|
||||
};
|
||||
|
||||
export const resetPassword = async (id: number) => {
|
||||
return request.post(`/api/v1/user/${id}/reset-password`, null, {
|
||||
errorMessage: '重置密码失败,请稍后重试'
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePassword = async (id: number, data: { oldPassword: string; newPassword: string }) => {
|
||||
return request.post(`/api/v1/user/${id}/update-password`, data, {
|
||||
errorMessage: '修改密码失败,请稍后重试'
|
||||
});
|
||||
};
|
||||
|
||||
export const enableUser = async (id: number) => {
|
||||
return request.post(`/api/v1/user/${id}/enable`, null, {
|
||||
errorMessage: '启用用户失败,请稍后重试'
|
||||
});
|
||||
};
|
||||
|
||||
export const disableUser = async (id: number) => {
|
||||
return request.post(`/api/v1/user/${id}/disable`, null, {
|
||||
errorMessage: '禁用用户失败,请稍后重试'
|
||||
});
|
||||
export const resetPassword = async (id: number, password: string) => {
|
||||
return request.post(`/api/v1/user/${id}/reset-password`, password);
|
||||
};
|
||||
16
frontend/src/types/error.ts
Normal file
16
frontend/src/types/error.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface ApiError {
|
||||
code: number;
|
||||
message: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
export class BusinessError extends Error {
|
||||
constructor(
|
||||
public code: number,
|
||||
message: string,
|
||||
public details?: any
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'BusinessError';
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
|
||||
import {message} from 'antd';
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
export interface Response<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
@ -9,85 +9,305 @@ export interface ApiResponse<T = any> {
|
||||
}
|
||||
|
||||
export interface RequestOptions extends AxiosRequestConfig {
|
||||
skipErrorHandler?: boolean;
|
||||
hideErrorMessage?: boolean;
|
||||
errorMessage?: string;
|
||||
hideMessage?: boolean;
|
||||
customMessage?: string;
|
||||
transform?: boolean;
|
||||
retryCount?: number;
|
||||
retryDelay?: number;
|
||||
retryAttempt?: number;
|
||||
cancelPrevious?: boolean;
|
||||
skipQueue?: boolean;
|
||||
onUploadProgress?: (progressEvent: any) => void;
|
||||
onDownloadProgress?: (progressEvent: any) => void;
|
||||
beforeRequest?: (config: AxiosRequestConfig) => void | Promise<void>;
|
||||
afterResponse?: (response: any) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 300000,
|
||||
timeout: 30000,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
const tenantId = localStorage.getItem('tenantId');
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (tenantId) {
|
||||
config.headers['X-Tenant-Id'] = tenantId;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse<any>>) => {
|
||||
const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
||||
const res = response.data;
|
||||
const config = response.config as RequestOptions;
|
||||
|
||||
if (res.success) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
if (!config.hideErrorMessage) {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(res.message));
|
||||
},
|
||||
(error) => {
|
||||
const config = error.config as RequestOptions;
|
||||
const response = error.response;
|
||||
|
||||
if (!config.hideErrorMessage) {
|
||||
if (response?.data?.message && response?.status !== 500) {
|
||||
message.error(response.data.message);
|
||||
if (res.success && res.code === 200) {
|
||||
!config.hideMessage && message.success(config.customMessage || res.message);
|
||||
return config.transform ? res.data : res;
|
||||
} else {
|
||||
message.error(config.errorMessage || '系统错误,请稍后重试');
|
||||
!config.hideMessage && message.error(config.customMessage || res.message);
|
||||
return Promise.reject(res);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const extendedRequest = {
|
||||
get: <T = any>(url: string, config?: RequestOptions) =>
|
||||
request.get<any, T>(url, config),
|
||||
|
||||
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||
request.post<any, T>(url, data, config),
|
||||
|
||||
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||
request.put<any, T>(url, data, config),
|
||||
|
||||
delete: <T = any>(url: string, config?: RequestOptions) =>
|
||||
request.delete<any, T>(url, config),
|
||||
|
||||
request: request
|
||||
};
|
||||
|
||||
export default extendedRequest;
|
||||
const errorHandler = (error: any) => {
|
||||
const config = error.config as RequestOptions;
|
||||
|
||||
if (!error.response) {
|
||||
message.error('网络连接异常,请检查网络');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (axios.isCancel(error)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const status = error.response.status;
|
||||
let msg = '';
|
||||
switch (status) {
|
||||
case 401:
|
||||
msg = '未授权,请重新登录';
|
||||
break;
|
||||
case 403:
|
||||
msg = '拒绝访问';
|
||||
break;
|
||||
case 404:
|
||||
msg = '请求错误,未找到该资源';
|
||||
break;
|
||||
case 500:
|
||||
msg = '服务器错误';
|
||||
break;
|
||||
default:
|
||||
msg = '服务器异常,请稍后再试!';
|
||||
}
|
||||
|
||||
!config.hideMessage && message.error(config.customMessage || msg);
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
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, Response<T>>(url, { transform: true, ...config }),
|
||||
|
||||
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||
request.post<any, Response<T>>(url, data, { transform: true, ...config }),
|
||||
|
||||
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||
request.put<any, Response<T>>(url, data, { transform: true, ...config }),
|
||||
|
||||
delete: <T = any>(url: string, config?: RequestOptions) =>
|
||||
request.delete<any, Response<T>>(url, { transform: true, ...config }),
|
||||
|
||||
upload: <T = any>(url: string, file: File, config?: RequestOptions) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return request.post<any, Response<T>>(url, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
...config
|
||||
});
|
||||
},
|
||||
|
||||
download: (url: string, filename?: string, config?: RequestOptions) =>
|
||||
request.get(url, {
|
||||
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 class RequestCancel {
|
||||
private static pendingMap = new Map<string, AbortController>();
|
||||
|
||||
static add(config: AxiosRequestConfig) {
|
||||
const url = [config.method, config.url].join('&');
|
||||
this.remove(url);
|
||||
const controller = new AbortController();
|
||||
config.signal = controller.signal;
|
||||
this.pendingMap.set(url, controller);
|
||||
}
|
||||
|
||||
static remove(url: string) {
|
||||
const controller = this.pendingMap.get(url);
|
||||
controller?.abort();
|
||||
this.pendingMap.delete(url);
|
||||
}
|
||||
|
||||
static removeAll() {
|
||||
this.pendingMap.forEach(controller => controller.abort());
|
||||
this.pendingMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const retryRequest = async (error: any) => {
|
||||
const config = error.config as RequestOptions;
|
||||
if (!config || !config.retryCount) return Promise.reject(error);
|
||||
|
||||
config.__retryCount = config.__retryCount || 0;
|
||||
if (config.__retryCount >= config.retryCount) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
config.__retryCount += 1;
|
||||
await new Promise(resolve => setTimeout(resolve, config.retryDelay || 1000));
|
||||
return request(config);
|
||||
};
|
||||
|
||||
const logRequest = (config: AxiosRequestConfig) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`, {
|
||||
data: config.data,
|
||||
params: config.params
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const logResponse = (response: AxiosResponse) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[Response] ${response.config.url}`, response.data);
|
||||
}
|
||||
};
|
||||
|
||||
export enum ErrorType {
|
||||
Timeout = 'TIMEOUT',
|
||||
Network = 'NETWORK',
|
||||
Business = 'BUSINESS',
|
||||
Auth = 'AUTH',
|
||||
Server = 'SERVER'
|
||||
}
|
||||
|
||||
export interface RequestError extends Error {
|
||||
type: ErrorType;
|
||||
code?: number;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
const createRequestError = (type: ErrorType, message: string, data?: any): RequestError => {
|
||||
const error = new Error(message) as RequestError;
|
||||
error.type = type;
|
||||
error.data = data;
|
||||
return error;
|
||||
};
|
||||
|
||||
class RequestQueue {
|
||||
private queue: Set<string> = new Set();
|
||||
|
||||
add(config: AxiosRequestConfig) {
|
||||
const url = [config.method, config.url].join('&');
|
||||
this.queue.add(url);
|
||||
}
|
||||
|
||||
remove(config: AxiosRequestConfig) {
|
||||
const url = [config.method, config.url].join('&');
|
||||
this.queue.delete(url);
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.queue.size;
|
||||
}
|
||||
}
|
||||
|
||||
const requestQueue = new RequestQueue();
|
||||
|
||||
export interface CancelablePromise<T> extends Promise<T> {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
const createCancelableRequest = <T>(promise: Promise<T>, controller: AbortController): CancelablePromise<T> => {
|
||||
const cancelablePromise = promise as CancelablePromise<T>;
|
||||
cancelablePromise.cancel = () => controller.abort();
|
||||
return cancelablePromise;
|
||||
};
|
||||
|
||||
const debounceRequest = <T>(fn: () => Promise<T>, delay: number): (() => Promise<T>) => {
|
||||
let timer: NodeJS.Timeout;
|
||||
return () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fn().then(resolve).catch(reject);
|
||||
}, delay);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
class RequestCache {
|
||||
private cache = new Map<string, {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
}>();
|
||||
|
||||
set(key: string, data: any, ttl: number = 5000) {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl
|
||||
});
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
const cached = this.cache.get(key);
|
||||
if (!cached) return null;
|
||||
if (Date.now() - cached.timestamp > cached.ttl) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
interface RequestInterceptors {
|
||||
beforeRequest?: (config: RequestOptions) => RequestOptions | Promise<RequestOptions>;
|
||||
afterResponse?: (response: any) => any;
|
||||
onError?: (error: any) => any;
|
||||
}
|
||||
|
||||
interface PriorityRequest {
|
||||
priority: number;
|
||||
request: () => Promise<any>;
|
||||
}
|
||||
|
||||
class PriorityQueue {
|
||||
private queue: PriorityRequest[] = [];
|
||||
private processing = false;
|
||||
|
||||
add(request: PriorityRequest) {
|
||||
this.queue.push(request);
|
||||
this.queue.sort((a, b) => b.priority - a.priority);
|
||||
if (!this.processing) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
|
||||
private async process() {
|
||||
this.processing = true;
|
||||
while (this.queue.length > 0) {
|
||||
const { request } = this.queue.shift()!;
|
||||
await request();
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default http;
|
||||
Loading…
Reference in New Issue
Block a user