388 lines
13 KiB
TypeScript
388 lines
13 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Table, Button, Modal, Form, Input, Space, Switch, Select, TreeSelect, Tooltip, InputNumber, Tree } from 'antd';
|
|
import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined, FolderOutlined, MenuOutlined, ToolOutlined, CaretRightOutlined } from '@ant-design/icons';
|
|
import type { TablePaginationConfig } from 'antd/es/table';
|
|
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
|
|
import { getMenuTree } from './service';
|
|
import type { MenuResponse } from './types';
|
|
import { MenuTypeEnum } from './types';
|
|
import IconSelect from '@/components/IconSelect';
|
|
import { useTableData } from '@/hooks/useTableData';
|
|
import * as AntdIcons from '@ant-design/icons';
|
|
import {FixedType} from "rc-table/lib/interface";
|
|
|
|
const MenuPage: React.FC = () => {
|
|
const {
|
|
list: menus,
|
|
loading,
|
|
loadData: fetchMenus,
|
|
handleCreate,
|
|
handleUpdate,
|
|
handleDelete
|
|
} = useTableData({
|
|
service: {
|
|
baseUrl: '/api/v1/menu'
|
|
},
|
|
defaultParams: {
|
|
sortField: 'sort',
|
|
sortOrder: 'asc'
|
|
}
|
|
});
|
|
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
const [editingMenu, setEditingMenu] = useState<MenuResponse | null>(null);
|
|
const [menuTree, setMenuTree] = useState<MenuResponse[]>([]);
|
|
const [iconSelectVisible, setIconSelectVisible] = useState(false);
|
|
const [form] = Form.useForm();
|
|
|
|
useEffect(() => {
|
|
getMenuTree().then(menus => setMenuTree(menus));
|
|
}, []);
|
|
|
|
const handleAdd = () => {
|
|
setEditingMenu(null);
|
|
form.resetFields();
|
|
|
|
const maxSort = Math.max(0, ...menus.map(menu => menu.sort));
|
|
|
|
form.setFieldsValue({
|
|
type: MenuTypeEnum.MENU,
|
|
sort: maxSort + 10,
|
|
hidden: false
|
|
});
|
|
setModalVisible(true);
|
|
};
|
|
|
|
const handleEdit = (record: MenuResponse) => {
|
|
setEditingMenu(record);
|
|
form.setFieldsValue({
|
|
...record,
|
|
parentId: record.parentId === 0 ? undefined : record.parentId
|
|
});
|
|
setModalVisible(true);
|
|
};
|
|
|
|
const handleTableChange = (
|
|
pagination: TablePaginationConfig,
|
|
filters: Record<string, FilterValue | null>,
|
|
sorter: SorterResult<MenuResponse> | SorterResult<MenuResponse>[]
|
|
) => {
|
|
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter;
|
|
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 columns = [
|
|
{
|
|
title: '菜单名称',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
width: 250,
|
|
fixed: 'left' as FixedType,
|
|
sorter: true
|
|
},
|
|
{
|
|
title: '图标',
|
|
dataIndex: 'icon',
|
|
key: 'icon',
|
|
width: 80,
|
|
render: (icon: string) => getIcon(icon)
|
|
},
|
|
{
|
|
title: '类型',
|
|
dataIndex: 'type',
|
|
key: 'type',
|
|
width: 100,
|
|
render: (type: MenuTypeEnum) => {
|
|
const typeMap = {
|
|
[MenuTypeEnum.DIRECTORY]: '目录',
|
|
[MenuTypeEnum.MENU]: '菜单',
|
|
[MenuTypeEnum.BUTTON]: '按钮'
|
|
};
|
|
return typeMap[type];
|
|
}
|
|
},
|
|
{
|
|
title: '路由地址',
|
|
dataIndex: 'path',
|
|
key: 'path',
|
|
width: 200,
|
|
ellipsis: true
|
|
},
|
|
{
|
|
title: '组件路径',
|
|
dataIndex: 'component',
|
|
key: 'component',
|
|
width: 200,
|
|
ellipsis: true
|
|
},
|
|
{
|
|
title: '权限标识',
|
|
dataIndex: 'permission',
|
|
key: 'permission',
|
|
width: 150,
|
|
ellipsis: true
|
|
},
|
|
{
|
|
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: '操作',
|
|
key: 'action',
|
|
width: 160,
|
|
fixed: 'right' as FixedType,
|
|
render: (_: any, record: MenuResponse) => (
|
|
<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.children?.length > 0}
|
|
>
|
|
删除
|
|
</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={menuTree}
|
|
rowKey="id"
|
|
scroll={{x: 1500}}
|
|
pagination={false}
|
|
size="middle"
|
|
bordered
|
|
indentSize={24}
|
|
/>
|
|
|
|
<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()}
|
|
placeholder="不选择则为顶级菜单"
|
|
allowClear
|
|
treeDefaultExpandAll
|
|
showSearch
|
|
treeNodeFilterProp="title"
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="path"
|
|
label="路由地址"
|
|
rules={[{ required: true, message: '请输入路由地址' }]}
|
|
>
|
|
<Input placeholder="请输入路由地址" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
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="显示排序"
|
|
tooltip="值越大排序越靠后,默认为当前最大值+10"
|
|
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;
|