可正常启用

This commit is contained in:
戚辰先生 2024-12-01 11:08:47 +08:00
parent 14a7477d55
commit 57e0f025b5
10 changed files with 830 additions and 633 deletions

View File

@ -0,0 +1,35 @@
.iconGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 16px;
max-height: 400px;
overflow-y: auto;
}
.iconItem {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
background: #e6f7ff;
}
}
.icon {
font-size: 24px;
margin-bottom: 4px;
}
.name {
font-size: 12px;
color: #666;
text-align: center;
word-break: break-all;
}

View File

@ -11,9 +11,8 @@ import {
CloudOutlined CloudOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import * as AntdIcons from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons';
import {logout, setMenus, setUserInfo} from '../store/userSlice'; import {logout, setMenus} from '../store/userSlice';
import type {MenuProps} from 'antd'; import type {MenuProps} from 'antd';
import {getCurrentUser} from '../pages/Login/service';
import {getCurrentUserMenus} from '@/pages/System/Menu/service'; import {getCurrentUserMenus} from '@/pages/System/Menu/service';
import {getWeather} from '../services/weather'; import {getWeather} from '../services/weather';
import type {MenuResponse} from '@/pages/System/Menu/types'; import type {MenuResponse} from '@/pages/System/Menu/types';
@ -52,9 +51,11 @@ const BasicLayout: React.FC = () => {
const initializeUserData = async () => { const initializeUserData = async () => {
// try { // try {
setLoading(true); setLoading(true);
const menuData = await getCurrentUserMenus() const menuData = await getCurrentUserMenus().then((response) => {
dispatch(setMenus(menuData)); console.log(response)
setLoading(false); dispatch(setMenus(response));
setLoading(false);
})
// } catch (error) { // } catch (error) {
// console.log(error); // console.log(error);
// message.error('初始化用户数据失败'); // message.error('初始化用户数据失败');

View File

@ -1,339 +1,354 @@
import React, { useEffect, useState } from 'react'; 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, Switch, Select, TreeSelect, Tooltip, InputNumber, Tree } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined, FolderOutlined, MenuOutlined, ToolOutlined, CaretRightOutlined } from '@ant-design/icons';
import * as AntdIcons from '@ant-design/icons'; import type { TablePaginationConfig } from 'antd/es/table';
import { getMenuTree, createMenu, updateMenu, deleteMenu } from './service'; import type { FilterValue, SorterResult } from 'antd/es/table/interface';
import { getMenuTree } from './service';
import type { MenuResponse } from './types';
import { MenuTypeEnum, MenuTypeNames } from './types';
import IconSelect from '@/components/IconSelect'; import IconSelect from '@/components/IconSelect';
import { useTableData } from '@/hooks/useTableData'; import { useTableData } from '@/hooks/useTableData';
import * as AntdIcons from '@ant-design/icons';
const MenuPage: React.FC = () => { const MenuPage: React.FC = () => {
const { const {
list: menus, list: menus,
pagination, pagination,
loading, loading,
loadData: fetchMenus, loadData: fetchMenus,
onPageChange, onPageChange,
handleCreate, handleCreate,
handleUpdate, handleUpdate,
handleDelete handleDelete
} = useTableData({ } = useTableData({
service: { service: {
baseUrl: '/api/v1/menu' baseUrl: '/api/v1/menu'
}, },
defaultParams: { defaultParams: {
sortField: 'sort', sortField: 'createTime',
sortOrder: 'asc' sortOrder: 'descend',
} pageSize: 1000
}); }
const [menuTreeData, setMenuTreeData] = useState<MenuDTO[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [iconSelectVisible, setIconSelectVisible] = useState(false);
const [editingMenu, setEditingMenu] = useState<MenuDTO | null>(null);
const [form] = Form.useForm();
const getIcon = (iconName: string | undefined) => {
if (!iconName) return null;
const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
const IconComponent = (AntdIcons as any)[iconKey];
return IconComponent ? <IconComponent /> : null;
};
const handleAdd = () => {
setEditingMenu(null);
form.resetFields();
form.setFieldsValue({
type: MenuTypeEnum.MENU,
sort: 0,
hidden: false,
enabled: true
}); });
setModalVisible(true);
};
const handleEdit = (record: MenuDTO) => { const [modalVisible, setModalVisible] = useState(false);
setEditingMenu(record); const [iconSelectVisible, setIconSelectVisible] = useState(false);
form.setFieldsValue({ const [editingMenu, setEditingMenu] = useState<MenuResponse | null>(null);
...record, const [form] = Form.useForm();
parentId: record.parentId === 0 ? undefined : record.parentId
});
setModalVisible(true);
};
const handleSubmit = async () => { const handleAdd = () => {
try { setEditingMenu(null);
const values = await form.validateFields(); form.resetFields();
const submitData = { form.setFieldsValue({
...values, type: MenuTypeEnum.MENU,
parentId: values.parentId || 0 sort: 0,
}; hidden: false
});
setModalVisible(true);
};
if (editingMenu) { const handleEdit = (record: MenuResponse) => {
await updateMenu(editingMenu.id, submitData); setEditingMenu(record);
message.success('更新成功'); form.setFieldsValue({
} else { ...record,
await createMenu(submitData); parentId: record.parentId === 0 ? undefined : record.parentId
message.success('创建成功'); });
} setModalVisible(true);
setModalVisible(false); };
fetchMenus();
} catch (error) {
message.error('操作失败');
}
};
const getTreeSelectData = (menus: MenuDTO[]) => { const handleTableChange = (
const treeData = menus.map(menu => ({ pagination: TablePaginationConfig,
title: menu.name, filters: Record<string, FilterValue | null>,
value: menu.id, sorter: SorterResult<MenuResponse> | SorterResult<MenuResponse>[]
children: menu.children?.map(child => ({ ) => {
title: child.name, const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
value: child.id,
disabled: editingMenu?.id === child.id fetchMenus({
})) sortField: field as string,
sortOrder: order
});
};
const buildMenuTree = (menuList: MenuResponse[]): MenuResponse[] => {
const menuMap = new Map<number, MenuResponse>();
const result: MenuResponse[] = [];
menuList.forEach(menu => {
menuMap.set(menu.id, { ...menu, children: [] });
});
menuList.forEach(menu => {
const node = menuMap.get(menu.id)!;
if (menu.parentId === 0 || !menuMap.has(menu.parentId)) {
result.push(node);
} else {
const parent = menuMap.get(menu.parentId)!;
parent.children = parent.children || [];
parent.children.push(node);
}
});
return result;
};
const getTreeSelectData = () => {
const menuTree = buildMenuTree(menus);
return menuTree.map(menu => ({
title: menu.name,
value: menu.id,
children: menu.children?.map(child => ({
title: child.name,
value: child.id,
disabled: editingMenu?.id === child.id
}))
}));
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingMenu) {
const success = await handleUpdate(editingMenu.id, {
...values,
version: editingMenu.version
});
if (success) {
setModalVisible(false);
fetchMenus();
}
} else {
const success = await handleCreate(values);
if (success) {
setModalVisible(false);
fetchMenus();
}
}
} catch (error) {
console.error('操作失败:', error);
}
};
const getIcon = (iconName: string | undefined) => {
if (!iconName) return null;
const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
const Icon = (AntdIcons as any)[iconKey];
return Icon ? <Icon /> : null;
};
const treeData = buildMenuTree(menus).map(menu => ({
key: menu.id,
title: (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Space>
{menu.type === MenuTypeEnum.DIRECTORY ? <FolderOutlined /> :
menu.type === MenuTypeEnum.MENU ? <MenuOutlined /> :
<ToolOutlined />}
<span>{menu.name}</span>
{menu.icon && getIcon(menu.icon)}
</Space>
<Space size={8}>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
handleEdit(menu);
}}
>
</Button>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
handleDelete(menu.id);
}}
>
</Button>
</Space>
</div>
),
children: menu.children?.map(child => ({
key: child.id,
title: (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Space>
{child.type === MenuTypeEnum.DIRECTORY ? <FolderOutlined /> :
child.type === MenuTypeEnum.MENU ? <MenuOutlined /> :
<ToolOutlined />}
<span>{child.name}</span>
{child.icon && getIcon(child.icon)}
</Space>
<Space size={8}>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
handleEdit(child);
}}
>
</Button>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
handleDelete(child.id);
}}
>
</Button>
</Space>
</div>
)
}))
})); }));
return treeData; return (
}; <div style={{ padding: '24px' }}>
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
</div>
const columns = [ <Tree
{ showLine={{ showLeafIcon: false }}
title: '菜单名称', defaultExpandAll
dataIndex: 'name', treeData={treeData}
key: 'name',
width: '200px',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: '100px',
render: (type: MenuTypeEnum) => MenuTypeNames[type],
},
{
title: '权限标识',
dataIndex: 'permission',
key: 'permission',
width: '150px',
},
{
title: '路由地址',
dataIndex: 'path',
key: 'path',
width: '150px',
},
{
title: '组件路径',
dataIndex: 'component',
key: 'component',
width: '150px',
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: '80px',
},
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
width: '80px',
render: (enabled: boolean) => (
<Switch checked={enabled} disabled />
),
},
{
title: '操作',
key: 'action',
width: '150px',
render: (_: any, record: MenuDTO) => (
<Space>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)}
>
</Button>
</Space>
),
},
];
return (
<div style={{ padding: '24px' }}>
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
</div>
<Table
loading={loading}
columns={columns}
dataSource={menus}
rowKey="id"
pagination={pagination}
size="middle"
bordered
onChange={onPageChange}
/>
<Modal
title={editingMenu ? '编辑菜单' : '新增菜单'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={600}
destroyOnClose
>
<Form
form={form}
layout="vertical"
initialValues={{ type: MenuTypeEnum.MENU, sort: 0 }}
>
<Form.Item
name="name"
label="菜单名称"
rules={[{ required: true, message: '请输入菜单名称' }]}
>
<Input placeholder="请输入菜单名称" />
</Form.Item>
<Form.Item
name="type"
label="菜单类型"
rules={[{ required: true, message: '请选择菜单类型' }]}
>
<Select>
<Select.Option value={MenuTypeEnum.DIRECTORY}></Select.Option>
<Select.Option value={MenuTypeEnum.MENU}></Select.Option>
<Select.Option value={MenuTypeEnum.BUTTON}></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="parentId"
label={
<span>
<Tooltip title="不选择则为顶级菜单">
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Tooltip>
</span>
}
>
<TreeSelect
treeData={getTreeSelectData(menuTreeData)}
placeholder="不选择则为顶级菜单"
allowClear
treeDefaultExpandAll
showSearch
treeNodeFilterProp="title"
/> />
</Form.Item>
{form.getFieldValue('type') !== MenuTypeEnum.BUTTON && ( <Modal
<> title={editingMenu ? '编辑菜单' : '新增菜单'}
<Form.Item open={modalVisible}
name="path" onOk={handleSubmit}
label={ onCancel={() => setModalVisible(false)}
<span> width={600}
destroyOnClose
<Tooltip title="目录示例: /system 菜单示例: /system/user">
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Tooltip>
</span>
}
rules={[{ required: true, message: '请输入路由地址' }]}
>
<Input placeholder="请输入路由地址" />
</Form.Item>
<Form.Item
name="component"
label={
<span>
<Tooltip title="目录固定值: LAYOUT 菜单示例: /System/User/index">
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Tooltip>
</span>
}
rules={[{ required: true, message: '请输入组件路径' }]}
>
<Input placeholder="请输入组件路径" />
</Form.Item>
<Form.Item
name="icon"
label="图标"
>
<Input
placeholder="请选择图标"
readOnly
onClick={() => setIconSelectVisible(true)}
suffix={form.getFieldValue('icon') && getIcon(form.getFieldValue('icon'))}
/>
</Form.Item>
</>
)}
{form.getFieldValue('type') === MenuTypeEnum.BUTTON && (
<Form.Item
name="permission"
label={
<span>
<Tooltip title="示例: system:user:add">
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Tooltip>
</span>
}
rules={[{ required: true, message: '请输入权限标识' }]}
> >
<Input placeholder="请输入权限标识" /> <Form
</Form.Item> form={form}
)} layout="vertical"
initialValues={{ type: MenuTypeEnum.MENU, sort: 0 }}
>
<Form.Item
name="name"
label="菜单名称"
rules={[{ required: true, message: '请输入菜单名称' }]}
>
<Input placeholder="请输入菜单名称" />
</Form.Item>
<Form.Item <Form.Item
name="sort" name="type"
label="显示排序" label="菜单类型"
rules={[{ required: true, message: '请输入显示排序' }]} rules={[{ required: true, message: '请选择菜单类型' }]}
> >
<InputNumber style={{ width: '100%' }} min={0} placeholder="请输入显示排序" /> <Select>
</Form.Item> <Select.Option value={MenuTypeEnum.DIRECTORY}></Select.Option>
<Select.Option value={MenuTypeEnum.MENU}></Select.Option>
<Select.Option value={MenuTypeEnum.BUTTON}></Select.Option>
</Select>
</Form.Item>
<Form.Item <Form.Item
name="hidden" name="parentId"
label="是否隐藏" label={
valuePropName="checked" <span>
>
<Switch /> <Tooltip title="不选择则为顶级菜单">
</Form.Item> <QuestionCircleOutlined style={{ marginLeft: 4 }} />
</Form> </Tooltip>
</Modal> </span>
}
>
<TreeSelect
treeData={getTreeSelectData()}
placeholder="不选择则为顶级菜单"
allowClear
treeDefaultExpandAll
showSearch
treeNodeFilterProp="title"
/>
</Form.Item>
<IconSelect <Form.Item
visible={iconSelectVisible} name="path"
onCancel={() => setIconSelectVisible(false)} label="路由地址"
value={form.getFieldValue('icon')} rules={[{ required: true, message: '请输入路由地址' }]}
onChange={value => { >
form.setFieldValue('icon', value); <Input placeholder="请输入路由地址" />
setIconSelectVisible(false); </Form.Item>
}}
/> <Form.Item
</div> name="component"
); label="组件路径"
>
<Input placeholder="请输入组件路径" />
</Form.Item>
<Form.Item
name="permission"
label="权限标识"
>
<Input placeholder="请输入权限标识" />
</Form.Item>
<Form.Item
name="icon"
label="图标"
>
<Input
placeholder="请选择图标"
readOnly
onClick={() => setIconSelectVisible(true)}
suffix={form.getFieldValue('icon') && getIcon(form.getFieldValue('icon'))}
/>
</Form.Item>
<Form.Item
name="sort"
label="显示排序"
rules={[{ required: true, message: '请输入显示排序' }]}
>
<InputNumber style={{ width: '100%' }} min={0} placeholder="请输入显示排序" />
</Form.Item>
<Form.Item
name="hidden"
label="隐藏菜单"
valuePropName="checked"
tooltip="设置为是则该菜单不会显示在导航栏中"
>
<Switch
checkedChildren="是"
unCheckedChildren="否"
/>
</Form.Item>
</Form>
</Modal>
<IconSelect
visible={iconSelectVisible}
onCancel={() => setIconSelectVisible(false)}
value={form.getFieldValue('icon')}
onChange={value => {
form.setFieldValue('icon', value);
setIconSelectVisible(false);
}}
/>
</div>
);
}; };
export default MenuPage; export default MenuPage;

View File

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

View File

@ -1,31 +1,48 @@
import type { BaseResponse } from '@/types/base/response';
export enum MenuTypeEnum {
DIRECTORY = 1, // 目录
MENU = 2, // 菜单
BUTTON = 3 // 按钮
}
export const MenuTypeNames = {
[MenuTypeEnum.DIRECTORY]: '目录',
[MenuTypeEnum.MENU]: '菜单',
[MenuTypeEnum.BUTTON]: '按钮'
};
export interface MenuQuery { export interface MenuQuery {
name?: string; pageNum?: number;
path?: string; pageSize?: number;
enabled?: boolean; sortField?: string;
pageNum?: number; sortOrder?: 'ascend' | 'descend';
pageSize?: number; name?: string;
sortField?: string; type?: MenuTypeEnum;
sortOrder?: string; enabled?: boolean;
} }
export interface MenuRequest { export interface MenuRequest {
name: string; name: string;
path?: string; type: MenuTypeEnum;
icon?: string; parentId?: number;
parentId?: number; path?: string;
sort?: number; component?: string;
enabled?: boolean; permission?: string;
icon?: string;
sort: number;
hidden: boolean;
} }
export interface MenuResponse { export interface MenuResponse extends BaseResponse {
id: number; name: string;
name: string; type: MenuTypeEnum;
path?: string; parentId: number;
icon?: string; path?: string;
parentId?: number; component?: string;
sort: number; permission?: string;
enabled: boolean; icon?: string;
children?: MenuResponse[]; sort: number;
createTime: string; hidden: boolean;
updateTime: string; children?: MenuResponse[];
} }

View File

@ -1,228 +1,320 @@
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Table, Button, Modal, Form, Input, Space, message } from 'antd'; import { Table, Button, Modal, Form, Input, Space, message, InputNumber, Switch, Select, Tag } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, TagOutlined } from '@ant-design/icons';
import type { RoleDTO } from './types'; import type { RoleResponse, RoleTagResponse } from './types';
import { getRoles, createRole, updateRole, deleteRole } from './service'; import { useTableData } from '@/hooks/useTableData';
import PermissionModal from './components/PermissionModal'; import type { FixedType } from 'rc-table/lib/interface';
import type { TablePaginationConfig } from 'antd/es/table';
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
import { assignTags, getAllTags } from './service';
const RolePage: React.FC = () => { const RolePage: React.FC = () => {
const [roles, setRoles] = useState<RoleDTO[]>([]); const {
const [modalVisible, setModalVisible] = useState(false); list: roles,
const [editingRole, setEditingRole] = useState<RoleDTO | null>(null); pagination,
const [loading, setLoading] = useState(false); loading,
const [form] = Form.useForm(); loadData: fetchRoles,
const [permissionModalVisible, setPermissionModalVisible] = useState(false); onPageChange,
const [selectedRole, setSelectedRole] = useState<RoleDTO | null>(null); handleCreate,
handleUpdate,
const fetchRoles = async () => { handleDelete
try { } = useTableData({
setLoading(true); service: {
const data = await getRoles(); baseUrl: '/api/v1/role'
setRoles(data || []); },
} catch (error) { defaultParams: {
console.error('获取角色列表失败:', error); sortField: 'createTime',
message.error('获取角色列表失败'); sortOrder: 'descend'
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchRoles();
}, []);
const handleAdd = () => {
setEditingRole(null);
form.resetFields();
form.setFieldsValue({
sort: 0
});
setModalVisible(true);
};
const handleEdit = (record: RoleDTO) => {
setEditingRole(record);
form.setFieldsValue(record);
setModalVisible(true);
};
const handleDelete = async (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个角色吗?',
onOk: async () => {
try {
await deleteRole(id);
message.success('删除成功');
fetchRoles();
} catch (error) {
message.error('删除失败');
} }
},
}); });
};
const handleSubmit = async () => { const [modalVisible, setModalVisible] = useState(false);
try { const [editingRole, setEditingRole] = useState<RoleResponse | null>(null);
const values = await form.validateFields(); const [form] = Form.useForm();
if (editingRole) {
await updateRole(editingRole.id, { const [tagModalVisible, setTagModalVisible] = useState(false);
...values, const [selectedRole, setSelectedRole] = useState<RoleResponse | null>(null);
version: editingRole.version const [selectedTags, setSelectedTags] = useState<number[]>([]);
const [allTags, setAllTags] = useState<RoleTagResponse[] | null>([]);
useEffect(() => {
getAllTags().then(response => {
setAllTags(response);
}); });
message.success('更新成功'); }, []);
} else {
await createRole(values);
message.success('创建成功');
}
setModalVisible(false);
fetchRoles();
} catch (error) {
message.error('操作失败');
}
};
const handlePermission = (record: RoleDTO) => { const handleAdd = () => {
setSelectedRole(record); setEditingRole(null);
setPermissionModalVisible(true); form.resetFields();
}; setModalVisible(true);
};
const columns = [ const handleEdit = (record: RoleResponse) => {
{ setEditingRole(record);
title: '角色名称', form.setFieldsValue(record);
dataIndex: 'name', setModalVisible(true);
key: 'name', };
width: '20%',
},
{
title: '角色编码',
dataIndex: 'code',
key: 'code',
width: '20%',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: '35%',
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: '10%',
},
{
title: '操作',
key: 'action',
width: '15%',
render: (_: any, record: RoleDTO) => (
<Space>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Button
type="link"
icon={<KeyOutlined />}
onClick={() => handlePermission(record)}
disabled={record.code === 'ROLE_ADMIN'}
>
</Button>
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)}
disabled={record.code === 'ROLE_ADMIN'}
>
</Button>
</Space>
),
},
];
return ( const handleSubmit = async () => {
<div style={{ padding: '24px' }}> try {
<div style={{ marginBottom: 16 }}> const values = await form.validateFields();
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}> if (editingRole) {
const success = await handleUpdate(editingRole.id, {
</Button> ...values,
</div> version: editingRole.version
});
if (success) {
setModalVisible(false);
fetchRoles();
}
} else {
const success = await handleCreate(values);
if (success) {
setModalVisible(false);
fetchRoles();
}
}
} catch (error) {
console.error('操作失败:', error);
}
};
<Table const handleAssignTags = (record: RoleResponse) => {
loading={loading} setSelectedRole(record);
columns={columns} if (record.tags) {
dataSource={roles} setSelectedTags(record.tags.map(tag => tag.id));
rowKey="id" } else {
pagination={false} setSelectedTags([]);
size="middle" }
bordered setTagModalVisible(true);
/> };
<Modal const handleTagSubmit = async () => {
title={editingRole ? '编辑角色' : '新增角色'} if (selectedRole) {
open={modalVisible} await assignTags(selectedRole.id, selectedTags);
onOk={handleSubmit} setTagModalVisible(false);
onCancel={() => setModalVisible(false)}
width={600}
destroyOnClose
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="name"
label="角色名称"
rules={[{ required: true, message: '请输入角色名称' }]}
>
<Input placeholder="请输入角色名称" />
</Form.Item>
<Form.Item
name="code"
label="角色编码"
rules={[{ required: true, message: '请输入角色编码' }]}
>
<Input placeholder="请输入角色编码" />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={4} placeholder="请输入描述" />
</Form.Item>
<Form.Item
name="sort"
label="排序"
>
<Input type="number" placeholder="请输入排序号" />
</Form.Item>
</Form>
</Modal>
{selectedRole && (
<PermissionModal
roleId={selectedRole.id}
visible={permissionModalVisible}
onCancel={() => {
setPermissionModalVisible(false);
setSelectedRole(null);
}}
onSuccess={() => {
setPermissionModalVisible(false);
setSelectedRole(null);
fetchRoles(); fetchRoles();
}} }
/> };
)}
</div> const columns = [
); {
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 60,
fixed: 'left' as FixedType,
sorter: true
},
{
title: '角色编码',
dataIndex: 'code',
key: 'code',
width: 120,
sorter: true
},
{
title: '角色名称',
dataIndex: 'name',
key: 'name',
width: 120,
sorter: true
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 200
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 80,
sorter: true
},
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
width: 80,
render: (enabled: boolean) => (
<Switch checked={enabled} disabled size="small" />
)
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 150,
sorter: true,
defaultSortOrder: 'descend'
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: 150,
sorter: true
},
{
title: '标签',
dataIndex: 'tags',
key: 'tags',
width: 200,
render: (tags: RoleTagResponse[]) => (
<Space>
{tags?.map(tag => (
<Tag key={tag.id} color={tag.color}>
{tag.name}
</Tag>
))}
</Space>
)
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right' as FixedType,
render: (_: any, record: RoleResponse) => (
<Space size={0}>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)}
disabled={record.code === 'admin'}
>
</Button>
<Button
type="link"
size="small"
icon={<TagOutlined />}
onClick={() => handleAssignTags(record)}
>
</Button>
</Space>
)
}
];
const handleTableChange = (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<RoleResponse> | SorterResult<RoleResponse>[]
) => {
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
fetchRoles({
sortField: field as string,
sortOrder: order
});
};
return (
<div style={{ padding: '24px' }}>
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
</div>
<Table
loading={loading}
columns={columns}
dataSource={roles}
rowKey="id"
scroll={{ x: 1500 }}
pagination={pagination}
onChange={handleTableChange}
size="middle"
bordered
/>
<Modal
title={editingRole ? '编辑角色' : '新增角色'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={600}
destroyOnClose
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="code"
label="角色编码"
rules={[{ required: true, message: '请输入角色编码' }]}
>
<Input placeholder="请输入角色编码" disabled={!!editingRole} />
</Form.Item>
<Form.Item
name="name"
label="角色名称"
rules={[{ required: true, message: '请输入角色名称' }]}
>
<Input placeholder="请输入角色名称" />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={4} placeholder="请输入描述" />
</Form.Item>
<Form.Item
name="sort"
label="排序"
rules={[{ required: true, message: '请输入排序' }]}
>
<InputNumber style={{ width: '100%' }} min={0} placeholder="请输入排序" />
</Form.Item>
</Form>
</Modal>
<Modal
title="分配标签"
open={tagModalVisible}
onOk={handleTagSubmit}
onCancel={() => setTagModalVisible(false)}
width={600}
destroyOnClose
>
<Form layout="vertical">
<Form.Item label="选择标签">
<Select
mode="multiple"
placeholder="请选择标签"
value={selectedTags}
onChange={setSelectedTags}
style={{ width: '100%' }}
>
{allTags?.map(tag => (
<Select.Option key={tag.id} value={tag.id}>
<Tag color={tag.color}>{tag.name}</Tag>
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
}; };
export default RolePage; export default RolePage;

View File

@ -1,30 +1,32 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { RoleResponse, RoleRequest, RoleQuery } from './types'; import type { Page } from '@/types/base/page';
import type { RoleResponse, RoleRequest, RoleQuery, RoleTagResponse } from './types';
export const getRoles = async (params?: RoleQuery) => { const BASE_URL = '/api/v1/role';
return request.get('/api/v1/role', {
params,
errorMessage: '获取角色列表失败,请刷新重试'
});
};
export const createRole = async (data: RoleRequest) => { // 获取角色列表(分页)
return request.post('/api/v1/role', data, { export const getRoles = (params?: RoleQuery) =>
errorMessage: '创建角色失败,请稍后重试' request.get<Page<RoleResponse>>(`${BASE_URL}/page`, {
}); params
}; });
export const updateRole = async (id: number, data: RoleRequest) => { // 创建角色
return request.put(`/api/v1/role/${id}`, data, { export const createRole = (data: RoleRequest) =>
errorMessage: '更新角色失败,请稍后重试' request.post<RoleResponse>(BASE_URL, data, {
}); successMessage: '创建角色成功'
}; });
export const deleteRole = async (id: number) => { // 更新角色
return request.delete(`/api/v1/role/${id}`, { export const updateRole = (id: number, data: RoleRequest) =>
errorMessage: '删除角色失败,请稍后重试' request.put<RoleResponse>(`${BASE_URL}/${id}`, data, {
}); successMessage: '更新角色成功'
}; });
// 删除角色
export const deleteRole = (id: number) =>
request.delete(`${BASE_URL}/${id}`, {
successMessage: '删除角色成功'
});
export const assignMenus = async (roleId: number, menuIds: number[]) => { export const assignMenus = async (roleId: number, menuIds: number[]) => {
return request.post(`/api/v1/role/${roleId}/menus`, menuIds, { return request.post(`/api/v1/role/${roleId}/menus`, menuIds, {
@ -36,4 +38,15 @@ export const getRoleMenus = async (roleId: number) => {
return request.get(`/api/v1/role/${roleId}/menus`, { return request.get(`/api/v1/role/${roleId}/menus`, {
errorMessage: '获取角色菜单失败,请刷新重试' errorMessage: '获取角色菜单失败,请刷新重试'
}); });
}; };
// 分配标签
export const assignTags = (roleId: number, tagIds: number[]) =>
request.post(`${BASE_URL}/${roleId}/tags`, tagIds, {
successMessage: '分配标签成功'
});
// 获取所有标签
export const getAllTags = () =>
request.get<RoleTagResponse[]>('/api/v1/role-tag/list');

View File

@ -1,17 +1,38 @@
export interface RoleDTO { import type { BaseResponse } from '@/types/base/response';
id: number;
name: string;
code: string;
description?: string;
sort: number;
version?: number;
createTime?: string;
updateTime?: string;
}
export interface RoleQuery { export interface RoleQuery {
page: number; pageNum?: number;
size: number; pageSize?: number;
name?: string; sortField?: string;
code?: string; sortOrder?: 'ascend' | 'descend';
code?: string;
name?: string;
}
export interface RoleRequest {
code: string;
name: string;
description?: string;
sort: number;
}
export interface RoleResponse extends BaseResponse {
code: string;
name: string;
description?: string;
sort: number;
enabled: boolean;
tags?: RoleTagResponse[];
}
export interface RoleTagRequest {
name: string;
color?: string;
description?: string;
}
export interface RoleTagResponse extends BaseResponse {
name: string;
color: string;
description?: string;
} }

View File

@ -3,10 +3,12 @@ import {Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect} f
import {PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined} from '@ant-design/icons'; import {PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined} from '@ant-design/icons';
import type {UserResponse, Role} from './types'; import type {UserResponse, Role} from './types';
import type {DepartmentDTO} from '../Department/types'; import type {DepartmentDTO} from '../Department/types';
import {getUsers, createUser, updateUser, deleteUser, resetPassword} from './service'; import {resetPassword} from './service';
import {useTableData} from '@/hooks/useTableData'; import {useTableData} from '@/hooks/useTableData';
import type {FixedType, AlignType, SortOrder} from 'rc-table/lib/interface'; import type {FixedType, AlignType, SortOrder} from 'rc-table/lib/interface';
import {Response} from "@/utils/request.ts"; import {Response} from "@/utils/request.ts";
import {TablePaginationConfig} from "antd/es/table";
import {FilterValue, SorterResult} from "antd/es/table/interface";
const UserPage: React.FC = () => { const UserPage: React.FC = () => {
const { const {
@ -219,7 +221,6 @@ const UserPage: React.FC = () => {
sorter: SorterResult<UserResponse> | SorterResult<UserResponse>[] sorter: SorterResult<UserResponse> | SorterResult<UserResponse>[]
) => { ) => {
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter; const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
fetchUsers({ fetchUsers({
sortField: field as string, sortField: field as string,
sortOrder: order sortOrder: order

View File

@ -41,53 +41,52 @@ const createRequestConfig = (options?: Partial<RequestOptions>): RequestOptions
}; };
const responseHandler = (response: AxiosResponse<Response<any>>) => { const responseHandler = (response: AxiosResponse<Response<any>>) => {
const res = response.data; const data = response.data;
const config = response.config as RequestOptions; const config = response.config as RequestOptions;
if (res.success && res.code === 200) { if (data.success && data.code === 200) {
config.successMessage && message.success(config.successMessage); config.successMessage && message.success(config.successMessage);
return res.data; return data.data;
} else { } else {
message.error(config.errorMessage || res.message); message.error(config.errorMessage || data.message);
return Promise.reject(res); return Promise.reject(response); // 隐式返回 Promise
} }
}; };
const statusMessages: Record<number, string> = {
401: '未授权,请重新登录',
403: '拒绝访问',
404: '请求错误,未找到该资源',
500: '服务器错误'
};
const defaultErrorMessage = '服务器异常,请稍后再试!';
const errorHandler = (error: any) => { const errorHandler = (error: any) => {
const config = error.config as RequestOptions; const config = error.config as RequestOptions;
// 检查是否有响应
if (!error.response) { if (!error.response) {
message.error('网络连接异常,请检查网络'); message.error('网络连接异常,请检查网络');
return Promise.reject(error); return Promise.reject(error);
} }
// 检查是否为取消请求
if (axios.isCancel(error)) { if (axios.isCancel(error)) {
return Promise.reject(error); return Promise.reject(error);
} }
const status = error.response.status; const status = error.response.status;
let msg = ''; const msg = statusMessages[status] || defaultErrorMessage; // 获取相应的消息
switch (status) {
case 401:
msg = '未授权,请重新登录';
break;
case 403:
msg = '拒绝访问';
break;
case 404:
msg = '请求错误,未找到该资源';
break;
case 500:
msg = '服务器错误';
break;
default:
msg = '服务器异常,请稍后再试!';
}
// 显示错误信息
message.error(config.errorMessage || msg); message.error(config.errorMessage || msg);
return Promise.reject(error);
return Promise.reject(error); // 拦截错误并返回
}; };
request.interceptors.request.use( request.interceptors.request.use(
(config) => { (config) => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
@ -102,8 +101,9 @@ request.interceptors.request.use(
request.interceptors.response.use(responseHandler, errorHandler); request.interceptors.response.use(responseHandler, errorHandler);
const http = { const http = {
get: <T = any>(url: string, config?: RequestOptions) => get: <T = any>(url: string, config?: RequestOptions) => {
request.get<any, Response<T>>(url, createRequestConfig(config)), return request.get<any, T>(url, createRequestConfig(config));
},
post: <T = any>(url: string, data?: any, config?: RequestOptions) => post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
request.post<any, Response<T>>(url, data, createRequestConfig(config)), request.post<any, Response<T>>(url, data, createRequestConfig(config)),