可用版本

This commit is contained in:
戚辰先生 2024-11-30 21:45:29 +08:00
parent a7bc39c70f
commit 582b7669af
6 changed files with 220 additions and 437 deletions

View File

@ -1,62 +1,104 @@
import {useState, useCallback, useEffect} from 'react';
import {message, Modal} from 'antd';
import type {UseTableDataProps, TableState} from '@/types/table';
import {createInitialState, handleTableError} from '@/utils/table';
import {Modal} from 'antd';
import type {TableState} from '@/types/table';
import {createInitialState} from '@/utils/table';
import request from '@/utils/request';
import type {Page} from '@/types/base/page';
export interface TableConfig<T> {
message?: {
createSuccess?: string;
updateSuccess?: string;
deleteSuccess?: string;
loadError?: string;
};
// ... 其他配置
// 修改TableService接口
export interface TableService<T, P = any> {
baseUrl: string; // 基础URL
}
export function useTableData<T extends { id: number }, P = any>({
service,
defaultParams,
config = {}
}: UseTableDataProps<T, P>) {
const {
message: messageConfig = {},
defaultPageSize = 10,
selection = false,
onDataChange
} = config;
service,
defaultParams,
config = {}
}: {
service: TableService<T, P>;
defaultParams?: Partial<P>;
config?: any;
}) {
const [state, setState] = useState<TableState<T>>(() =>
createInitialState<T>(defaultPageSize)
createInitialState(config.defaultPageSize || 10)
);
// 加载数据
const loadData = useCallback(async (params?: Partial<P>) => {
setState(prev => ({...prev, loading: true}));
setState(prev => ({ ...prev, loading: true }));
try {
const pageData = await service.list({
...defaultParams,
...params,
pageNum: state.pagination.current,
pageSize: state.pagination.pageSize
} as P);
const pageData = await request.get<Page<T>>(`${service.baseUrl}/page`, {
params: {
...defaultParams,
...params,
pageNum: state.pagination.current,
pageSize: state.pagination.pageSize
},
transform: true
});
const newList = pageData?.content || [];
setState(prev => ({
...prev,
list: newList,
list: pageData?.content || [],
pagination: {
...prev.pagination,
total: pageData?.totalElements || 0
},
loading: false
}));
onDataChange?.(newList);
} catch (error) {
setState(prev => ({...prev, loading: false}));
message.error(messageConfig.loadError || '加载数据失败');
setState(prev => ({ ...prev, loading: false }));
}
}, [service, defaultParams, state.pagination, onDataChange]);
}, [service, defaultParams, state.pagination]);
// CRUD操作
const handleCreate = useCallback(async (data: Partial<T>) => {
try {
await request.post<T>(service.baseUrl, data, {
successMessage: '创建成功'
});
loadData();
return true;
} catch (error) {
return false;
}
}, [service, loadData]);
const handleUpdate = useCallback(async (id: number, data: Partial<T>) => {
try {
await request.put<T>(`${service.baseUrl}/${id}`, data, {
successMessage: '更新成功'
});
loadData();
return true;
} catch (error) {
return false;
}
}, [service, loadData]);
const handleDelete = useCallback(async (id: number) => {
return new Promise((resolve) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该记录吗?',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await request.delete(`${service.baseUrl}/${id}`, {
successMessage: '删除成功'
});
loadData();
resolve(true);
} catch (error) {
resolve(false);
}
},
onCancel: () => resolve(false)
});
});
}, [service, loadData]);
// 分页变化
const onPageChange = useCallback((page: number, pageSize?: number) => {
@ -70,66 +112,15 @@ export function useTableData<T extends { id: number }, P = any>({
}));
}, []);
// CRUD操作
const handleCreate = useCallback(async (data: Partial<T>) => {
if (!service.create) return false;
try {
await service.create(data);
message.success(messageConfig.createSuccess || '创建成功');
loadData();
return true;
} catch (error) {
return handleTableError(error, '创建失败');
}
}, [service, loadData]);
const handleUpdate = useCallback(async (id: number, data: Partial<T>) => {
if (!service.update) return false;
try {
await service.update(id, data);
message.success(messageConfig.updateSuccess || '更新成功');
loadData();
return true;
} catch (error) {
return handleTableError(error, '更新失败');
}
}, [service, loadData]);
const handleDelete = useCallback(async (id: number) => {
if (!service.delete) return false;
return new Promise((resolve) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该记录吗?',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await service.delete(id);
message.success(messageConfig.deleteSuccess || '删除成功');
loadData();
resolve(true);
} catch (error) {
resolve(false);
}
},
onCancel: () => {
resolve(false);
}
});
});
}, [service, loadData]);
// 选择行
const onSelectChange = useCallback((selectedRowKeys: React.Key[], selectedRows: T[]) => {
if (!selection) return;
if (!config.selection) return;
setState(prev => ({
...prev,
selectedRowKeys,
selectedRows
}));
}, [selection]);
}, [config.selection]);
// 监听分页变化自动加载数据
useEffect(() => {
@ -144,7 +135,6 @@ export function useTableData<T extends { id: number }, P = any>({
handleUpdate,
handleDelete,
onSelectChange,
// 提供重置方法
reset: () => setState(createInitialState<T>(defaultPageSize))
reset: () => setState(createInitialState(config.defaultPageSize || 10))
};
}

View File

@ -4,37 +4,34 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined } fr
import * as AntdIcons from '@ant-design/icons';
import { getMenuTree, createMenu, updateMenu, deleteMenu } from './service';
import IconSelect from '@/components/IconSelect';
import { useTableData } from '@/hooks/useTableData';
const MenuPage: React.FC = () => {
const [menus, setMenus] = useState<MenuDTO[]>([]);
const {
list: menus,
pagination,
loading,
loadData: fetchMenus,
onPageChange,
handleCreate,
handleUpdate,
handleDelete
} = useTableData({
service: {
baseUrl: '/api/v1/menu'
},
defaultParams: {
sortField: 'sort',
sortOrder: 'asc'
}
});
const [menuTreeData, setMenuTreeData] = useState<MenuDTO[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [iconSelectVisible, setIconSelectVisible] = useState(false);
const [editingMenu, setEditingMenu] = useState<MenuDTO | null>(null);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const fetchData = async () => {
try {
setLoading(true);
const [menuList, treeData] = await Promise.all([
getMenuTree(),
getMenuTreeWithoutButtons()
]);
setMenus(menuList);
setMenuTreeData(treeData);
} catch (error) {
console.error('获取菜单列表失败:', error);
message.error('获取菜单列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const getIcon = (iconName: string | undefined) => {
if (!iconName) return null;
const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
@ -63,22 +60,6 @@ const MenuPage: React.FC = () => {
setModalVisible(true);
};
const handleDelete = async (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个菜单吗?',
onOk: async () => {
try {
await deleteMenu(id);
message.success('删除成功');
fetchData();
} catch (error) {
message.error('删除失败');
}
},
});
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
@ -95,7 +76,7 @@ const MenuPage: React.FC = () => {
message.success('创建成功');
}
setModalVisible(false);
fetchData();
fetchMenus();
} catch (error) {
message.error('操作失败');
}
@ -201,9 +182,10 @@ const MenuPage: React.FC = () => {
columns={columns}
dataSource={menus}
rowKey="id"
pagination={false}
pagination={pagination}
size="middle"
bordered
onChange={onPageChange}
/>
<Modal

View File

@ -1,37 +1,42 @@
import request from '@/utils/request';
import type { Page } from '@/types/base/page';
import type { MenuResponse, MenuRequest, MenuQuery } from './types';
export const getMenus = async (params?: MenuQuery) => {
return request.get('/api/v1/menu', {
params,
errorMessage: '获取菜单列表失败,请刷新重试'
});
};
const BASE_URL = '/api/v1/menu';
export const createMenu = async (data: MenuRequest) => {
return request.post('/api/v1/menu', data, {
errorMessage: '创建菜单失败,请稍后重试'
});
};
// 获取菜单列表(分页)
export const getMenus = (params?: MenuQuery) =>
request.get<Page<MenuResponse>>(`${BASE_URL}/page`, {
params,
transform: true
});
export const updateMenu = async (id: number, data: MenuRequest) => {
return request.put(`/api/v1/menu/${id}`, data, {
errorMessage: '更新菜单失败,请稍后重试'
});
};
// 获取菜单树
export const getMenuTree = () =>
request.get<MenuResponse[]>(`${BASE_URL}/tree`, {
transform: true
});
export const deleteMenu = async (id: number) => {
return request.delete(`/api/v1/menu/${id}`, {
errorMessage: '删除菜单失败,请稍后重试'
});
};
// 获取当前用户菜单
export const getCurrentUserMenus = () =>
request.get<MenuResponse[]>(`${BASE_URL}/current`, {
transform: true
});
export const getMenuTree = async () => {
return request.get('/api/v1/menu/tree', {
errorMessage: '获取菜单树失败,请刷新重试'
});
};
// 创建菜单
export const createMenu = (data: MenuRequest) =>
request.post<MenuResponse>(BASE_URL, data, {
successMessage: '创建菜单成功'
});
export const getCurrentUserMenus = async (): Promise<MenuResponse[]> => {
return request.get('/api/v1/menu/current');
};
// 更新菜单
export const updateMenu = (id: number, data: MenuRequest) =>
request.put<MenuResponse>(`${BASE_URL}/${id}`, data, {
successMessage: '更新菜单成功'
});
// 删除菜单
export const deleteMenu = (id: number) =>
request.delete(`${BASE_URL}/${id}`, {
successMessage: '删除菜单成功'
});

View File

@ -20,20 +20,11 @@ const UserPage: React.FC = () => {
handleDelete
} = useTableData({
service: {
list: getUsers,
create: createUser,
update: updateUser,
delete: deleteUser
baseUrl: '/api/v1/user'
},
defaultParams: {
sortField: 'createTime',
sortOrder: 'desc'
},
message: {
createSuccess: '用户创建成功',
updateSuccess: '用户更新成功',
deleteSuccess: '用户删除成功',
loadError: '获取用户列表失败'
}
});
@ -72,39 +63,29 @@ const UserPage: React.FC = () => {
const values = await passwordForm.validateFields();
if (editingUser) {
await resetPassword(editingUser.id, values.password);
message.success('密码重置成功');
setResetPasswordModalVisible(false);
}
} catch (error) {
message.error('密码重置失败');
console.error('密码重置失败:', error);
}
};
const handleSubmit = async () => {
const values = await form.validateFields()
.catch(() => {
// 表单验证失败,直接返回
return Promise.reject();
});
const request = editingUser
? updateUser(editingUser.id, {
...values,
version: editingUser.version
})
: createUser(values);
return request
.then(res => {
message.success(editingUser ? '更新用户成功' : '新增用户成功');
setModalVisible(false);
fetchUsers();
})
.catch(error => {
if (error.code) {
message.error(error.message);
}
});
try {
const values = await form.validateFields();
if (editingUser) {
await handleUpdate(editingUser.id, {
...values,
version: editingUser.version
});
} else {
await handleCreate(values);
}
setModalVisible(false);
fetchUsers();
} catch (error) {
console.error('操作失败:', error);
}
};
const getTreeData = (deps: DepartmentDTO[]): any[] => {

View File

@ -1,43 +1,36 @@
import request from '@/utils/request';
import type {BaseResponse} from '@/types/api';
import type {UserResponse, UserRequest, UserQuery} from './types';
import type {Page} from '@/types/base/page';
import type { Page } from '@/types/base/page';
import type { UserResponse, UserRequest, UserQuery } from './types';
export const getUsers = async (params?: UserQuery) => {
return request.get<Page<UserResponse>>('/api/v1/user/page', {
const BASE_URL = '/api/v1/user';
// 获取用户列表(分页)
export const getUsers = (params?: UserQuery) =>
request.get<Page<UserResponse>>(`${BASE_URL}/page`, {
params,
retryCount: 3,
retryDelay: 1000,
cancelPrevious: true,
transform: true,
customMessage: '获取用户列表失败',
beforeRequest: () => {
console.log('开始请求');
},
afterResponse: () => {
console.log('请求完成');
}
transform: true
});
};
export const getUserList = async (params?: UserQuery) => {
return request.get<BaseResponse<UserResponse[]>>('/api/v1/user/list', {
params
// 创建用户
export const createUser = (data: UserRequest) =>
request.post<UserResponse>(BASE_URL, data, {
successMessage: '创建用户成功'
});
};
export const createUser = async (data: UserRequest): Promise<Response> => {
return request.post('/api/v1/user', data, { hideMessage: true });
};
// 更新用户
export const updateUser = (id: number, data: UserRequest) =>
request.put<UserResponse>(`${BASE_URL}/${id}`, data, {
successMessage: '更新用户成功'
});
export const updateUser = async (id: number, data: UserRequest) => {
return request.put(`/api/v1/user/${id}`, data);
};
// 删除用户
export const deleteUser = (id: number) =>
request.delete(`${BASE_URL}/${id}`, {
successMessage: '删除用户成功'
});
export const deleteUser = async (id: number) => {
return request.delete<void>(`/api/v1/user/${id}`, {});
};
export const resetPassword = async (id: number, password: string) => {
return request.post(`/api/v1/user/${id}/reset-password`, password);
};
// 重置密码
export const resetPassword = (id: number, password: string) =>
request.post(`${BASE_URL}/${id}/reset-password`, password, {
successMessage: '密码重置成功'
});

View File

@ -9,20 +9,22 @@ export interface Response<T = any> {
}
export interface RequestOptions extends AxiosRequestConfig {
hideMessage?: boolean;
customMessage?: string;
successMessage?: 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 defaultConfig: Partial<RequestOptions> = {
transform: true,
retryCount: 3,
retryDelay: 1000,
timeout: 30000
};
// 创建请求实例
const request = axios.create({
baseURL: '',
timeout: 30000,
@ -32,15 +34,23 @@ const request = axios.create({
}
});
// 创建请求配置
const createRequestConfig = (options?: Partial<RequestOptions>): RequestOptions => {
return {
...defaultConfig,
...options
};
};
const responseHandler = (response: AxiosResponse<Response<any>>) => {
const res = response.data;
const config = response.config as RequestOptions;
if (res.success && res.code === 200) {
!config.hideMessage && message.success(config.customMessage || res.message);
config.successMessage && message.success(config.successMessage);
return config.transform ? res.data : res;
} else {
!config.hideMessage && message.error(config.customMessage || res.message);
message.error(config.errorMessage || res.message);
return Promise.reject(res);
}
};
@ -76,7 +86,7 @@ const errorHandler = (error: any) => {
msg = '服务器异常,请稍后再试!';
}
!config.hideMessage && message.error(config.customMessage || msg);
message.error(config.errorMessage || msg);
return Promise.reject(error);
};
@ -94,32 +104,32 @@ request.interceptors.request.use(
request.interceptors.response.use(responseHandler, errorHandler);
const http = {
get: <T = any>(url: string, config?: RequestOptions) =>
request.get<any, Response<T>>(url, { transform: true, ...config }),
get: <T = any>(url: string, config?: RequestOptions) =>
request.get<any, Response<T>>(url, createRequestConfig({transform: true, ...config})),
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
request.post<any, Response<T>>(url, data, { transform: true, ...config }),
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
request.post<any, Response<T>>(url, data, createRequestConfig({transform: true, ...config})),
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
request.put<any, Response<T>>(url, data, { transform: true, ...config }),
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
request.put<any, Response<T>>(url, data, createRequestConfig({transform: true, ...config})),
delete: <T = any>(url: string, config?: RequestOptions) =>
request.delete<any, Response<T>>(url, { transform: true, ...config }),
delete: <T = any>(url: string, config?: RequestOptions) =>
request.delete<any, Response<T>>(url, createRequestConfig({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' },
return request.post<any, Response<T>>(url, formData, createRequestConfig({
headers: {'Content-Type': 'multipart/form-data'},
...config
});
}));
},
download: (url: string, filename?: string, config?: RequestOptions) =>
request.get(url, {
download: (url: string, filename?: string, config?: RequestOptions) =>
request.get(url, createRequestConfig({
responseType: 'blob',
...config
}).then(response => {
...config
})).then(response => {
const blob = new Blob([response.data]);
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
@ -132,182 +142,4 @@ const http = {
})
};
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;