diff --git a/frontend/src/hooks/useTableData.ts b/frontend/src/hooks/useTableData.ts index 7bfc5df7..a9f1610b 100644 --- a/frontend/src/hooks/useTableData.ts +++ b/frontend/src/hooks/useTableData.ts @@ -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 { - message?: { - createSuccess?: string; - updateSuccess?: string; - deleteSuccess?: string; - loadError?: string; - }; - // ... 其他配置 +// 修改TableService接口 +export interface TableService { + baseUrl: string; // 基础URL } export function useTableData({ - service, - defaultParams, - config = {} - }: UseTableDataProps) { - const { - message: messageConfig = {}, - defaultPageSize = 10, - selection = false, - onDataChange - } = config; - + service, + defaultParams, + config = {} +}: { + service: TableService; + defaultParams?: Partial

; + config?: any; +}) { const [state, setState] = useState>(() => - createInitialState(defaultPageSize) + createInitialState(config.defaultPageSize || 10) ); // 加载数据 const loadData = useCallback(async (params?: Partial

) => { - 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>(`${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) => { + try { + await request.post(service.baseUrl, data, { + successMessage: '创建成功' + }); + loadData(); + return true; + } catch (error) { + return false; + } + }, [service, loadData]); + + const handleUpdate = useCallback(async (id: number, data: Partial) => { + try { + await request.put(`${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({ })); }, []); - // CRUD操作 - const handleCreate = useCallback(async (data: Partial) => { - 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) => { - 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({ handleUpdate, handleDelete, onSelectChange, - // 提供重置方法 - reset: () => setState(createInitialState(defaultPageSize)) + reset: () => setState(createInitialState(config.defaultPageSize || 10)) }; } \ No newline at end of file diff --git a/frontend/src/pages/System/Menu/index.tsx b/frontend/src/pages/System/Menu/index.tsx index aff7a468..b05501c3 100644 --- a/frontend/src/pages/System/Menu/index.tsx +++ b/frontend/src/pages/System/Menu/index.tsx @@ -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([]); + 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([]); const [modalVisible, setModalVisible] = useState(false); const [iconSelectVisible, setIconSelectVisible] = useState(false); const [editingMenu, setEditingMenu] = useState(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} /> { - 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>(`${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(`${BASE_URL}/tree`, { + transform: true + }); -export const deleteMenu = async (id: number) => { - return request.delete(`/api/v1/menu/${id}`, { - errorMessage: '删除菜单失败,请稍后重试' - }); -}; +// 获取当前用户菜单 +export const getCurrentUserMenus = () => + request.get(`${BASE_URL}/current`, { + transform: true + }); -export const getMenuTree = async () => { - return request.get('/api/v1/menu/tree', { - errorMessage: '获取菜单树失败,请刷新重试' - }); -}; +// 创建菜单 +export const createMenu = (data: MenuRequest) => + request.post(BASE_URL, data, { + successMessage: '创建菜单成功' + }); -export const getCurrentUserMenus = async (): Promise => { - return request.get('/api/v1/menu/current'); -}; \ No newline at end of file +// 更新菜单 +export const updateMenu = (id: number, data: MenuRequest) => + request.put(`${BASE_URL}/${id}`, data, { + successMessage: '更新菜单成功' + }); + +// 删除菜单 +export const deleteMenu = (id: number) => + request.delete(`${BASE_URL}/${id}`, { + successMessage: '删除菜单成功' + }); \ No newline at end of file diff --git a/frontend/src/pages/System/User/index.tsx b/frontend/src/pages/System/User/index.tsx index ea6282d1..2da90281 100644 --- a/frontend/src/pages/System/User/index.tsx +++ b/frontend/src/pages/System/User/index.tsx @@ -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[] => { diff --git a/frontend/src/pages/System/User/service.ts b/frontend/src/pages/System/User/service.ts index 89b56d64..60854b7b 100644 --- a/frontend/src/pages/System/User/service.ts +++ b/frontend/src/pages/System/User/service.ts @@ -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>('/api/v1/user/page', { +const BASE_URL = '/api/v1/user'; + +// 获取用户列表(分页) +export const getUsers = (params?: UserQuery) => + request.get>(`${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>('/api/v1/user/list', { - params +// 创建用户 +export const createUser = (data: UserRequest) => + request.post(BASE_URL, data, { + successMessage: '创建用户成功' }); -}; -export const createUser = async (data: UserRequest): Promise => { - return request.post('/api/v1/user', data, { hideMessage: true }); -}; +// 更新用户 +export const updateUser = (id: number, data: UserRequest) => + request.put(`${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(`/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: '密码重置成功' + }); diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index d3da738f..de46cbde 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -9,20 +9,22 @@ export interface Response { } 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; - afterResponse?: (response: any) => void | Promise; } +// 默认请求配置 +const defaultConfig: Partial = { + 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 => { + return { + ...defaultConfig, + ...options + }; +}; + const responseHandler = (response: AxiosResponse>) => { 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: (url: string, config?: RequestOptions) => - request.get>(url, { transform: true, ...config }), + get: (url: string, config?: RequestOptions) => + request.get>(url, createRequestConfig({transform: true, ...config})), - post: (url: string, data?: any, config?: RequestOptions) => - request.post>(url, data, { transform: true, ...config }), + post: (url: string, data?: any, config?: RequestOptions) => + request.post>(url, data, createRequestConfig({transform: true, ...config})), - put: (url: string, data?: any, config?: RequestOptions) => - request.put>(url, data, { transform: true, ...config }), + put: (url: string, data?: any, config?: RequestOptions) => + request.put>(url, data, createRequestConfig({transform: true, ...config})), - delete: (url: string, config?: RequestOptions) => - request.delete>(url, { transform: true, ...config }), + delete: (url: string, config?: RequestOptions) => + request.delete>(url, createRequestConfig({transform: true, ...config})), upload: (url: string, file: File, config?: RequestOptions) => { const formData = new FormData(); formData.append('file', file); - return request.post>(url, formData, { - headers: { 'Content-Type': 'multipart/form-data' }, + return request.post>(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(); - - 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 = 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 extends Promise { - cancel: () => void; -} - -const createCancelableRequest = (promise: Promise, controller: AbortController): CancelablePromise => { - const cancelablePromise = promise as CancelablePromise; - cancelablePromise.cancel = () => controller.abort(); - return cancelablePromise; -}; - -const debounceRequest = (fn: () => Promise, delay: number): (() => Promise) => { - 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(); - - 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; - afterResponse?: (response: any) => any; - onError?: (error: any) => any; -} - -interface PriorityRequest { - priority: number; - request: () => Promise; -} - -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; \ No newline at end of file