Parcourir la source

修改菜单管理和岗位管理新增修改表单必填样式。
部门管理表格修改图标颜色修改

wyn il y a 5 mois
Parent
commit
2cbb647806
3 fichiers modifiés avec 447 ajouts et 504 suppressions
  1. 2 2
      src/components/DepartmentManagement.tsx
  2. 354 365
      src/components/MenuForm.tsx
  3. 91 137
      src/components/PostForm.tsx

+ 2 - 2
src/components/DepartmentManagement.tsx

@@ -169,7 +169,7 @@ export default function DepartmentManagement() {
                     onClick={() => openForm('update', node.id)}
                     className="h-8 px-2"
                   >
-                    <Edit2 className="w-4 h-4 text-blue-500" />
+                    <Edit2 className="w-4 h-4" />
                   </Button>
                 </PermissionWrapper>
                 <PermissionWrapper permission="system:dept:delete">
@@ -179,7 +179,7 @@ export default function DepartmentManagement() {
                     onClick={() => handleDelete(node.id)}
                     className="h-8 px-2 text-red-600 hover:text-red-700"
                   >
-                    <Trash2 className="w-4 h-4 text-red-500"/>
+                    <Trash2 className="w-4 h-4"/>
                   </Button>
                 </PermissionWrapper>
               </div>

+ 354 - 365
src/components/MenuForm.tsx

@@ -11,7 +11,7 @@ import {
   ShoppingBag, Package, Database, PieChart, BarChart3, 
   Grid3x3, List, Link2, Key, Wrench, Monitor, Server
 } 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';
 
 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 [formType, setFormType] = useState<'create' | 'update'>('create');
   const [formLoading, setFormLoading] = useState(false);
+  const [currentId, setCurrentId] = useState<number | undefined>();
   const [menuTree, setMenuTree] = useState<MenuNode[]>([]);
   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, () => ({
     open: (type: string, id?: number, parentId?: number) => {
       setDialogTitle(type === 'create' ? '新增菜单' : '修改菜单');
       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 {
+        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);
       }
-
-      // 获取菜单树
-      getTree();
-      
-      // 设置 dialogVisible 为 true
-      setDialogVisible(true);
-    },
-  }));
+      setPendingOpen(null);
+    }
+  }, [dialogVisible, pendingOpen]);
 
   // 组件挂载时获取菜单树
   useEffect(() => {
@@ -112,11 +117,21 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
     try {
       const res = await menuApi.getMenu(id);
       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) {
       toast.error(error.message || '获取菜单详情失败');
     } 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) => {
     return /^(https?:|mailto:|tel:)/.test(path);
@@ -150,53 +146,38 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
 
   // 提交表单
   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 {
+      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') {
-        await menuApi.createMenu(formData);
+        await menuApi.createMenu(data);
         toast.success('创建成功');
       } else {
-        await menuApi.updateMenu(formData);
+        if (currentId) {
+          data.id = currentId;
+        }
+        await menuApi.updateMenu(data);
         toast.success('更新成功');
       }
       setDialogVisible(false);
       onSuccess?.();
     } catch (error: any) {
+      if (error.errorFields) {
+        // 表单验证错误
+        return;
+      }
       toast.error(error.message || '操作失败');
     } finally {
       setFormLoading(false);
@@ -260,7 +241,7 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
       title: node.name,
       value: node.id,
       key: node.id,
-      disabled: node.id === formData.id,
+      disabled: node.id === currentId,
       children: node.children ? convertToTreeSelectData(node.children) : undefined,
     }));
   };
@@ -269,295 +250,302 @@ const MenuForm = forwardRef<MenuFormRef, MenuFormProps>(({ onSuccess }, ref) =>
     <Modal
       title={dialogTitle}
       open={dialogVisible}
-      onCancel={() => setDialogVisible(false)}
+      onCancel={() => {
+        setDialogVisible(false);
+        setPendingOpen(null);
+      }}
+      onOk={submitForm}
+      confirmLoading={formLoading}
       width={600}
-      footer={[
-        <Button key="cancel" onClick={() => setDialogVisible(false)}>
-          取消
-        </Button>,
-        <Button 
-          key="submit" 
-          type="primary" 
-          loading={formLoading}
-          onClick={submitForm}
-        >
-          确定
-        </Button>,
-      ]}
       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
-              style={{ flex: 1 }}
-              value={formData.parentId || 0}
               treeData={convertToTreeSelectData(menuTree)}
               placeholder="请选择上级菜单"
-              onChange={(value) => setFormData(prev => ({ ...prev, parentId: value as number }))}
               disabled={!menuTree || menuTree.length === 0}
               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.MENU}>菜单</Radio.Button>
               <Radio.Button value={MenuType.BUTTON}>按钮</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" 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={1}>关闭</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.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>
   );
 });
@@ -566,3 +554,4 @@ MenuForm.displayName = 'MenuForm';
 
 export default MenuForm;
 
+

+ 91 - 137
src/components/PostForm.tsx

@@ -1,7 +1,7 @@
 import React, { useState, useImperativeHandle, forwardRef } from 'react';
 import { postApi, PostVO, PostStatus } from '../api/Post';
 import { toast } from 'sonner';
-import { Modal, Input, Button, Select, Space } from 'antd';
+import { Modal, Form, Input, Button, Select, Spin } from 'antd';
 
 interface PostFormProps {
   onSuccess?: () => void;
@@ -16,24 +16,27 @@ const PostForm = forwardRef<PostFormRef, PostFormProps>(({ onSuccess }, ref) =>
   const [dialogTitle, setDialogTitle] = useState('');
   const [formType, setFormType] = useState<'create' | 'update'>('create');
   const [formLoading, setFormLoading] = useState(false);
-  const [formData, setFormData] = useState<PostVO>({
-    name: '',
-    code: '',
-    sort: 0,
-    status: PostStatus.ENABLE,
-    remark: '',
-  });
+  const [currentId, setCurrentId] = useState<number | undefined>();
+  const [form] = Form.useForm();
 
   // 暴露方法给父组件
   useImperativeHandle(ref, () => ({
     open: (type: string, id?: number) => {
       setDialogTitle(type === 'create' ? '新增岗位' : '修改岗位');
       setFormType(type as 'create' | 'update');
-      resetForm();
+      setCurrentId(id);
+      form.resetFields();
 
       if (id) {
         loadPostData(id);
       } else {
+        form.setFieldsValue({
+          name: '',
+          code: '',
+          sort: 0,
+          status: PostStatus.ENABLE,
+          remark: '',
+        });
         setFormLoading(false);
       }
 
@@ -47,10 +50,13 @@ const PostForm = forwardRef<PostFormRef, PostFormProps>(({ onSuccess }, ref) =>
     try {
       const res = await postApi.getPost(id);
       const data = (res as any)?.data || res;
-      setFormData({
-        ...data,
+      form.setFieldsValue({
+        name: data.name,
+        code: data.code,
+        sort: data.sort ?? 0,
         status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? PostStatus.ENABLE),
-      } as PostVO);
+        remark: data.remark || '',
+      });
     } catch (error: any) {
       toast.error(error.message || '获取岗位详情失败');
     } finally {
@@ -58,55 +64,35 @@ const PostForm = forwardRef<PostFormRef, PostFormProps>(({ onSuccess }, ref) =>
     }
   };
 
-  // 重置表单
-  const resetForm = () => {
-    setFormData({
-      name: '',
-      code: '',
-      sort: 0,
-      status: PostStatus.ENABLE,
-      remark: '',
-    });
-  };
-
   // 提交表单
   const submitForm = async () => {
-    // 验证必填字段
-    if (!formData.name || formData.name.trim() === '') {
-      toast.error('岗位名称不能为空');
-      return;
-    }
-    if (!formData.code || formData.code.trim() === '') {
-      toast.error('岗位编码不能为空');
-      return;
-    }
-    if (formData.sort === undefined || formData.sort === null) {
-      toast.error('岗位顺序不能为空');
-      return;
-    }
-    if (formData.status === undefined || formData.status === null) {
-      toast.error('岗位状态不能为空');
-      return;
-    }
-
-    setFormLoading(true);
     try {
+      const values = await form.validateFields();
+      setFormLoading(true);
+      
       const submitData: PostVO = {
-        ...formData,
-        sort: Number(formData.sort) || 0,
-        status: Number(formData.status),
+        ...values,
+        sort: Number(values.sort) || 0,
+        status: Number(values.status),
       };
 
       if (formType === 'create') {
         await postApi.createPost(submitData);
         toast.success('创建成功');
       } else {
+        if (currentId) {
+          submitData.id = currentId;
+        }
         await postApi.updatePost(submitData);
         toast.success('更新成功');
       }
       setDialogVisible(false);
       onSuccess?.();
     } catch (error: any) {
+      if (error.errorFields) {
+        // 表单验证错误
+        return;
+      }
       toast.error(error.message || '操作失败');
     } finally {
       setFormLoading(false);
@@ -118,104 +104,72 @@ const PostForm = forwardRef<PostFormRef, PostFormProps>(({ onSuccess }, ref) =>
       title={dialogTitle}
       open={dialogVisible}
       onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
       width={600}
-      footer={[
-        <Button key="cancel" onClick={() => setDialogVisible(false)}>
-          取消
-        </Button>,
-        <Button 
-          key="submit" 
-          type="primary" 
-          loading={formLoading}
-          onClick={submitForm}
-        >
-          确定
-        </Button>,
-      ]}
       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 className="text-red-500 mr-1">*</span>
-              岗位名称
-            </span>
-            <Input
-              style={{ flex: 1 }}
-              placeholder="请输入岗位名称"
-              value={formData.name}
-              onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
-              disabled={formLoading}
-            />
-          </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 }}
-              placeholder="请输入岗位编码"
-              value={formData.code}
-              onChange={(e) => setFormData(prev => ({ ...prev, code: e.target.value }))}
-              disabled={formLoading}
-            />
-          </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"
-              placeholder="请输入岗位顺序"
-              value={formData.sort}
-              onChange={(e) => setFormData(prev => ({ ...prev, sort: Number(e.target.value) || 0 }))}
-              disabled={formLoading}
-            />
-          </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>
-            <Select
-              style={{ flex: 1 }}
-              value={formData.status}
-              onChange={(value) => setFormData(prev => ({ ...prev, status: Number(value) }))}
-              disabled={formLoading}
-              placeholder="请选择状态"
-            >
+      <Spin spinning={formLoading && formType === 'update'}>
+        <Form
+          form={form}
+          layout="horizontal"
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+          initialValues={{
+            sort: 0,
+            status: PostStatus.ENABLE,
+            remark: '',
+          }}
+        >
+          <Form.Item
+            label="岗位名称"
+            name="name"
+            rules={[
+              { required: true, message: '请输入岗位名称' },
+              { max: 50, message: '岗位名称不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入岗位名称" />
+          </Form.Item>
+
+          <Form.Item
+            label="岗位编码"
+            name="code"
+            rules={[
+              { required: true, message: '请输入岗位编码' },
+              { max: 50, message: '岗位编码不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入岗位编码" />
+          </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: '请选择状态' }]}
+          >
+            <Select placeholder="请选择状态">
               <Select.Option value={PostStatus.ENABLE}>启用</Select.Option>
               <Select.Option value={PostStatus.DISABLE}>禁用</Select.Option>
             </Select>
-          </div>
-
-          {/* 备注 */}
-          <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
-            <span className="text-sm text-gray-700" style={{ fontWeight: 500, minWidth: '80px', flexShrink: 0, paddingTop: '6px' }}>备注</span>
-            <Input.TextArea
-              style={{ flex: 1 }}
-              placeholder="请输入备注"
-              value={formData.remark}
-              onChange={(e) => setFormData(prev => ({ ...prev, remark: e.target.value }))}
-              disabled={formLoading}
-              rows={4}
-            />
-          </div>
-        </div>
-      )}
+          </Form.Item>
+
+          <Form.Item
+            label="备注"
+            name="remark"
+          >
+            <Input.TextArea placeholder="请输入备注" rows={4} />
+          </Form.Item>
+        </Form>
+      </Spin>
     </Modal>
   );
 });