可正常启用
This commit is contained in:
parent
14a7477d55
commit
57e0f025b5
35
frontend/src/components/IconSelect/index.module.less
Normal file
35
frontend/src/components/IconSelect/index.module.less
Normal 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;
|
||||||
|
}
|
||||||
@ -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('初始化用户数据失败');
|
||||||
|
|||||||
@ -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;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
@ -15,8 +15,10 @@ 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) =>
|
||||||
|
|||||||
@ -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[];
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
@ -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, {
|
||||||
@ -37,3 +39,14 @@ export const getRoleMenus = async (roleId: number) => {
|
|||||||
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');
|
||||||
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user