mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-14 00:03:42 +08:00
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import type { DataNode } from 'antd/es/tree';
|
|
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
import {
|
|
ModalForm,
|
|
PageContainer,
|
|
ProFormDigit,
|
|
ProFormRadio,
|
|
ProFormText,
|
|
ProFormTreeSelect,
|
|
ProTable,
|
|
type ActionType,
|
|
type ProColumns
|
|
} from '@ant-design/pro-components';
|
|
import { useBoolean } from 'ahooks';
|
|
import { Button, Form, message, Modal, Space, Tag, Tree } from 'antd';
|
|
import { useMemo, useRef, useState } from 'react';
|
|
import type { MenuForm, MenuQuery, MenuTreeOption, MenuVO } from '@/api/system/menu/types';
|
|
import { addMenu, cascadeDelMenu, delMenu, getMenu, listMenu, treeselect, updateMenu } from '@/api/system/menu';
|
|
import DictTag from '@/components/common/DictTag';
|
|
import EllipsisText from '@/components/common/EllipsisText';
|
|
import IconSelect from '@/components/common/IconSelect';
|
|
import RowActions from '@/components/common/RowActions';
|
|
import { useDict } from '@/hooks/useDict';
|
|
import { useLoading } from '@/hooks/useLoading';
|
|
import { useTableScroll } from '@/hooks/useTableScroll';
|
|
import { useUserStore } from '@/stores/userStore';
|
|
import { routeIcon } from '@/utils/menu';
|
|
import { dictOptions } from '@/utils/dict';
|
|
import { hasPermi } from '@/utils/permission';
|
|
import { handleTree } from '@/utils/ruoyi';
|
|
|
|
const defaultMenuForm: MenuForm = {
|
|
parentId: 0,
|
|
menuType: 'M',
|
|
orderNum: 1,
|
|
isFrame: 'N',
|
|
isCache: 'Y',
|
|
visible: '0',
|
|
status: '0'
|
|
};
|
|
|
|
|
|
function menuTypeMeta(row: MenuVO) {
|
|
if (row.menuType === 'F') return { label: '按钮', color: 'orange' };
|
|
if (row.isFrame === 'Y') return { label: '外链', color: 'red' };
|
|
if (row.menuType === 'M') return { label: '目录', color: 'blue' };
|
|
return { label: '菜单', color: 'green' };
|
|
}
|
|
|
|
function toTreeNodes(nodes: MenuTreeOption[]): DataNode[] {
|
|
return nodes.map(node => ({
|
|
title: node.label,
|
|
key: node.id,
|
|
children: node.children ? toTreeNodes(node.children) : undefined
|
|
}));
|
|
}
|
|
|
|
export default function SystemMenuPage() {
|
|
const actionRef = useRef<ActionType | undefined>(undefined);
|
|
const { tableScroll } = useTableScroll(1120);
|
|
const [form] = Form.useForm<MenuForm>();
|
|
const userInfo = useUserStore(state => state.userInfo);
|
|
const dicts = useDict('sys_show_hide', 'sys_normal_disable', 'sys_yes_no');
|
|
const [menuOptions, setMenuOptions] = useState<MenuTreeOption[]>([]);
|
|
const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
|
|
const [modalTitle, setModalTitle] = useState('');
|
|
const [cascadeOpen, { setTrue: openCascadeModal, setFalse: closeCascadeModal }] = useBoolean(false);
|
|
const [cascadeKeys, setCascadeKeys] = useState<Array<string | number>>([]);
|
|
const { loading: cascadeLoading, withLoading: withCascadeLoading } = useLoading();
|
|
|
|
const canAdd = hasPermi(userInfo, ['system:menu:add']);
|
|
const canEdit = hasPermi(userInfo, ['system:menu:edit']);
|
|
const canRemove = hasPermi(userInfo, ['system:menu:remove']);
|
|
const menuType = Form.useWatch('menuType', form) || defaultMenuForm.menuType;
|
|
const visible = Form.useWatch('visible', form) || defaultMenuForm.visible;
|
|
const treeNodes = useMemo(() => toTreeNodes(menuOptions), [menuOptions]);
|
|
|
|
const loadMenuTree = async () => {
|
|
const res = await treeselect();
|
|
setMenuOptions([{ id: 0, label: '主类目', parentId: 0, weight: 0, children: res.data || [] }]);
|
|
};
|
|
|
|
const openAdd = async (row?: MenuVO) => {
|
|
await loadMenuTree();
|
|
form.resetFields();
|
|
form.setFieldsValue({ ...defaultMenuForm, parentId: row?.menuId || 0 });
|
|
setModalTitle('添加菜单');
|
|
openModal();
|
|
};
|
|
|
|
const openEdit = async (row: MenuVO) => {
|
|
await loadMenuTree();
|
|
const res = await getMenu(row.menuId);
|
|
form.resetFields();
|
|
form.setFieldsValue(res.data);
|
|
setModalTitle('修改菜单');
|
|
openModal();
|
|
};
|
|
|
|
const submitForm = async (values: MenuForm) => {
|
|
values.menuId ? await updateMenu(values) : await addMenu(values);
|
|
message.success('操作成功');
|
|
form.resetFields();
|
|
actionRef.current?.reload();
|
|
return true;
|
|
};
|
|
|
|
const remove = async (row: MenuVO) => {
|
|
await delMenu(row.menuId);
|
|
message.success('删除成功');
|
|
actionRef.current?.reload();
|
|
};
|
|
|
|
const openCascadeDelete = async () => {
|
|
await loadMenuTree();
|
|
setCascadeKeys([]);
|
|
openCascadeModal();
|
|
};
|
|
|
|
const submitCascadeDelete = async () => {
|
|
if (!cascadeKeys.length) {
|
|
message.warning('请选择要删除的菜单');
|
|
return;
|
|
}
|
|
await withCascadeLoading(async () => {
|
|
await cascadeDelMenu(cascadeKeys);
|
|
message.success('删除成功');
|
|
closeCascadeModal();
|
|
actionRef.current?.reload();
|
|
});
|
|
};
|
|
|
|
const columns: ProColumns<MenuVO>[] = [
|
|
{
|
|
title: '菜单名称',
|
|
dataIndex: 'menuName',
|
|
width: 220,
|
|
render: (_, row) => (
|
|
<Space size={8}>
|
|
{row.icon && <span className="menu-name-icon-react">{routeIcon(row.icon)}</span>}
|
|
<span>{row.menuName}</span>
|
|
</Space>
|
|
)
|
|
},
|
|
{
|
|
title: '类型',
|
|
dataIndex: 'menuType',
|
|
search: false,
|
|
width: 90,
|
|
render: (_, row) => <Tag color={menuTypeMeta(row).color}>{menuTypeMeta(row).label}</Tag>
|
|
},
|
|
{ title: '排序', dataIndex: 'orderNum', search: false, width: 90 },
|
|
{
|
|
title: '权限标识',
|
|
dataIndex: 'perms',
|
|
search: false,
|
|
width: 200,
|
|
render: (_, row) => <EllipsisText value={row.perms} maxWidth={180} />
|
|
},
|
|
{
|
|
title: '组件路径',
|
|
dataIndex: 'component',
|
|
search: false,
|
|
width: 220,
|
|
render: (_, row) => <EllipsisText value={row.component} maxWidth={200} />
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
valueType: 'select',
|
|
fieldProps: { options: dictOptions(dicts.sys_normal_disable) },
|
|
width: 90,
|
|
render: (_, row) => <DictTag options={dicts.sys_normal_disable} value={row.status} />
|
|
},
|
|
{
|
|
title: '操作',
|
|
valueType: 'option',
|
|
width: 130,
|
|
fixed: 'right',
|
|
render: (_, row) => (
|
|
<RowActions
|
|
actions={[
|
|
canEdit && { key: 'edit', label: '修改', icon: <EditOutlined />, onClick: () => openEdit(row) },
|
|
canAdd && { key: 'add', label: '新增', icon: <PlusOutlined />, onClick: () => openAdd(row) },
|
|
canRemove && {
|
|
key: 'delete',
|
|
label: '删除',
|
|
icon: <DeleteOutlined />,
|
|
danger: true,
|
|
confirm: `是否确认删除名称为"${row.menuName}"的数据项?`,
|
|
onClick: () => remove(row)
|
|
}
|
|
]}
|
|
/>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<PageContainer title="菜单管理">
|
|
<ProTable<MenuVO, MenuQuery>
|
|
actionRef={actionRef}
|
|
rowKey="menuId"
|
|
columns={columns}
|
|
scroll={tableScroll}
|
|
pagination={false}
|
|
search={{ labelWidth: 90 }}
|
|
expandable={{ defaultExpandAllRows: false }}
|
|
request={async params => {
|
|
const res = await listMenu({ menuName: params.menuName, status: params.status });
|
|
const rows = handleTree<MenuVO>(res.data || [], 'menuId');
|
|
return { data: rows, total: rows.length, success: true };
|
|
}}
|
|
toolbar={{ title: '菜单列表' }}
|
|
toolBarRender={() => [
|
|
canAdd && (
|
|
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => openAdd()}>
|
|
新增
|
|
</Button>
|
|
),
|
|
canRemove && (
|
|
<Button key="cascade" danger icon={<DeleteOutlined />} onClick={openCascadeDelete}>
|
|
级联删除
|
|
</Button>
|
|
)
|
|
]}
|
|
/>
|
|
|
|
<ModalForm<MenuForm>
|
|
title={modalTitle}
|
|
open={modalOpen}
|
|
width={760}
|
|
form={form}
|
|
layout="vertical"
|
|
initialValues={defaultMenuForm}
|
|
modalProps={{ destroyOnHidden: true, onCancel: closeModal }}
|
|
onOpenChange={open => !open && closeModal()}
|
|
onFinish={submitForm}
|
|
>
|
|
<ProFormText name="menuId" hidden />
|
|
<ProFormTreeSelect
|
|
name="parentId"
|
|
label="上级菜单"
|
|
fieldProps={{
|
|
treeDefaultExpandAll: true,
|
|
treeData: menuOptions,
|
|
fieldNames: { label: 'label', value: 'id', children: 'children' }
|
|
}}
|
|
/>
|
|
<ProFormRadio.Group
|
|
name="menuType"
|
|
label="菜单类型"
|
|
rules={[{ required: true, message: '菜单类型不能为空' }]}
|
|
options={[
|
|
{ label: '目录', value: 'M' },
|
|
{ label: '菜单', value: 'C' },
|
|
{ label: '按钮', value: 'F' }
|
|
]}
|
|
/>
|
|
{menuType !== 'F' && (
|
|
<Form.Item name="icon" label="菜单图标">
|
|
<IconSelect />
|
|
</Form.Item>
|
|
)}
|
|
<div className="form-grid">
|
|
<ProFormText name="menuName" label="菜单名称" rules={[{ required: true, message: '菜单名称不能为空' }]} />
|
|
<ProFormDigit
|
|
name="orderNum"
|
|
label="显示排序"
|
|
min={0}
|
|
rules={[{ required: true, message: '显示排序不能为空' }]}
|
|
/>
|
|
</div>
|
|
<div className="form-grid">
|
|
{menuType !== 'F' && (
|
|
<ProFormRadio.Group name="isFrame" label="是否外链" options={dictOptions(dicts.sys_yes_no)} />
|
|
)}
|
|
{menuType !== 'F' && (
|
|
<ProFormText name="path" label="路由地址" rules={[{ required: true, message: '路由地址不能为空' }]} />
|
|
)}
|
|
{menuType === 'C' && <ProFormText name="component" label="组件路径" />}
|
|
{menuType !== 'M' && <ProFormText name="perms" label="权限字符" fieldProps={{ maxLength: 100 }} />}
|
|
{menuType === 'C' && <ProFormText name="queryParam" label="路由参数" fieldProps={{ maxLength: 255 }} />}
|
|
{menuType === 'C' && (
|
|
<ProFormRadio.Group
|
|
name="isCache"
|
|
label="是否缓存"
|
|
options={[
|
|
{ label: '缓存', value: 'Y' },
|
|
{ label: '不缓存', value: 'N' }
|
|
]}
|
|
/>
|
|
)}
|
|
{menuType !== 'F' && (
|
|
<ProFormRadio.Group name="visible" label="显示状态" options={dictOptions(dicts.sys_show_hide)} />
|
|
)}
|
|
<ProFormRadio.Group name="status" label="菜单状态" options={dictOptions(dicts.sys_normal_disable)} />
|
|
{visible !== '0' && <ProFormText name="activeMenu" label="激活路由" />}
|
|
<ProFormText name="remark" label="备注" />
|
|
</div>
|
|
</ModalForm>
|
|
|
|
<Modal
|
|
title="级联删除菜单"
|
|
open={cascadeOpen}
|
|
width={760}
|
|
confirmLoading={cascadeLoading}
|
|
onOk={submitCascadeDelete}
|
|
onCancel={closeCascadeModal}
|
|
destroyOnHidden
|
|
>
|
|
<Tree
|
|
checkable
|
|
defaultExpandAll
|
|
treeData={treeNodes}
|
|
checkedKeys={cascadeKeys}
|
|
onCheck={keys => setCascadeKeys(keys as Array<string | number>)}
|
|
/>
|
|
</Modal>
|
|
</PageContainer>
|
|
);
|
|
}
|