可用版本

This commit is contained in:
戚辰先生 2024-11-30 17:24:12 +08:00
parent ada0b0316c
commit b01d9a630a
13 changed files with 467 additions and 90 deletions

View File

@ -0,0 +1,71 @@
import { useState, useCallback } from 'react';
import type { Page } from '@/types/base/page';
interface UsePageDataProps<T, P = any> {
service: (params: P) => Promise<Page<T>>;
defaultParams?: Partial<P>;
}
interface PageState<T> {
list: T[];
pagination: {
current: number;
pageSize: number;
total: number;
};
loading: boolean;
}
export function usePageData<T, P = any>({ service, defaultParams }: UsePageDataProps<T, P>) {
const [state, setState] = useState<PageState<T>>({
list: [],
pagination: {
current: 1,
pageSize: 10,
total: 0
},
loading: false
});
const loadData = useCallback(async (params?: Partial<P>) => {
setState(prev => ({ ...prev, loading: true }));
try {
const pageData = await service({
...defaultParams,
...params,
pageNum: state.pagination.current,
pageSize: state.pagination.pageSize
} as P);
setState(prev => ({
list: pageData?.content || [],
pagination: {
current: prev.pagination.current,
pageSize: prev.pagination.pageSize,
total: pageData?.totalElements || 0
},
loading: false
}));
} catch (error) {
setState(prev => ({ ...prev, loading: false }));
throw error;
}
}, [service, defaultParams, state.pagination]);
const onPageChange = useCallback((page: number, pageSize?: number) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
current: page,
pageSize: pageSize || prev.pagination.pageSize
}
}));
}, []);
return {
...state,
loadData,
onPageChange
};
}

View File

@ -0,0 +1,150 @@
import { useState, useCallback, useEffect } from 'react';
import { message, Modal } from 'antd';
import type { UseTableDataProps, TableState } from '@/types/table';
import { createInitialState, handleTableError } from '@/utils/table';
export interface TableConfig<T> {
message?: {
createSuccess?: string;
updateSuccess?: string;
deleteSuccess?: string;
loadError?: string;
};
// ... 其他配置
}
export function useTableData<T extends { id: number }, P = any>({
service,
defaultParams,
config = {}
}: UseTableDataProps<T, P>) {
const {
message: messageConfig = {},
defaultPageSize = 10,
selection = false,
onDataChange
} = config;
const [state, setState] = useState<TableState<T>>(() =>
createInitialState<T>(defaultPageSize)
);
// 加载数据
const loadData = useCallback(async (params?: Partial<P>) => {
setState(prev => ({ ...prev, loading: true }));
try {
const pageData = await service.list({
...defaultParams,
...params,
pageNum: state.pagination.current,
pageSize: state.pagination.pageSize
} as P);
const newList = pageData?.content || [];
setState(prev => ({
...prev,
list: newList,
pagination: {
...prev.pagination,
total: pageData?.totalElements || 0
},
loading: false
}));
onDataChange?.(newList);
} catch (error) {
setState(prev => ({ ...prev, loading: false }));
message.error(messageConfig.loadError || '加载数据失败');
}
}, [service, defaultParams, state.pagination, onDataChange]);
// 分页变化
const onPageChange = useCallback((page: number, pageSize?: number) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
current: page,
pageSize: pageSize || prev.pagination.pageSize
}
}));
}, []);
// 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;
setState(prev => ({
...prev,
selectedRowKeys,
selectedRows
}));
}, [selection]);
// 监听分页变化自动加载数据
useEffect(() => {
loadData();
}, [state.pagination.current, state.pagination.pageSize]);
return {
...state,
loadData,
onPageChange,
handleCreate,
handleUpdate,
handleDelete,
onSelectChange,
// 提供重置方法
reset: () => setState(createInitialState<T>(defaultPageSize))
};
}

View File

@ -13,9 +13,10 @@ import {
import * as AntdIcons from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons';
import { logout, setMenus, setUserInfo } from '../store/userSlice'; import { logout, setMenus, setUserInfo } from '../store/userSlice';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { getCurrentUser, getUserMenus } from '../pages/Login/service'; import { getCurrentUser } from '../pages/Login/service';
import { getCurrentUserMenus } from '@/pages/System/Menu/service';
import { getWeather } from '../services/weather'; import { getWeather } from '../services/weather';
import type { MenuDTO } from '../pages/System/Menu/types'; import type { MenuResponse } from '@/pages/System/Menu/types';
import type { RootState } from '../store'; import type { RootState } from '../store';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
@ -63,8 +64,7 @@ const BasicLayout: React.FC = () => {
const userData = await getCurrentUser(); const userData = await getCurrentUser();
dispatch(setUserInfo(userData)); dispatch(setUserInfo(userData));
// 获取并更新用户菜单 const menuData = await getCurrentUserMenus();
const menuData = await getUserMenus();
dispatch(setMenus(menuData)); dispatch(setMenus(menuData));
setIsInitialized(true); setIsInitialized(true);
@ -134,7 +134,7 @@ const BasicLayout: React.FC = () => {
}; };
// 将菜单数据转换为antd Menu需要的格式 // 将菜单数据转换为antd Menu需要的格式
const getMenuItems = (menuList: MenuDTO[]): 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),

View File

@ -2,9 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Table, Button, Modal, Form, Input, Space, message, Switch, Select, TreeSelect, Tooltip, InputNumber } from 'antd'; import { Table, Button, Modal, Form, Input, Space, message, Switch, Select, TreeSelect, Tooltip, InputNumber } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import * as AntdIcons from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons';
import type { MenuDTO } from './types'; import { getMenuTree, createMenu, updateMenu, deleteMenu } from './service';
import { MenuTypeEnum, MenuTypeNames } from './types';
import { getMenuTree, createMenu, updateMenu, deleteMenu, getMenuTreeWithoutButtons } from './service';
import IconSelect from '@/components/IconSelect'; import IconSelect from '@/components/IconSelect';
const MenuPage: React.FC = () => { const MenuPage: React.FC = () => {

View File

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

View File

@ -1,10 +1,12 @@
import { BaseResponse } from '@/types/base/response'; import { BaseResponse } from '@/types/base/response';
export interface MenuResponse extends BaseResponse { export interface MenuResponse extends BaseResponse {
id: number;
name: string; name: string;
path?: string; path?: string;
icon?: string; icon?: string;
parentId?: number; parentId?: number;
sort: number; sort: number;
enabled: boolean;
children?: MenuResponse[]; children?: MenuResponse[];
} }

View File

@ -1,51 +1,53 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect } from 'antd'; import { Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined } from '@ant-design/icons';
import type { UserDTO } from './types'; import type { UserResponse } from './types';
import type { DepartmentDTO } from '../Department/types'; import type { DepartmentDTO } from '../Department/types';
import { getUsers, createUser, updateUser, deleteUser, resetPassword } from './service'; import { getUsers, createUser, updateUser, deleteUser, resetPassword } from './service';
import { getDepartmentTree } from '../Department/service'; import { getDepartmentTree } from '../Department/service';
import RoleModal from './components/RoleModal'; import { convertToPageParams, convertToPageInfo } from '@/utils/page';
import { useTableData } from '@/hooks/useTableData';
// import RoleModal from './components/RoleModal';
const UserPage: React.FC = () => { const UserPage: React.FC = () => {
const [users, setUsers] = useState<UserDTO[]>([]); const {
list: users,
pagination,
loading,
loadData: fetchUsers,
onPageChange,
handleCreate,
handleUpdate,
handleDelete
} = useTableData({
service: {
list: getUsers,
create: createUser,
update: updateUser,
delete: deleteUser
},
defaultParams: {
sortField: 'createTime',
sortOrder: 'desc'
},
message: {
createSuccess: '用户创建成功',
updateSuccess: '用户更新成功',
deleteSuccess: '用户删除成功',
loadError: '获取用户列表失败'
}
});
const [departments, setDepartments] = useState<DepartmentDTO[]>([]); const [departments, setDepartments] = useState<DepartmentDTO[]>([]);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [roleModalVisible, setRoleModalVisible] = useState(false);
const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false); const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false);
const [editingUser, setEditingUser] = useState<UserDTO | null>(null); const [editingUser, setEditingUser] = useState<UserResponse | null>(null);
const [selectedUser, setSelectedUser] = useState<UserDTO | null>(null);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [passwordForm] = Form.useForm(); const [passwordForm] = Form.useForm();
const fetchUsers = async () => {
try {
setLoading(true);
const data = await getUsers({ page: 1, size: 100 });
setUsers(data || []);
} catch (error) {
console.error('获取用户列表失败:', error);
message.error('获取用户列表失败');
} finally {
setLoading(false);
}
};
const fetchDepartments = async () => {
try {
const data = await getDepartmentTree();
setDepartments(data || []);
} catch (error) {
console.error('获取部门列表失败:', error);
message.error('获取部门列表失败');
}
};
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
fetchDepartments(); }, [pagination.current, pagination.pageSize]);
}, []);
const handleAdd = () => { const handleAdd = () => {
setEditingUser(null); setEditingUser(null);
@ -56,32 +58,16 @@ const UserPage: React.FC = () => {
setModalVisible(true); setModalVisible(true);
}; };
const handleEdit = (record: UserDTO) => { const handleEdit = (record: UserResponse) => {
setEditingUser(record); setEditingUser(record);
form.setFieldsValue({ form.setFieldsValue({
...record, ...record,
password: undefined password: 'UNCHANGED_PASSWORD'
}); });
setModalVisible(true); setModalVisible(true);
}; };
const handleDelete = async (id: number) => { const handleResetPassword = (record: UserResponse) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个用户吗?',
onOk: async () => {
try {
await deleteUser(id);
message.success('删除成功');
fetchUsers();
} catch (error) {
message.error('删除失败');
}
},
});
};
const handleResetPassword = (record: UserDTO) => {
setEditingUser(record); setEditingUser(record);
passwordForm.resetFields(); passwordForm.resetFields();
setResetPasswordModalVisible(true); setResetPasswordModalVisible(true);
@ -91,7 +77,7 @@ const UserPage: React.FC = () => {
try { try {
const values = await passwordForm.validateFields(); const values = await passwordForm.validateFields();
if (editingUser) { if (editingUser) {
await resetPassword(editingUser.id, values.password); await resetPassword(editingUser.id);
message.success('密码重置成功'); message.success('密码重置成功');
setResetPasswordModalVisible(false); setResetPasswordModalVisible(false);
} }
@ -106,6 +92,7 @@ const UserPage: React.FC = () => {
if (editingUser) { if (editingUser) {
await updateUser(editingUser.id, { await updateUser(editingUser.id, {
...values, ...values,
password: values.password === 'UNCHANGED_PASSWORD' ? editingUser.password : values.password,
version: editingUser.version version: editingUser.version
}); });
message.success('更新成功'); message.success('更新成功');
@ -128,23 +115,23 @@ const UserPage: React.FC = () => {
})); }));
}; };
const handleAssignRoles = (record: UserDTO) => { // const handleAssignRoles = (record: UserResponse) => {
setSelectedUser(record); // setSelectedUser(record);
setRoleModalVisible(true); // setRoleModalVisible(true);
}; // };
const columns = [ const columns = [
{ {
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'username',
key: 'username', key: 'username',
width: '15%', width: '12%',
}, },
{ {
title: '昵称', title: '昵称',
dataIndex: 'nickname', dataIndex: 'nickname',
key: 'nickname', key: 'nickname',
width: '15%', width: '12%',
}, },
{ {
title: '邮箱', title: '邮箱',
@ -156,19 +143,26 @@ const UserPage: React.FC = () => {
title: '手机号', title: '手机号',
dataIndex: 'phone', dataIndex: 'phone',
key: 'phone', key: 'phone',
width: '15%', width: '12%',
}, },
{ {
title: '所属部门', title: '角色',
dataIndex: 'deptName', dataIndex: 'roles',
key: 'deptName', key: 'roles',
width: '15%',
render: (roles: Role[]) => roles?.map(role => role.name).join(', ') || '-'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: '15%', width: '15%',
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'enabled', dataIndex: 'enabled',
key: 'enabled', key: 'enabled',
width: '10%', width: '8%',
render: (enabled: boolean) => ( render: (enabled: boolean) => (
<Switch checked={enabled} disabled /> <Switch checked={enabled} disabled />
), ),
@ -177,33 +171,35 @@ const UserPage: React.FC = () => {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: '280px', width: '280px',
render: (_: any, record: UserDTO) => ( render: (_: any, record: UserResponse) => (
<Space> <Space>
<Button <Button
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => handleEdit(record)} onClick={() => handleEdit(record)}
> >
</Button> </Button>
<Button {/*
type="link" <Button
type="link"
icon={<TeamOutlined />} icon={<TeamOutlined />}
onClick={() => handleAssignRoles(record)} onClick={() => handleAssignRoles(record)}
disabled={record.username === 'admin'} disabled={record.username === 'admin'}
> >
</Button> </Button>
<Button */}
type="link" <Button
type="link"
icon={<KeyOutlined />} icon={<KeyOutlined />}
onClick={() => handleResetPassword(record)} onClick={() => handleResetPassword(record)}
> >
</Button> </Button>
<Button <Button
type="link" type="link"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)} onClick={() => handleDelete(record.id)}
disabled={record.username === 'admin'} disabled={record.username === 'admin'}
@ -228,7 +224,13 @@ const UserPage: React.FC = () => {
columns={columns} columns={columns}
dataSource={users} dataSource={users}
rowKey="id" rowKey="id"
pagination={false} pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
onChange: onPageChange
}}
size="middle" size="middle"
bordered bordered
/> />
@ -252,6 +254,11 @@ const UserPage: React.FC = () => {
> >
<Input placeholder="请输入用户名" disabled={!!editingUser} /> <Input placeholder="请输入用户名" disabled={!!editingUser} />
</Form.Item> </Form.Item>
{editingUser && (
<Form.Item name="password" hidden>
<Input type="hidden" />
</Form.Item>
)}
{!editingUser && ( {!editingUser && (
<Form.Item <Form.Item
name="password" name="password"
@ -303,6 +310,7 @@ const UserPage: React.FC = () => {
</Form> </Form>
</Modal> </Modal>
{/* RoleModal
{selectedUser && ( {selectedUser && (
<RoleModal <RoleModal
userId={selectedUser.id} userId={selectedUser.id}
@ -318,6 +326,7 @@ const UserPage: React.FC = () => {
}} }}
/> />
)} )}
*/}
<Modal <Modal
title="重置密码" title="重置密码"

View File

@ -1,8 +1,17 @@
import request from '@/utils/request'; import request from '@/utils/request';
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';
export const getUsers = async (params?: UserQuery) => { export const getUsers = async (params?: UserQuery) => {
return request.get('/api/v1/user', { return request.get<Page<UserResponse>>('/api/v1/user/page', {
params,
errorMessage: '获取用户列表失败,请刷新重试'
});
};
export const getUserList = async (params?: UserQuery) => {
return request.get<BaseResponse<UserResponse[]>>('/api/v1/user/list', {
params, params,
errorMessage: '获取用户列表失败,请刷新重试' errorMessage: '获取用户列表失败,请刷新重试'
}); });
@ -21,8 +30,8 @@ export const updateUser = async (id: number, data: UserRequest) => {
}; };
export const deleteUser = async (id: number) => { export const deleteUser = async (id: number) => {
return request.delete(`/api/v1/user/${id}`, { return request.delete<void>(`/api/v1/user/${id}`, {
errorMessage: '删除用户失败,请稍后重试' errorMessage: '删除用户失败'
}); });
}; };

View File

@ -1,12 +1,15 @@
import type { BaseResponse } from '@/types/base/response';
import type { Page } from '@/types/base/page';
export interface UserQuery { export interface UserQuery {
current?: number;
pageSize?: number;
sortField?: string;
sortOrder?: string;
username?: string; username?: string;
nickname?: string; nickname?: string;
email?: string; email?: string;
enabled?: boolean; enabled?: boolean;
pageNum?: number;
pageSize?: number;
sortField?: string;
sortOrder?: string;
} }
export interface UserRequest { export interface UserRequest {
@ -18,13 +21,22 @@ export interface UserRequest {
enabled?: boolean; enabled?: boolean;
} }
export interface UserResponse { // 角色类型定义
export interface Role {
id: number; id: number;
code: string;
name: string;
description?: string;
type: number;
sort: number;
enabled: boolean;
}
export interface UserResponse extends BaseResponse {
username: string; username: string;
nickname?: string; nickname?: string;
email?: string; email?: string;
phone?: string; phone?: string;
enabled: boolean; enabled: boolean;
createTime: string; roles: Role[];
updateTime: string;
} }

View File

@ -0,0 +1,29 @@
// 分页请求参数
export interface PageParams {
pageNum: number; // 页码(从1开始)
pageSize: number; // 每页大小
sortField?: string; // 排序字段
sortOrder?: string; // 排序方向
}
// 分页响应数据
export interface Page<T> {
content: T[]; // 数据内容
totalElements: number; // 总记录数
totalPages: number; // 总页数
size: number; // 每页大小
number: number; // 当前页码(从0开始)
numberOfElements: number; // 当前页记录数
first: boolean; // 是否第一页
last: boolean; // 是否最后一页
empty: boolean; // 是否为空
pageable: {
pageNumber: number;
pageSize: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
};
}

View File

@ -0,0 +1,42 @@
import type { Page } from './base/page';
// 表格服务接口
export interface TableService<T, P = any> {
list: (params: P) => Promise<Page<T>>;
create?: (data: T) => Promise<T>;
update?: (id: number, data: T) => Promise<T>;
delete?: (id: number) => Promise<void>;
}
// 表格配置项
export interface TableConfig<T> {
message?: {
createSuccess?: string;
updateSuccess?: string;
deleteSuccess?: string;
loadError?: string;
};
defaultPageSize?: number;
selection?: boolean; // 是否支持选择
onDataChange?: (data: T[]) => void; // 数据变化回调
}
// 表格状态
export interface TableState<T> {
list: T[];
pagination: {
current: number;
pageSize: number;
total: number;
};
loading: boolean;
selectedRows?: T[];
selectedRowKeys?: React.Key[];
}
// Hook参数
export interface UseTableDataProps<T, P = any> {
service: TableService<T, P>;
defaultParams?: Partial<P>;
config?: TableConfig<T>;
}

View File

@ -0,0 +1,29 @@
import type { Page, PageParams } from '@/types/base/page';
// 默认分页参数
const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_CURRENT = 1;
/**
*
*/
export const convertToPageParams = (params?: {
current?: number;
pageSize?: number;
sortField?: string;
sortOrder?: string;
}): PageParams => ({
pageNum: Math.max(1, params?.current || DEFAULT_CURRENT) - 1, // 转换为从0开始的页码
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
sortField: params?.sortField,
sortOrder: params?.sortOrder?.replace('end', '')
});
/**
*
*/
export const convertToPageInfo = (page?: Page<any>) => ({
current: (page?.number || 0) + 1,
pageSize: page?.size || DEFAULT_PAGE_SIZE,
total: page?.totalElements || 0
});

View File

@ -0,0 +1,20 @@
import type { TableState } from '@/types/table';
// 创建初始状态
export const createInitialState = <T>(pageSize: number = 10): TableState<T> => ({
list: [],
pagination: {
current: 1,
pageSize,
total: 0
},
loading: false,
selectedRows: [],
selectedRowKeys: []
});
// 处理错误
export const handleTableError = (error: any, message: string) => {
console.error(message, error);
return false;
};