可用版本

This commit is contained in:
戚辰先生 2024-11-30 19:51:44 +08:00
parent 445766b458
commit a7bc39c70f
8 changed files with 495 additions and 278 deletions

View File

@ -1,7 +1,7 @@
import { useState, useCallback, useEffect } from 'react'; import {useState, useCallback, useEffect} from 'react';
import { message, Modal } from 'antd'; import {message, Modal} from 'antd';
import type { UseTableDataProps, TableState } from '@/types/table'; import type {UseTableDataProps, TableState} from '@/types/table';
import { createInitialState, handleTableError } from '@/utils/table'; import {createInitialState, handleTableError} from '@/utils/table';
export interface TableConfig<T> { export interface TableConfig<T> {
message?: { message?: {
@ -17,7 +17,7 @@ export function useTableData<T extends { id: number }, P = any>({
service, service,
defaultParams, defaultParams,
config = {} config = {}
}: UseTableDataProps<T, P>) { }: UseTableDataProps<T, P>) {
const { const {
message: messageConfig = {}, message: messageConfig = {},
defaultPageSize = 10, defaultPageSize = 10,
@ -31,7 +31,7 @@ export function useTableData<T extends { id: number }, P = any>({
// 加载数据 // 加载数据
const loadData = useCallback(async (params?: Partial<P>) => { const loadData = useCallback(async (params?: Partial<P>) => {
setState(prev => ({ ...prev, loading: true })); setState(prev => ({...prev, loading: true}));
try { try {
const pageData = await service.list({ const pageData = await service.list({
...defaultParams, ...defaultParams,
@ -53,7 +53,7 @@ export function useTableData<T extends { id: number }, P = any>({
onDataChange?.(newList); onDataChange?.(newList);
} catch (error) { } catch (error) {
setState(prev => ({ ...prev, loading: false })); setState(prev => ({...prev, loading: false}));
message.error(messageConfig.loadError || '加载数据失败'); message.error(messageConfig.loadError || '加载数据失败');
} }
}, [service, defaultParams, state.pagination, onDataChange]); }, [service, defaultParams, state.pagination, onDataChange]);

View File

@ -50,19 +50,19 @@ const BasicLayout: React.FC = () => {
// 初始化用户数据 // 初始化用户数据
useEffect(() => { useEffect(() => {
const initializeUserData = async () => { const initializeUserData = async () => {
try { // try {
setLoading(true); setLoading(true);
const userData = await getCurrentUser(); const menuData = await getCurrentUserMenus()
dispatch(setUserInfo(userData));
const menuData = await getCurrentUserMenus();
dispatch(setMenus(menuData)); dispatch(setMenus(menuData));
} catch (error) {
message.error('初始化用户数据失败');
dispatch(logout());
navigate('/login', {replace: true});
} finally {
setLoading(false); setLoading(false);
} // } catch (error) {
// console.log(error);
// message.error('初始化用户数据失败');
// dispatch(logout());
// navigate('/login', {replace: true});
// } finally {
// setLoading(false);
// }
}; };
if (!userInfo) { if (!userInfo) {
@ -137,7 +137,7 @@ const BasicLayout: React.FC = () => {
// 将菜单数据转换为antd Menu需要的格式 // 将菜单数据转换为antd Menu需要的格式
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => { const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
return menuList.map(menu => ({ return menuList?.map(menu => ({
key: menu.path || menu.id.toString(), key: menu.path || menu.id.toString(),
icon: getIcon(menu.icon), icon: getIcon(menu.icon),
label: menu.name, label: menu.name,

View File

@ -16,9 +16,7 @@ export const logout = async (): Promise<void> => {
}; };
export const getCurrentUser = async (): Promise<LoginResponse> => { export const getCurrentUser = async (): Promise<LoginResponse> => {
return request.get('/api/v1/user/current', { return request.get('/api/v1/user/current');
errorMessage: '获取用户信息失败,请重新登录'
});
}; };
export const getUserMenus = async (): Promise<MenuResponse[]> => { export const getUserMenus = async (): Promise<MenuResponse[]> => {

View File

@ -33,7 +33,5 @@ export const getMenuTree = async () => {
}; };
export const getCurrentUserMenus = async (): Promise<MenuResponse[]> => { export const getCurrentUserMenus = async (): Promise<MenuResponse[]> => {
return request.get('/api/v1/menu/current', { return request.get('/api/v1/menu/current');
errorMessage: '获取菜单失败,请刷新页面重试'
});
}; };

View File

@ -6,6 +6,7 @@ import type {DepartmentDTO} from '../Department/types';
import {getUsers, createUser, updateUser, deleteUser, resetPassword} from './service'; import {getUsers, createUser, updateUser, deleteUser, resetPassword} from './service';
import {useTableData} from '@/hooks/useTableData'; import {useTableData} from '@/hooks/useTableData';
import type {FixedType, AlignType} from 'rc-table/lib/interface'; import type {FixedType, AlignType} from 'rc-table/lib/interface';
import {Response} from "@/utils/request.ts";
const UserPage: React.FC = () => { const UserPage: React.FC = () => {
const { const {
@ -80,23 +81,30 @@ const UserPage: React.FC = () => {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try { const values = await form.validateFields()
const values = await form.validateFields(); .catch(() => {
if (editingUser) { // 表单验证失败,直接返回
await updateUser(editingUser.id, { return Promise.reject();
});
const request = editingUser
? updateUser(editingUser.id, {
...values, ...values,
version: editingUser.version version: editingUser.version
}); })
message.success('更新成功'); : createUser(values);
} else {
await createUser(values); return request
message.success('创建成功'); .then(res => {
} message.success(editingUser ? '更新用户成功' : '新增用户成功');
setModalVisible(false); setModalVisible(false);
fetchUsers(); fetchUsers();
} catch (error) { })
message.error('操作失败'); .catch(error => {
if (error.code) {
message.error(error.message);
} }
});
}; };
const getTreeData = (deps: DepartmentDTO[]): any[] => { const getTreeData = (deps: DepartmentDTO[]): any[] => {

View File

@ -1,66 +1,43 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { BaseResponse } from '@/types/api'; import type {BaseResponse} from '@/types/api';
import type { UserResponse, UserRequest, UserQuery } from './types'; import type {UserResponse, UserRequest, UserQuery} from './types';
import type { Page } from '@/types/base/page'; import type {Page} from '@/types/base/page';
export const getUsers = async (params?: UserQuery) => { export const getUsers = async (params?: UserQuery) => {
return request.get<Page<UserResponse>>('/api/v1/user/page', { return request.get<Page<UserResponse>>('/api/v1/user/page', {
params, params,
errorMessage: '获取用户列表失败,请刷新重试' retryCount: 3,
retryDelay: 1000,
cancelPrevious: true,
transform: true,
customMessage: '获取用户列表失败',
beforeRequest: () => {
console.log('开始请求');
},
afterResponse: () => {
console.log('请求完成');
}
}); });
}; };
export const getUserList = async (params?: UserQuery) => { export const getUserList = async (params?: UserQuery) => {
return request.get<BaseResponse<UserResponse[]>>('/api/v1/user/list', { return request.get<BaseResponse<UserResponse[]>>('/api/v1/user/list', {
params, params
errorMessage: '获取用户列表失败,请刷新重试'
}); });
}; };
export const createUser = async (data: UserRequest) => { export const createUser = async (data: UserRequest): Promise<Response> => {
return request.post('/api/v1/user', data, { return request.post('/api/v1/user', data, { hideMessage: true });
errorMessage: '创建用户失败,请稍后重试'
});
}; };
export const updateUser = async (id: number, data: UserRequest) => { export const updateUser = async (id: number, data: UserRequest) => {
return request.put(`/api/v1/user/${id}`, data, { return request.put(`/api/v1/user/${id}`, data);
errorMessage: '更新用户信息失败,请稍后重试'
});
}; };
export const deleteUser = async (id: number) => { export const deleteUser = async (id: number) => {
return request.delete<void>(`/api/v1/user/${id}`, { return request.delete<void>(`/api/v1/user/${id}`, {});
errorMessage: '删除用户失败'
});
}; };
export const assignRoles = async (userId: number, roleIds: number[]) => { export const resetPassword = async (id: number, password: string) => {
return request.post(`/api/v1/user/${userId}/roles`, roleIds, { return request.post(`/api/v1/user/${id}/reset-password`, password);
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: '禁用用户失败,请稍后重试'
});
}; };

View 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';
}
}

View File

@ -1,7 +1,7 @@
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'; import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
import {message} from 'antd'; import {message} from 'antd';
export interface ApiResponse<T = any> { export interface Response<T = any> {
code: number; code: number;
message: string; message: string;
data: T; data: T;
@ -9,85 +9,305 @@ export interface ApiResponse<T = any> {
} }
export interface RequestOptions extends AxiosRequestConfig { export interface RequestOptions extends AxiosRequestConfig {
skipErrorHandler?: boolean; hideMessage?: boolean;
hideErrorMessage?: boolean; customMessage?: string;
errorMessage?: 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({ const request = axios.create({
baseURL: '', baseURL: '',
timeout: 300000, timeout: 30000,
withCredentials: true, withCredentials: true,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
request.interceptors.request.use( const responseHandler = (response: AxiosResponse<Response<any>>) => {
(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 res = response.data; const res = response.data;
const config = response.config as RequestOptions; const config = response.config as RequestOptions;
if (res.success) { if (res.success && res.code === 200) {
return res.data; !config.hideMessage && message.success(config.customMessage || res.message);
} return config.transform ? res.data : res;
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);
} else { } 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;