deploy-ease-platform/frontend/src/pages/System/Menu/index.tsx
2024-12-01 16:50:49 +08:00

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;