357 lines
9.8 KiB
TypeScript
357 lines
9.8 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
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 * as AntdIcons from '@ant-design/icons';
|
|
import { getMenuTree, createMenu, updateMenu, deleteMenu } from './service';
|
|
import IconSelect from '@/components/IconSelect';
|
|
|
|
const MenuPage: React.FC = () => {
|
|
const [menus, setMenus] = useState<MenuDTO[]>([]);
|
|
const [menuTreeData, setMenuTreeData] = useState<MenuDTO[]>([]);
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
const [iconSelectVisible, setIconSelectVisible] = useState(false);
|
|
const [editingMenu, setEditingMenu] = useState<MenuDTO | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [form] = Form.useForm();
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [menuList, treeData] = await Promise.all([
|
|
getMenuTree(),
|
|
getMenuTreeWithoutButtons()
|
|
]);
|
|
setMenus(menuList);
|
|
setMenuTreeData(treeData);
|
|
} catch (error) {
|
|
console.error('获取菜单列表失败:', error);
|
|
message.error('获取菜单列表失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const getIcon = (iconName: string | undefined) => {
|
|
if (!iconName) return null;
|
|
const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
|
|
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) => {
|
|
setEditingMenu(record);
|
|
form.setFieldsValue({
|
|
...record,
|
|
parentId: record.parentId === 0 ? undefined : record.parentId
|
|
});
|
|
setModalVisible(true);
|
|
};
|
|
|
|
const handleDelete = async (id: number) => {
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: '确定要删除这个菜单吗?',
|
|
onOk: async () => {
|
|
try {
|
|
await deleteMenu(id);
|
|
message.success('删除成功');
|
|
fetchData();
|
|
} catch (error) {
|
|
message.error('删除失败');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
const submitData = {
|
|
...values,
|
|
parentId: values.parentId || 0
|
|
};
|
|
|
|
if (editingMenu) {
|
|
await updateMenu(editingMenu.id, submitData);
|
|
message.success('更新成功');
|
|
} else {
|
|
await createMenu(submitData);
|
|
message.success('创建成功');
|
|
}
|
|
setModalVisible(false);
|
|
fetchData();
|
|
} catch (error) {
|
|
message.error('操作失败');
|
|
}
|
|
};
|
|
|
|
const getTreeSelectData = (menus: MenuDTO[]) => {
|
|
const treeData = menus.map(menu => ({
|
|
title: menu.name,
|
|
value: menu.id,
|
|
children: menu.children?.map(child => ({
|
|
title: child.name,
|
|
value: child.id,
|
|
disabled: editingMenu?.id === child.id
|
|
}))
|
|
}));
|
|
|
|
return treeData;
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '菜单名称',
|
|
dataIndex: 'name',
|
|
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={false}
|
|
size="middle"
|
|
bordered
|
|
/>
|
|
|
|
<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 && (
|
|
<>
|
|
<Form.Item
|
|
name="path"
|
|
label={
|
|
<span>
|
|
路由地址
|
|
<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.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"
|
|
>
|
|
<Switch />
|
|
</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;
|