فهرست منبع

部门管理实现弹框和弹框样式替换为组件形式

wyn 5 ماه پیش
والد
کامیت
64b069bec1
2فایلهای تغییر یافته به همراه163 افزوده شده و 227 حذف شده
  1. 2 2
      src/components/DepartmentManagement.tsx
  2. 161 225
      src/components/dept/DeptForm.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" />
+                    <Edit2 className="w-4 h-4 text-blue-500" />
                   </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" />
+                    <Trash2 className="w-4 h-4 text-red-500"/>
                   </Button>
                 </PermissionWrapper>
               </div>

+ 161 - 225
src/components/dept/DeptForm.tsx

@@ -1,8 +1,5 @@
-import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
-import { Button } from '../ui/button';
-import { Input } from '../ui/input';
-import { Label } from '../ui/label';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { Modal, Form, Input, Select, Spin } from 'antd';
 import { deptApi, DeptVO } from '../../api/dept';
 import { userApi } from '../../api/user';
 import { UserVO } from '../../types';
@@ -24,15 +21,8 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
   const [dialogTitle, setDialogTitle] = useState('');
   const [formLoading, setFormLoading] = useState(false);
   const [formType, setFormType] = useState<'create' | 'update'>('create');
-  const [formData, setFormData] = useState<Partial<DeptVO>>({
-    name: '',
-    parentId: 0,
-    sort: 0,
-    leaderUserId: undefined,
-    phone: '',
-    email: '',
-    status: CommonStatusEnum.ENABLE,
-  });
+  const [currentId, setCurrentId] = useState<number | undefined>();
+  const [form] = Form.useForm();
   const [deptTree, setDeptTree] = useState<TreeNode[]>([]);
   const [userList, setUserList] = useState<UserVO[]>([]);
   const [statusOptions, setStatusOptions] = useState(getIntDictOptions(DICT_TYPE.COMMON_STATUS));
@@ -42,7 +32,8 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
     setDialogVisible(true);
     setDialogTitle(type === 'create' ? '新增部门' : '编辑部门');
     setFormType(type as 'create' | 'update');
-    resetForm();
+    setCurrentId(id);
+    form.resetFields();
 
     // 加载用户列表
     try {
@@ -55,7 +46,9 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
     // 加载部门树
     try {
       const deptList = await deptApi.getSimpleDeptList();
-      const treeData = handleTree(deptList);
+      // 过滤掉没有 id 的项,并确保 id 存在
+      const validDeptList = deptList.filter((dept): dept is DeptVO & { id: number } => !!dept.id);
+      const treeData = handleTree(validDeptList);
       // 添加顶级部门选项
       const rootDept: TreeNode = { id: 0, name: '顶级部门', children: treeData };
       setDeptTree([rootDept]);
@@ -68,12 +61,30 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
       setFormLoading(true);
       try {
         const deptData = await deptApi.getDept(id);
-        setFormData(deptData);
+        form.setFieldsValue({
+          name: deptData.name,
+          parentId: deptData.parentId ?? 0,
+          sort: deptData.sort ?? 0,
+          leaderUserId: deptData.leaderUserId,
+          phone: deptData.phone || '',
+          email: deptData.email || '',
+          status: deptData.status ?? CommonStatusEnum.ENABLE,
+        });
       } catch (error) {
         toast.error('获取部门信息失败');
       } finally {
         setFormLoading(false);
       }
+    } else {
+      form.setFieldsValue({
+        name: '',
+        parentId: 0,
+        sort: 0,
+        leaderUserId: undefined,
+        phone: '',
+        email: '',
+        status: CommonStatusEnum.ENABLE,
+      });
     }
   };
 
@@ -81,243 +92,168 @@ const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) =>
     open,
   }));
 
-  // 重置表单
-  const resetForm = () => {
-    setFormData({
-      name: '',
-      parentId: 0,
-      sort: 0,
-      leaderUserId: undefined,
-      phone: '',
-      email: '',
-      status: CommonStatusEnum.ENABLE,
-    });
-  };
-
   // 提交表单
   const submitForm = async () => {
-    // 基本验证
-    if (!formData.name?.trim()) {
-      toast.error('部门名称不能为空');
-      return;
-    }
-    if (formData.parentId === undefined || formData.parentId === null) {
-      toast.error('上级部门不能为空');
-      return;
-    }
-    if (formData.sort === undefined || formData.sort === null) {
-      toast.error('显示排序不能为空');
-      return;
-    }
-    if (formData.status === undefined || formData.status === null) {
-      toast.error('状态不能为空');
-      return;
-    }
-
-    // 邮箱验证
-    if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
-      toast.error('请输入正确的邮箱地址');
-      return;
-    }
-
-    // 手机号验证
-    if (formData.phone && !/^1[3|4|5|6|7|8|9][0-9]\d{8}$/.test(formData.phone)) {
-      toast.error('请输入正确的手机号码');
-      return;
-    }
-
-    setFormLoading(true);
     try {
-      const data = formData as DeptVO;
+      const values = await form.validateFields();
+      setFormLoading(true);
+      
+      const data: DeptVO = {
+        ...values,
+        parentId: values.parentId ?? 0,
+        sort: values.sort ?? 0,
+        status: values.status ?? CommonStatusEnum.ENABLE,
+      };
+
       if (formType === 'create') {
         await deptApi.createDept(data);
         toast.success('创建成功');
       } else {
+        if (currentId) {
+          data.id = currentId;
+        }
         await deptApi.updateDept(data);
         toast.success('更新成功');
       }
       setDialogVisible(false);
       onSuccess?.();
     } catch (error: any) {
+      if (error.errorFields) {
+        // 表单验证错误
+        return;
+      }
       toast.error(error.message || '操作失败');
     } finally {
       setFormLoading(false);
     }
   };
 
-  // 递归渲染部门树选项
-  const renderDeptOptions = (nodes: TreeNode[], level: number = 0): React.ReactNode[] => {
-    return nodes.map((node) => (
-      <React.Fragment key={node.id}>
-        <SelectItem value={String(node.id)}>
-          {' '.repeat(level)}
-          {node.name}
-        </SelectItem>
-        {node.children && node.children.length > 0 && renderDeptOptions(node.children, level + 1)}
-      </React.Fragment>
-    ));
+  // 递归构建部门树选项
+  const buildDeptOptions = (nodes: TreeNode[], level: number = 0): { label: string; value: number }[] => {
+    const options: { label: string; value: number }[] = [];
+    nodes.forEach((node) => {
+      options.push({
+        label: ' '.repeat(level) + node.name,
+        value: node.id,
+      });
+      if (node.children && node.children.length > 0) {
+        options.push(...buildDeptOptions(node.children, level + 1));
+      }
+    });
+    return options;
   };
 
-  if (!dialogVisible) return null;
-
   return (
-    <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[9999] animate-in fade-in duration-200 p-4">
-      <div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl animate-in zoom-in duration-200 relative z-[10000] max-h-[85vh] flex flex-col overflow-hidden">
-        {/* 弹窗标题 */}
-        <div className="px-5 py-3 border-b border-gray-200 flex items-center justify-between shrink-0">
-          <h3 className="text-base text-gray-900">{dialogTitle}</h3>
-          <div className="flex items-center gap-2">
-            <button
-              onClick={() => setDialogVisible(false)}
-              className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
-            >
-              <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
-              </svg>
-            </button>
-          </div>
-        </div>
-
-        {/* 弹窗内容 */}
-        <div className="px-5 py-3 overflow-y-auto overflow-x-hidden flex-1 min-h-0">
-          {formLoading ? (
-            <div className="py-6 text-center text-gray-500">加载中...</div>
-          ) : (
-            <div className="space-y-4">
-              {/* 上级部门 */}
-              <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
-                <label className="text-sm text-gray-700 whitespace-nowrap">上级部门 *</label>
-                <Select
-                  value={String(formData.parentId ?? 0)}
-                  onValueChange={(value) => setFormData({ ...formData, parentId: Number(value) })}
-                >
-                  <SelectTrigger className="h-[36px]">
-                    <SelectValue placeholder="请选择上级部门" />
-                  </SelectTrigger>
-                  <SelectContent>
-                    {renderDeptOptions(deptTree)}
-                  </SelectContent>
-                </Select>
-              </div>
-
-              {/* 部门名称 */}
-              <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
-                <label className="text-sm text-gray-700 whitespace-nowrap">部门名称 *</label>
-                <Input
-                  id="name"
-                  value={formData.name || ''}
-                  onChange={(e) => setFormData({ ...formData, name: e.target.value })}
-                  placeholder="请输入部门名称"
-                  className="h-[36px]"
-                />
-              </div>
-
-              {/* 显示排序 */}
-              <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
-                <label className="text-sm text-gray-700 whitespace-nowrap">显示排序 *</label>
-                <Input
-                  id="sort"
-                  type="number"
-                  value={formData.sort ?? 0}
-                  onChange={(e) => setFormData({ ...formData, sort: Number(e.target.value) })}
-                  placeholder="请输入显示排序"
-                  min={0}
-                  className="h-[36px]"
-                />
-              </div>
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={600}
+      destroyOnClose
+    >
+      <Spin spinning={formLoading}>
+        <Form
+          form={form}
+          layout="horizontal"
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+          initialValues={{
+            parentId: 0,
+            sort: 0,
+            status: CommonStatusEnum.ENABLE,
+          }}
+        >
+          <Form.Item
+            label="上级部门"
+            name="parentId"
+            rules={[{ required: true, message: '请选择上级部门' }]}
+          >
+            <Select
+              placeholder="请选择上级部门"
+              options={buildDeptOptions(deptTree)}
+            />
+          </Form.Item>
 
-              {/* 负责人 */}
-              <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
-                <label className="text-sm text-gray-700 whitespace-nowrap">负责人</label>
-                <Select
-                  value={formData.leaderUserId ? String(formData.leaderUserId) : 'none'}
-                  onValueChange={(value) =>
-                    setFormData({ ...formData, leaderUserId: value === 'none' ? undefined : Number(value) })
-                  }
-                >
-                  <SelectTrigger className="h-[36px]">
-                    <SelectValue placeholder="请选择负责人" />
-                  </SelectTrigger>
-                  <SelectContent>
-                    <SelectItem value="none">无</SelectItem>
-                    {userList.map((user) => (
-                      <SelectItem key={user.id} value={String(user.id)}>
-                        {user.nickname}
-                      </SelectItem>
-                    ))}
-                  </SelectContent>
-                </Select>
-              </div>
+          <Form.Item
+            label="部门名称"
+            name="name"
+            rules={[
+              { required: true, message: '请输入部门名称' },
+              { max: 50, message: '部门名称不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入部门名称" />
+          </Form.Item>
 
-              {/* 联系电话 */}
-              <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
-                <label className="text-sm text-gray-700 whitespace-nowrap">联系电话</label>
-                <Input
-                  id="phone"
-                  value={formData.phone || ''}
-                  onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
-                  placeholder="请输入联系电话"
-                  maxLength={11}
-                  className="h-[36px]"
-                />
-              </div>
+          <Form.Item
+            label="显示排序"
+            name="sort"
+            rules={[{ required: true, message: '请输入显示排序' }]}
+          >
+            <Input type="number" placeholder="请输入显示排序" min={0} />
+          </Form.Item>
 
-              {/* 邮箱 */}
-              <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
-                <label className="text-sm text-gray-700 whitespace-nowrap">邮箱</label>
-                <Input
-                  id="email"
-                  type="email"
-                  value={formData.email || ''}
-                  onChange={(e) => setFormData({ ...formData, email: e.target.value })}
-                  placeholder="请输入邮箱"
-                  maxLength={50}
-                  className="h-[36px]"
-                />
-              </div>
+          <Form.Item
+            label="负责人"
+            name="leaderUserId"
+          >
+            <Select
+              placeholder="请选择负责人"
+              allowClear
+              options={[
+                ...userList.map((user) => ({
+                  label: user.nickname,
+                  value: user.id,
+                })),
+              ]}
+            />
+          </Form.Item>
 
-              {/* 状态 */}
-              <div className="grid grid-cols-[100px_1fr] items-center gap-3 min-h-[40px]">
-                <label className="text-sm text-gray-700 whitespace-nowrap">状态 *</label>
-                <Select
-                  value={String(formData.status ?? CommonStatusEnum.ENABLE)}
-                  onValueChange={(value) => setFormData({ ...formData, status: Number(value) })}
-                >
-                  <SelectTrigger className="h-[36px]">
-                    <SelectValue placeholder="请选择状态" />
-                  </SelectTrigger>
-                  <SelectContent>
-                    {statusOptions.map((dict) => (
-                      <SelectItem key={dict.value} value={String(dict.value)}>
-                        {dict.label}
-                      </SelectItem>
-                    ))}
-                  </SelectContent>
-                </Select>
-              </div>
-            </div>
-          )}
-        </div>
+          <Form.Item
+            label="联系电话"
+            name="phone"
+            rules={[
+              {
+                pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
+                message: '请输入正确的手机号码',
+              },
+            ]}
+          >
+            <Input placeholder="请输入联系电话" maxLength={11} />
+          </Form.Item>
 
-        {/* 弹窗底部 */}
-        <div className="px-5 py-3 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-2 rounded-b-xl shrink-0">
-          <button
-            onClick={() => setDialogVisible(false)}
-            disabled={formLoading}
-            className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+          <Form.Item
+            label="邮箱"
+            name="email"
+            rules={[
+              {
+                type: 'email',
+                message: '请输入正确的邮箱地址',
+              },
+              { max: 50, message: '邮箱不能超过50个字符' },
+            ]}
           >
-            取消
-          </button>
-          <button
-            onClick={submitForm}
-            disabled={formLoading}
-            className="px-4 py-2 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
+            <Input type="email" placeholder="请输入邮箱" maxLength={50} />
+          </Form.Item>
+
+          <Form.Item
+            label="状态"
+            name="status"
+            rules={[{ required: true, message: '请选择状态' }]}
           >
-            确定
-          </button>
-        </div>
-      </div>
-    </div>
+            <Select
+              placeholder="请选择状态"
+              options={statusOptions.map((dict) => ({
+                label: dict.label,
+                value: dict.value,
+              }))}
+            />
+          </Form.Item>
+        </Form>
+      </Spin>
+    </Modal>
   );
 });