|
@@ -11,7 +11,7 @@ import {
|
|
|
ShoppingBag, Package, Database, PieChart, BarChart3,
|
|
ShoppingBag, Package, Database, PieChart, BarChart3,
|
|
|
Grid3x3, List, Link2, Key, Wrench, Monitor, Server
|
|
Grid3x3, List, Link2, Key, Wrench, Monitor, Server
|
|
|
} from 'lucide-react';
|
|
} from 'lucide-react';
|
|
|
-import { Modal, Input, Button, Radio, TreeSelect, Popover, Space } from 'antd';
|
|
|
|
|
|
|
+import { Modal, Form, Input, Button, Radio, TreeSelect, Popover, Space, Spin } from 'antd';
|
|
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
|
|
|
|
|
|
interface MenuNode extends Omit<MenuVO, 'id'>, Omit<TreeNode, 'id'> {
|
|
interface MenuNode extends Omit<MenuVO, 'id'>, Omit<TreeNode, 'id'> {
|
|
@@ -32,48 +32,53 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
|
|
|
const [dialogTitle, setDialogTitle] = useState('');
|
|
const [dialogTitle, setDialogTitle] = useState('');
|
|
|
const [formType, setFormType] = useState<'create' | 'update'>('create');
|
|
const [formType, setFormType] = useState<'create' | 'update'>('create');
|
|
|
const [formLoading, setFormLoading] = useState(false);
|
|
const [formLoading, setFormLoading] = useState(false);
|
|
|
|
|
+ const [currentId, setCurrentId] = useState<number | undefined>();
|
|
|
const [menuTree, setMenuTree] = useState<MenuNode[]>([]);
|
|
const [menuTree, setMenuTree] = useState<MenuNode[]>([]);
|
|
|
const [iconPickerOpen, setIconPickerOpen] = useState(false);
|
|
const [iconPickerOpen, setIconPickerOpen] = useState(false);
|
|
|
- const [formData, setFormData] = useState<MenuVO>({
|
|
|
|
|
- name: '',
|
|
|
|
|
- permission: '',
|
|
|
|
|
- type: MenuType.DIR,
|
|
|
|
|
- sort: 0,
|
|
|
|
|
- parentId: 0,
|
|
|
|
|
- path: '',
|
|
|
|
|
- icon: '',
|
|
|
|
|
- component: '',
|
|
|
|
|
- componentName: '',
|
|
|
|
|
- status: 0, // 0 是开启,1 是关闭
|
|
|
|
|
- visible: true,
|
|
|
|
|
- keepAlive: true,
|
|
|
|
|
- alwaysShow: true,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const [form] = Form.useForm();
|
|
|
|
|
+
|
|
|
|
|
+ const [pendingOpen, setPendingOpen] = useState<{ type: string; id?: number; parentId?: number } | null>(null);
|
|
|
|
|
|
|
|
// 暴露方法给父组件
|
|
// 暴露方法给父组件
|
|
|
useImperativeHandle(ref, () => ({
|
|
useImperativeHandle(ref, () => ({
|
|
|
open: (type: string, id?: number, parentId?: number) => {
|
|
open: (type: string, id?: number, parentId?: number) => {
|
|
|
setDialogTitle(type === 'create' ? '新增菜单' : '修改菜单');
|
|
setDialogTitle(type === 'create' ? '新增菜单' : '修改菜单');
|
|
|
setFormType(type as 'create' | 'update');
|
|
setFormType(type as 'create' | 'update');
|
|
|
- resetForm();
|
|
|
|
|
-
|
|
|
|
|
- if (parentId) {
|
|
|
|
|
- setFormData(prev => ({ ...prev, parentId }));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ setCurrentId(id);
|
|
|
|
|
+ setPendingOpen({ type, id, parentId });
|
|
|
|
|
+ setDialogVisible(true);
|
|
|
|
|
+ getTree();
|
|
|
|
|
+ },
|
|
|
|
|
+ }));
|
|
|
|
|
|
|
|
- if (id) {
|
|
|
|
|
- loadMenuData(id);
|
|
|
|
|
|
|
+ // 当 Modal 打开时,设置表单值
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (dialogVisible && pendingOpen) {
|
|
|
|
|
+ form.resetFields();
|
|
|
|
|
+
|
|
|
|
|
+ if (pendingOpen.id) {
|
|
|
|
|
+ loadMenuData(pendingOpen.id);
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ form.setFieldsValue({
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ permission: '',
|
|
|
|
|
+ type: MenuType.DIR,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ parentId: pendingOpen.parentId ?? 0,
|
|
|
|
|
+ path: '',
|
|
|
|
|
+ icon: '',
|
|
|
|
|
+ component: '',
|
|
|
|
|
+ componentName: '',
|
|
|
|
|
+ status: 0,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ keepAlive: true,
|
|
|
|
|
+ alwaysShow: true,
|
|
|
|
|
+ });
|
|
|
setFormLoading(false);
|
|
setFormLoading(false);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 获取菜单树
|
|
|
|
|
- getTree();
|
|
|
|
|
-
|
|
|
|
|
- // 设置 dialogVisible 为 true
|
|
|
|
|
- setDialogVisible(true);
|
|
|
|
|
- },
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ setPendingOpen(null);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [dialogVisible, pendingOpen]);
|
|
|
|
|
|
|
|
// 组件挂载时获取菜单树
|
|
// 组件挂载时获取菜单树
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -112,11 +117,21 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
|
|
|
try {
|
|
try {
|
|
|
const res = await menuApi.getMenu(id);
|
|
const res = await menuApi.getMenu(id);
|
|
|
const data = (res as any)?.data || res;
|
|
const data = (res as any)?.data || res;
|
|
|
- // 确保 status 是数字类型,用于正确回显
|
|
|
|
|
- setFormData({
|
|
|
|
|
- ...data,
|
|
|
|
|
- status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? 0) // 0 是开启,1 是关闭
|
|
|
|
|
- } as MenuVO);
|
|
|
|
|
|
|
+ form.setFieldsValue({
|
|
|
|
|
+ name: data.name,
|
|
|
|
|
+ permission: data.permission || '',
|
|
|
|
|
+ type: data.type,
|
|
|
|
|
+ sort: data.sort ?? 0,
|
|
|
|
|
+ parentId: data.parentId ?? 0,
|
|
|
|
|
+ path: data.path || '',
|
|
|
|
|
+ icon: data.icon || '',
|
|
|
|
|
+ component: data.component || '',
|
|
|
|
|
+ componentName: data.componentName || '',
|
|
|
|
|
+ status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? 0),
|
|
|
|
|
+ visible: data.visible ?? true,
|
|
|
|
|
+ keepAlive: data.keepAlive ?? true,
|
|
|
|
|
+ alwaysShow: data.alwaysShow ?? true,
|
|
|
|
|
+ });
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
toast.error(error.message || '获取菜单详情失败');
|
|
toast.error(error.message || '获取菜单详情失败');
|
|
|
} finally {
|
|
} finally {
|
|
@@ -124,25 +139,6 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 重置表单
|
|
|
|
|
- const resetForm = () => {
|
|
|
|
|
- setFormData({
|
|
|
|
|
- name: '',
|
|
|
|
|
- permission: '',
|
|
|
|
|
- type: MenuType.DIR,
|
|
|
|
|
- sort: 0,
|
|
|
|
|
- parentId: 0,
|
|
|
|
|
- path: '',
|
|
|
|
|
- icon: '',
|
|
|
|
|
- component: '',
|
|
|
|
|
- componentName: '',
|
|
|
|
|
- status: 0, // 0 是开启,1 是关闭
|
|
|
|
|
- visible: true,
|
|
|
|
|
- keepAlive: true,
|
|
|
|
|
- alwaysShow: true,
|
|
|
|
|
- });
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
// 判断是否是外部链接
|
|
// 判断是否是外部链接
|
|
|
const isExternal = (path: string) => {
|
|
const isExternal = (path: string) => {
|
|
|
return /^(https?:|mailto:|tel:)/.test(path);
|
|
return /^(https?:|mailto:|tel:)/.test(path);
|
|
@@ -150,53 +146,38 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
|
|
|
|
|
|
|
|
// 提交表单
|
|
// 提交表单
|
|
|
const submitForm = async () => {
|
|
const submitForm = async () => {
|
|
|
- // 验证必填字段
|
|
|
|
|
- if (!formData.name) {
|
|
|
|
|
- toast.error('菜单名称不能为空');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (!formData.type) {
|
|
|
|
|
- toast.error('菜单类型不能为空');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (formData.sort === undefined || formData.sort === null) {
|
|
|
|
|
- toast.error('菜单顺序不能为空');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (formData.type !== MenuType.BUTTON && !formData.path) {
|
|
|
|
|
- toast.error('路由地址不能为空');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (formData.status === undefined || formData.status === null) {
|
|
|
|
|
- toast.error('菜单状态不能为空');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 验证路径格式
|
|
|
|
|
- if (formData.type === MenuType.DIR || formData.type === MenuType.MENU) {
|
|
|
|
|
- if (!isExternal(formData.path)) {
|
|
|
|
|
- if (formData.parentId === 0 && formData.path.charAt(0) !== '/') {
|
|
|
|
|
- toast.error('路径必须以 / 开头');
|
|
|
|
|
- return;
|
|
|
|
|
- } else if (formData.parentId !== 0 && formData.path.charAt(0) === '/') {
|
|
|
|
|
- toast.error('路径不能以 / 开头');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- setFormLoading(true);
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ const values = await form.validateFields();
|
|
|
|
|
+ setFormLoading(true);
|
|
|
|
|
+
|
|
|
|
|
+ const data: MenuVO = {
|
|
|
|
|
+ ...values,
|
|
|
|
|
+ type: values.type,
|
|
|
|
|
+ sort: values.sort ?? 0,
|
|
|
|
|
+ parentId: values.parentId ?? 0,
|
|
|
|
|
+ status: values.status ?? 0,
|
|
|
|
|
+ visible: values.visible ?? true,
|
|
|
|
|
+ keepAlive: values.keepAlive ?? true,
|
|
|
|
|
+ alwaysShow: values.alwaysShow ?? true,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
if (formType === 'create') {
|
|
if (formType === 'create') {
|
|
|
- await menuApi.createMenu(formData);
|
|
|
|
|
|
|
+ await menuApi.createMenu(data);
|
|
|
toast.success('创建成功');
|
|
toast.success('创建成功');
|
|
|
} else {
|
|
} else {
|
|
|
- await menuApi.updateMenu(formData);
|
|
|
|
|
|
|
+ if (currentId) {
|
|
|
|
|
+ data.id = currentId;
|
|
|
|
|
+ }
|
|
|
|
|
+ await menuApi.updateMenu(data);
|
|
|
toast.success('更新成功');
|
|
toast.success('更新成功');
|
|
|
}
|
|
}
|
|
|
setDialogVisible(false);
|
|
setDialogVisible(false);
|
|
|
onSuccess?.();
|
|
onSuccess?.();
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
|
|
+ if (error.errorFields) {
|
|
|
|
|
+ // 表单验证错误
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
toast.error(error.message || '操作失败');
|
|
toast.error(error.message || '操作失败');
|
|
|
} finally {
|
|
} finally {
|
|
|
setFormLoading(false);
|
|
setFormLoading(false);
|
|
@@ -260,7 +241,7 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
|
|
|
title: node.name,
|
|
title: node.name,
|
|
|
value: node.id,
|
|
value: node.id,
|
|
|
key: node.id,
|
|
key: node.id,
|
|
|
- disabled: node.id === formData.id,
|
|
|
|
|
|
|
+ disabled: node.id === currentId,
|
|
|
children: node.children ? convertToTreeSelectData(node.children) : undefined,
|
|
children: node.children ? convertToTreeSelectData(node.children) : undefined,
|
|
|
}));
|
|
}));
|
|
|
};
|
|
};
|
|
@@ -269,295 +250,302 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
|
|
|
<Modal
|
|
<Modal
|
|
|
title={dialogTitle}
|
|
title={dialogTitle}
|
|
|
open={dialogVisible}
|
|
open={dialogVisible}
|
|
|
- onCancel={() => setDialogVisible(false)}
|
|
|
|
|
|
|
+ onCancel={() => {
|
|
|
|
|
+ setDialogVisible(false);
|
|
|
|
|
+ setPendingOpen(null);
|
|
|
|
|
+ }}
|
|
|
|
|
+ onOk={submitForm}
|
|
|
|
|
+ confirmLoading={formLoading}
|
|
|
width={600}
|
|
width={600}
|
|
|
- footer={[
|
|
|
|
|
- <Button key="cancel" onClick={() => setDialogVisible(false)}>
|
|
|
|
|
- 取消
|
|
|
|
|
- </Button>,
|
|
|
|
|
- <Button
|
|
|
|
|
- key="submit"
|
|
|
|
|
- type="primary"
|
|
|
|
|
- loading={formLoading}
|
|
|
|
|
- onClick={submitForm}
|
|
|
|
|
- >
|
|
|
|
|
- 确定
|
|
|
|
|
- </Button>,
|
|
|
|
|
- ]}
|
|
|
|
|
destroyOnClose
|
|
destroyOnClose
|
|
|
>
|
|
>
|
|
|
- {formLoading && formType === 'update' ? (
|
|
|
|
|
- <div className="py-6 text-center text-gray-500">加载中...</div>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <div style={{ padding: '8px 0' }}>
|
|
|
|
|
- {/* 上级菜单 */}
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>上级菜单</span>
|
|
|
|
|
|
|
+ <Spin spinning={formLoading && formType === 'update'}>
|
|
|
|
|
+ <Form
|
|
|
|
|
+ form={form}
|
|
|
|
|
+ layout="horizontal"
|
|
|
|
|
+ labelCol={{ span: 4 }}
|
|
|
|
|
+ wrapperCol={{ span: 20 }}
|
|
|
|
|
+ initialValues={{
|
|
|
|
|
+ type: MenuType.DIR,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ parentId: 0,
|
|
|
|
|
+ status: 0,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ keepAlive: true,
|
|
|
|
|
+ alwaysShow: true,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label="上级菜单"
|
|
|
|
|
+ name="parentId"
|
|
|
|
|
+ >
|
|
|
<TreeSelect
|
|
<TreeSelect
|
|
|
- style={{ flex: 1 }}
|
|
|
|
|
- value={formData.parentId || 0}
|
|
|
|
|
treeData={convertToTreeSelectData(menuTree)}
|
|
treeData={convertToTreeSelectData(menuTree)}
|
|
|
placeholder="请选择上级菜单"
|
|
placeholder="请选择上级菜单"
|
|
|
- onChange={(value) => setFormData(prev => ({ ...prev, parentId: value as number }))}
|
|
|
|
|
disabled={!menuTree || menuTree.length === 0}
|
|
disabled={!menuTree || menuTree.length === 0}
|
|
|
treeDefaultExpandAll
|
|
treeDefaultExpandAll
|
|
|
/>
|
|
/>
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 菜单名称 */}
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
- 菜单名称
|
|
|
|
|
- </span>
|
|
|
|
|
- <Input
|
|
|
|
|
- style={{ flex: 1 }}
|
|
|
|
|
- value={formData.name}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
|
|
|
- placeholder="请输入菜单名称"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 菜单类型 */}
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
- 菜单类型
|
|
|
|
|
- </span>
|
|
|
|
|
- <Radio.Group
|
|
|
|
|
- value={formData.type}
|
|
|
|
|
- onChange={(e) => {
|
|
|
|
|
- setFormData(prev => ({ ...prev, type: e.target.value }));
|
|
|
|
|
- }}
|
|
|
|
|
- buttonStyle="solid"
|
|
|
|
|
- optionType="button"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label="菜单名称"
|
|
|
|
|
+ name="name"
|
|
|
|
|
+ rules={[
|
|
|
|
|
+ { required: true, message: '请输入菜单名称' },
|
|
|
|
|
+ { max: 50, message: '菜单名称不能超过50个字符' },
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入菜单名称" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label="菜单类型"
|
|
|
|
|
+ name="type"
|
|
|
|
|
+ rules={[{ required: true, message: '请选择菜单类型' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Radio.Group buttonStyle="solid" optionType="button">
|
|
|
<Radio.Button value={MenuType.DIR}>目录</Radio.Button>
|
|
<Radio.Button value={MenuType.DIR}>目录</Radio.Button>
|
|
|
<Radio.Button value={MenuType.MENU}>菜单</Radio.Button>
|
|
<Radio.Button value={MenuType.MENU}>菜单</Radio.Button>
|
|
|
<Radio.Button value={MenuType.BUTTON}>按钮</Radio.Button>
|
|
<Radio.Button value={MenuType.BUTTON}>按钮</Radio.Button>
|
|
|
</Radio.Group>
|
|
</Radio.Group>
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 菜单图标 */}
|
|
|
|
|
- {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>菜单图标</span>
|
|
|
|
|
- <Space.Compact style={{ flex: 1 }}>
|
|
|
|
|
- <Input
|
|
|
|
|
- value={formData.icon}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, icon: e.target.value }))}
|
|
|
|
|
- placeholder="请选择或输入图标名称(如:ep:document)"
|
|
|
|
|
- />
|
|
|
|
|
- <Popover
|
|
|
|
|
- content={
|
|
|
|
|
- <div style={{ width: 400 }}>
|
|
|
|
|
- <h4 style={{ marginBottom: 8, fontSize: 14, fontWeight: 600 }}>选择图标</h4>
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- display: 'grid',
|
|
|
|
|
- gridTemplateColumns: 'repeat(8, 1fr)',
|
|
|
|
|
- gap: 6,
|
|
|
|
|
- maxHeight: 300,
|
|
|
|
|
- overflowY: 'auto'
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- {elementPlusIcons.map((iconItem) => {
|
|
|
|
|
- const IconComponent = iconItem.icon;
|
|
|
|
|
- const isSelected = formData.icon === iconItem.name;
|
|
|
|
|
- return (
|
|
|
|
|
- <div
|
|
|
|
|
- key={iconItem.name}
|
|
|
|
|
- onClick={() => {
|
|
|
|
|
- setFormData(prev => ({ ...prev, icon: iconItem.name }));
|
|
|
|
|
- setIconPickerOpen(false);
|
|
|
|
|
- }}
|
|
|
|
|
- style={{
|
|
|
|
|
- display: 'flex',
|
|
|
|
|
- alignItems: 'center',
|
|
|
|
|
- justifyContent: 'center',
|
|
|
|
|
- padding: 6,
|
|
|
|
|
- borderRadius: 4,
|
|
|
|
|
- cursor: 'pointer',
|
|
|
|
|
- border: `1px solid ${isSelected ? '#1890ff' : '#d9d9d9'}`,
|
|
|
|
|
- backgroundColor: isSelected ? '#e6f7ff' : '#fff',
|
|
|
|
|
- aspectRatio: '1'
|
|
|
|
|
- }}
|
|
|
|
|
- title={`${iconItem.name} - ${iconItem.label}`}
|
|
|
|
|
- >
|
|
|
|
|
- <IconComponent
|
|
|
|
|
- style={{
|
|
|
|
|
- width: 14,
|
|
|
|
|
- height: 14,
|
|
|
|
|
- color: isSelected ? '#1890ff' : '#666'
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ noStyle
|
|
|
|
|
+ shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ getFieldValue }) =>
|
|
|
|
|
+ getFieldValue('type') !== MenuType.BUTTON ? (
|
|
|
|
|
+ <Form.Item label="菜单图标" name="icon">
|
|
|
|
|
+ <Space.Compact style={{ width: '100%' }}>
|
|
|
|
|
+ <Input placeholder="请选择或输入图标名称(如:ep:document)" />
|
|
|
|
|
+ <Popover
|
|
|
|
|
+ content={
|
|
|
|
|
+ <div style={{ width: 400 }}>
|
|
|
|
|
+ <h4 style={{ marginBottom: 8, fontSize: 14, fontWeight: 600 }}>选择图标</h4>
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ display: 'grid',
|
|
|
|
|
+ gridTemplateColumns: 'repeat(8, 1fr)',
|
|
|
|
|
+ gap: 6,
|
|
|
|
|
+ maxHeight: 300,
|
|
|
|
|
+ overflowY: 'auto'
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {elementPlusIcons.map((iconItem) => {
|
|
|
|
|
+ const IconComponent = iconItem.icon;
|
|
|
|
|
+ const currentIcon = form.getFieldValue('icon');
|
|
|
|
|
+ const isSelected = currentIcon === iconItem.name;
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={iconItem.name}
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ form.setFieldsValue({ icon: iconItem.name });
|
|
|
|
|
+ setIconPickerOpen(false);
|
|
|
|
|
+ }}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ padding: 6,
|
|
|
|
|
+ borderRadius: 4,
|
|
|
|
|
+ cursor: 'pointer',
|
|
|
|
|
+ border: `1px solid ${isSelected ? '#1890ff' : '#d9d9d9'}`,
|
|
|
|
|
+ backgroundColor: isSelected ? '#e6f7ff' : '#fff',
|
|
|
|
|
+ aspectRatio: '1'
|
|
|
|
|
+ }}
|
|
|
|
|
+ title={`${iconItem.name} - ${iconItem.label}`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <IconComponent
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: 14,
|
|
|
|
|
+ height: 14,
|
|
|
|
|
+ color: isSelected ? '#1890ff' : '#666'
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ title="选择图标"
|
|
|
|
|
+ trigger="click"
|
|
|
|
|
+ open={iconPickerOpen}
|
|
|
|
|
+ onOpenChange={setIconPickerOpen}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Button type="primary">选择图标</Button>
|
|
|
|
|
+ </Popover>
|
|
|
|
|
+ </Space.Compact>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ }
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ noStyle
|
|
|
|
|
+ shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ getFieldValue }) =>
|
|
|
|
|
+ getFieldValue('type') !== MenuType.BUTTON ? (
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label={
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 路由地址
|
|
|
|
|
+ <QuestionCircleOutlined style={{ fontSize: 14, color: '#999', marginLeft: 4 }} />
|
|
|
|
|
+ </span>
|
|
|
}
|
|
}
|
|
|
- title="选择图标"
|
|
|
|
|
- trigger="click"
|
|
|
|
|
- open={iconPickerOpen}
|
|
|
|
|
- onOpenChange={setIconPickerOpen}
|
|
|
|
|
|
|
+ name="path"
|
|
|
|
|
+ rules={[
|
|
|
|
|
+ { required: true, message: '请输入路由地址' },
|
|
|
|
|
+ ({ getFieldValue }) => ({
|
|
|
|
|
+ validator(_, value) {
|
|
|
|
|
+ if (!value) {
|
|
|
|
|
+ return Promise.resolve();
|
|
|
|
|
+ }
|
|
|
|
|
+ const parentId = getFieldValue('parentId');
|
|
|
|
|
+ const isExternal = /^(https?:|mailto:|tel:)/.test(value);
|
|
|
|
|
+ if (!isExternal) {
|
|
|
|
|
+ if (parentId === 0 && value.charAt(0) !== '/') {
|
|
|
|
|
+ return Promise.reject(new Error('路径必须以 / 开头'));
|
|
|
|
|
+ } else if (parentId !== 0 && value.charAt(0) === '/') {
|
|
|
|
|
+ return Promise.reject(new Error('路径不能以 / 开头'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Promise.resolve();
|
|
|
|
|
+ },
|
|
|
|
|
+ }),
|
|
|
|
|
+ ]}
|
|
|
>
|
|
>
|
|
|
- <Button type="primary">选择图标</Button>
|
|
|
|
|
- </Popover>
|
|
|
|
|
- </Space.Compact>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 路由地址 */}
|
|
|
|
|
- {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700 flex items-center gap-1" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- <span className="text-red-500">*</span>
|
|
|
|
|
- 路由地址
|
|
|
|
|
- <QuestionCircleOutlined style={{ fontSize: 14, color: '#999' }} />
|
|
|
|
|
- </span>
|
|
|
|
|
- <Input
|
|
|
|
|
- style={{ flex: 1 }}
|
|
|
|
|
- value={formData.path}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, path: e.target.value }))}
|
|
|
|
|
- placeholder="请输入路由地址"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 组件地址 */}
|
|
|
|
|
- {formData.type === MenuType.MENU && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>组件地址</span>
|
|
|
|
|
- <Input
|
|
|
|
|
- style={{ flex: 1 }}
|
|
|
|
|
- value={formData.component}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, component: e.target.value }))}
|
|
|
|
|
- placeholder="例如:system/user/index"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 组件名字 */}
|
|
|
|
|
- {formData.type === MenuType.MENU && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>组件名字</span>
|
|
|
|
|
- <Input
|
|
|
|
|
- style={{ flex: 1 }}
|
|
|
|
|
- value={formData.componentName || ''}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, componentName: e.target.value }))}
|
|
|
|
|
- placeholder="例如:SystemUser"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 权限标识 */}
|
|
|
|
|
- {formData.type !== MenuType.DIR && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700 flex items-center gap-1" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- 权限标识
|
|
|
|
|
- <QuestionCircleOutlined style={{ fontSize: 14, color: '#999' }} />
|
|
|
|
|
- </span>
|
|
|
|
|
- <Input
|
|
|
|
|
- style={{ flex: 1 }}
|
|
|
|
|
- value={formData.permission}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, permission: e.target.value }))}
|
|
|
|
|
- placeholder="请输入权限标识"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 显示排序 */}
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
- 显示排序
|
|
|
|
|
- </span>
|
|
|
|
|
- <Input
|
|
|
|
|
- style={{ flex: 1 }}
|
|
|
|
|
- type="number"
|
|
|
|
|
- value={formData.sort}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, sort: Number(e.target.value) || 0 }))}
|
|
|
|
|
- placeholder="请输入排序号"
|
|
|
|
|
- min={0}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 菜单状态 */}
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- <span className="text-red-500 mr-1">*</span>
|
|
|
|
|
- 菜单状态
|
|
|
|
|
- </span>
|
|
|
|
|
- <Radio.Group
|
|
|
|
|
- value={Number(formData.status)}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, status: Number(e.target.value) }))}
|
|
|
|
|
- buttonStyle="solid"
|
|
|
|
|
- optionType="button"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <Input placeholder="请输入路由地址" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ }
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ noStyle
|
|
|
|
|
+ shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ getFieldValue }) =>
|
|
|
|
|
+ getFieldValue('type') === MenuType.MENU ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Form.Item label="组件地址" name="component">
|
|
|
|
|
+ <Input placeholder="例如:system/user/index" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item label="组件名字" name="componentName">
|
|
|
|
|
+ <Input placeholder="例如:SystemUser" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ }
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ noStyle
|
|
|
|
|
+ shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ getFieldValue }) =>
|
|
|
|
|
+ getFieldValue('type') !== MenuType.DIR ? (
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label={
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 权限标识
|
|
|
|
|
+ <QuestionCircleOutlined style={{ fontSize: 14, color: '#999', marginLeft: 4 }} />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ }
|
|
|
|
|
+ name="permission"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入权限标识" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ }
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label="显示排序"
|
|
|
|
|
+ name="sort"
|
|
|
|
|
+ rules={[{ required: true, message: '请输入显示排序' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input type="number" placeholder="请输入排序号" min={0} />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label="菜单状态"
|
|
|
|
|
+ name="status"
|
|
|
|
|
+ rules={[{ required: true, message: '请选择菜单状态' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Radio.Group buttonStyle="solid" optionType="button">
|
|
|
<Radio.Button value={0}>开启</Radio.Button>
|
|
<Radio.Button value={0}>开启</Radio.Button>
|
|
|
<Radio.Button value={1}>关闭</Radio.Button>
|
|
<Radio.Button value={1}>关闭</Radio.Button>
|
|
|
</Radio.Group>
|
|
</Radio.Group>
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 显示状态 */}
|
|
|
|
|
- {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700 flex items-center gap-1" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- 显示状态
|
|
|
|
|
- <QuestionCircleOutlined style={{ fontSize: 14, color: '#999' }} />
|
|
|
|
|
- </span>
|
|
|
|
|
- <Radio.Group
|
|
|
|
|
- value={formData.visible}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, visible: e.target.value as boolean }))}
|
|
|
|
|
- buttonStyle="solid"
|
|
|
|
|
- optionType="button"
|
|
|
|
|
- >
|
|
|
|
|
- <Radio.Button value={true}>显示</Radio.Button>
|
|
|
|
|
- <Radio.Button value={false}>隐藏</Radio.Button>
|
|
|
|
|
- </Radio.Group>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 总是显示 */}
|
|
|
|
|
- {formData.type !== MenuType.BUTTON && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700 flex items-center gap-1" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- 总是显示
|
|
|
|
|
- <QuestionCircleOutlined style={{ fontSize: 14, color: '#999' }} />
|
|
|
|
|
- </span>
|
|
|
|
|
- <Radio.Group
|
|
|
|
|
- value={formData.alwaysShow ?? true}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, alwaysShow: e.target.value as boolean }))}
|
|
|
|
|
- buttonStyle="solid"
|
|
|
|
|
- optionType="button"
|
|
|
|
|
- >
|
|
|
|
|
- <Radio.Button value={true}>总是</Radio.Button>
|
|
|
|
|
- <Radio.Button value={false}>不是</Radio.Button>
|
|
|
|
|
- </Radio.Group>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 缓存状态 */}
|
|
|
|
|
- {formData.type === MenuType.MENU && (
|
|
|
|
|
- <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
- <span className="text-sm text-gray-700 flex items-center gap-1" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0 }}>
|
|
|
|
|
- 缓存状态
|
|
|
|
|
- <QuestionCircleOutlined style={{ fontSize: 14, color: '#999' }} />
|
|
|
|
|
- </span>
|
|
|
|
|
- <Radio.Group
|
|
|
|
|
- value={formData.keepAlive ?? true}
|
|
|
|
|
- onChange={(e) => setFormData(prev => ({ ...prev, keepAlive: e.target.value as boolean }))}
|
|
|
|
|
- buttonStyle="solid"
|
|
|
|
|
- optionType="button"
|
|
|
|
|
- >
|
|
|
|
|
- <Radio.Button value={true}>缓存</Radio.Button>
|
|
|
|
|
- <Radio.Button value={false}>不缓存</Radio.Button>
|
|
|
|
|
- </Radio.Group>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ noStyle
|
|
|
|
|
+ shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ getFieldValue }) =>
|
|
|
|
|
+ getFieldValue('type') !== MenuType.BUTTON ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label={
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 显示状态
|
|
|
|
|
+ <QuestionCircleOutlined style={{ fontSize: 14, color: '#999', marginLeft: 4 }} />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ }
|
|
|
|
|
+ name="visible"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Radio.Group buttonStyle="solid" optionType="button">
|
|
|
|
|
+ <Radio.Button value={true}>显示</Radio.Button>
|
|
|
|
|
+ <Radio.Button value={false}>隐藏</Radio.Button>
|
|
|
|
|
+ </Radio.Group>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label={
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 总是显示
|
|
|
|
|
+ <QuestionCircleOutlined style={{ fontSize: 14, color: '#999', marginLeft: 4 }} />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ }
|
|
|
|
|
+ name="alwaysShow"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Radio.Group buttonStyle="solid" optionType="button">
|
|
|
|
|
+ <Radio.Button value={true}>总是</Radio.Button>
|
|
|
|
|
+ <Radio.Button value={false}>不是</Radio.Button>
|
|
|
|
|
+ </Radio.Group>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ }
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ noStyle
|
|
|
|
|
+ shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
|
|
|
|
|
+ >
|
|
|
|
|
+ {({ getFieldValue }) =>
|
|
|
|
|
+ getFieldValue('type') === MenuType.MENU ? (
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ label={
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 缓存状态
|
|
|
|
|
+ <QuestionCircleOutlined style={{ fontSize: 14, color: '#999', marginLeft: 4 }} />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ }
|
|
|
|
|
+ name="keepAlive"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Radio.Group buttonStyle="solid" optionType="button">
|
|
|
|
|
+ <Radio.Button value={true}>缓存</Radio.Button>
|
|
|
|
|
+ <Radio.Button value={false}>不缓存</Radio.Button>
|
|
|
|
|
+ </Radio.Group>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ }
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </Spin>
|
|
|
</Modal>
|
|
</Modal>
|
|
|
);
|
|
);
|
|
|
});
|
|
});
|
|
@@ -566,3 +554,4 @@ MenuForm.displayName = 'MenuForm';
|
|
|
|
|
|
|
|
export default MenuForm;
|
|
export default MenuForm;
|
|
|
|
|
|
|
|
|
|
+
|