Переглянути джерело

部门管理页面表格,搜索功能开发

wyn 5 місяців тому
батько
коміт
1c7cf0140c

+ 55 - 0
src/api/dept/index.ts

@@ -0,0 +1,55 @@
+import axiosInstance from '../../utils/axios';
+
+// 分页参数类型
+export interface PageParam {
+  pageNo?: number;
+  pageSize?: number;
+  [key: string]: any;
+}
+
+// 部门 VO 类型
+export interface DeptVO {
+  id?: number;
+  name: string;
+  parentId: number;
+  status: number;
+  sort: number;
+  leaderUserId?: number;
+  phone?: string;
+  email?: string;
+  createTime?: Date | string;
+}
+
+// 部门管理 API
+export const deptApi = {
+  // 查询部门(精简)列表
+  getSimpleDeptList: (): Promise<DeptVO[]> => {
+    return axiosInstance.get('/system/dept/simple-list');
+  },
+
+  // 查询部门列表
+  getDeptPage: (params: PageParam): Promise<DeptVO[]> => {
+    return axiosInstance.get('/system/dept/list', { params });
+  },
+
+  // 查询部门详情
+  getDept: (id: number): Promise<DeptVO> => {
+    return axiosInstance.get(`/system/dept/get?id=${id}`);
+  },
+
+  // 新增部门
+  createDept: (data: DeptVO): Promise<void> => {
+    return axiosInstance.post('/system/dept/create', data);
+  },
+
+  // 修改部门
+  updateDept: (data: DeptVO): Promise<void> => {
+    return axiosInstance.put('/system/dept/update', data);
+  },
+
+  // 删除部门
+  deleteDept: (id: number): Promise<void> => {
+    return axiosInstance.delete(`/system/dept/delete?id=${id}`);
+  },
+};
+

+ 3 - 0
src/api/index.ts

@@ -6,6 +6,7 @@ import { userImportApi } from './user/import';
 import { userCharacteristicApi } from './user/characteristic';
 import { systemApi } from './System';
 import { hardwareApi } from './Hardware';
+import { deptApi } from './dept';
 
 // API 响应类型
 export interface ApiResponse<T = any> {
@@ -23,6 +24,7 @@ export { userImportApi } from './user/import';
 export { userCharacteristicApi } from './user/characteristic';
 export { systemApi } from './System';
 export { hardwareApi } from './Hardware';
+export { deptApi } from './dept';
 
 // 为了兼容旧代码,导出 authApi 作为 loginApi 的别名
 export { loginApi as authApi } from './Login';
@@ -38,5 +40,6 @@ export default {
   userCharacteristic: userCharacteristicApi,
   system: systemApi,
   hardware: hardwareApi,
+  dept: deptApi,
 };
 

+ 291 - 488
src/components/DepartmentManagement.tsx

@@ -1,525 +1,328 @@
-import React, { useState } from 'react';
-import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown, Building2 } from 'lucide-react';
+import React, { useState, useEffect, useRef } from 'react';
+import { Plus, Search, RefreshCw, ChevronRight, ChevronDown, Edit2, Trash2 } from 'lucide-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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
 import PermissionWrapper from './PermissionWrapper';
-import { hasPermission } from '../utils/permission';
-
-interface DepartmentNode {
-  id: number;
-  name: string;
-  manager?: string;
-  phone?: string;
-  email?: string;
-  memberCount?: number;
-  createTime?: string;
-  parentId?: number;
-  children?: DepartmentNode[];
-}
+import { deptApi, DeptVO } from '../api/dept';
+import { userApi } from '../api/user';
+import { UserVO } from '../types';
+import { toast } from 'sonner';
+import { DICT_TYPE, getIntDictOptions, getDictLabel } from '../utils/dict';
+import { dateFormatter } from '../utils/formatTime';
+import { handleTree, TreeNode } from '../utils/tree';
+import DeptForm, { DeptFormRef } from './dept/DeptForm';
 
 export default function DepartmentManagement() {
-  const [expandedDeptIds, setExpandedDeptIds] = useState<number[]>([1]); // 默认展开总公司
-  const [selectedDeptId, setSelectedDeptId] = useState<number | null>(1); // 选中的部门ID
-  const [searchTerm, setSearchTerm] = useState('');
-  const [showAddModal, setShowAddModal] = useState(false);
-  const [editingItem, setEditingItem] = useState<any>(null);
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<TreeNode[]>([]);
+  const [queryParams, setQueryParams] = useState({
+    pageNo: 1,
+    pageSize: 100,
+    name: '',
+    status: undefined as number | undefined,
+  });
+  const [isExpandAll, setIsExpandAll] = useState(true);
+  const [refreshTable, setRefreshTable] = useState(true);
+  const [userList, setUserList] = useState<UserVO[]>([]);
+  const [statusOptions] = useState(() => {
+    try {
+      return getIntDictOptions(DICT_TYPE.COMMON_STATUS);
+    } catch (error) {
+      console.error('获取字典选项失败:', error);
+      return [];
+    }
+  });
+  const formRef = useRef<DeptFormRef>(null);
 
-  // 部门树形数据
-  const departmentTree: DepartmentNode[] = [
-    {
-      id: 1,
-      name: '总公司',
-      manager: '李总',
-      phone: '138001380 00',
-      email: 'ceo@company.com',
-      memberCount: 120,
-      createTime: '2024-01-01',
-      children: [
-        {
-          id: 2,
-          name: '技术中心',
-          manager: '张三',
-          phone: '13800138001',
-          email: 'tech@company.com',
-          memberCount: 45,
-          createTime: '2024-01-05',
-          parentId: 1,
-          children: [
-            {
-              id: 21,
-              name: '研发部',
-              manager: '王研',
-              phone: '13800138011',
-              email: 'rd@company.com',
-              memberCount: 25,
-              createTime: '2024-01-10',
-              parentId: 2
-            },
-            {
-              id: 22,
-              name: '测试部',
-              manager: '赵测',
-              phone: '13800138012',
-              email: 'qa@company.com',
-              memberCount: 12,
-              createTime: '2024-01-10',
-              parentId: 2
-            },
-            {
-              id: 23,
-              name: '运维部',
-              manager: '李运',
-              phone: '13800138013',
-              email: 'ops@company.com',
-              memberCount: 8,
-              createTime: '2024-01-10',
-              parentId: 2
-            }
-          ]
-        },
-        {
-          id: 3,
-          name: '市场中心',
-          manager: '李四',
-          phone: '13800138002',
-          email: 'market@company.com',
-          memberCount: 30,
-          createTime: '2024-01-05',
-          parentId: 1,
-          children: [
-            {
-              id: 31,
-              name: '市场部',
-              manager: '钱市',
-              phone: '13800138021',
-              email: 'marketing@company.com',
-              memberCount: 18,
-              createTime: '2024-01-10',
-              parentId: 3
-            },
-            {
-              id: 32,
-              name: '销售部',
-              manager: '孙销',
-              phone: '13800138022',
-              email: 'sales@company.com',
-              memberCount: 12,
-              createTime: '2024-01-10',
-              parentId: 3
-            }
-          ]
-        },
-        {
-          id: 4,
-          name: '行政中心',
-          manager: '王五',
-          phone: '13800138003',
-          email: 'admin@company.com',
-          memberCount: 25,
-          createTime: '2024-01-05',
-          parentId: 1,
-          children: [
-            {
-              id: 41,
-              name: '人力资源部',
-              manager: '周人',
-              phone: '13800138031',
-              email: 'hr@company.com',
-              memberCount: 10,
-              createTime: '2024-01-10',
-              parentId: 4
-            },
-            {
-              id: 42,
-              name: '财务部',
-              manager: '吴财',
-              phone: '13800138032',
-              email: 'finance@company.com',
-              memberCount: 8,
-              createTime: '2024-01-10',
-              parentId: 4
-            },
-            {
-              id: 43,
-              name: '行政部',
-              manager: '郑行',
-              phone: '13800138033',
-              email: 'office@company.com',
-              memberCount: 7,
-              createTime: '2024-01-10',
-              parentId: 4
-            }
-          ]
-        },
-        {
-          id: 5,
-          name: '安全部',
-          manager: '赵六',
-          phone: '13800138004',
-          email: 'security@company.com',
-          memberCount: 20,
-          createTime: '2024-01-05',
-          parentId: 1
-        }
-      ]
+  // 查询部门列表
+  const getList = async () => {
+    setLoading(true);
+    try {
+      const data = await deptApi.getDeptPage(queryParams);
+      const treeData = handleTree(data || []);
+      setList(treeData);
+    } catch (error: any) {
+      console.error('获取部门列表失败:', error);
+      toast.error(error.message || '获取部门列表失败');
+      setList([]); // 设置空数组,确保页面能显示
+    } finally {
+      setLoading(false);
     }
-  ];
+  };
 
-  // 切换展开/收起
-  const toggleDeptExpand = (id: number) => {
-    setExpandedDeptIds(prev =>
-      prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
-    );
+  // 展开/折叠操作
+  const toggleExpandAll = () => {
+    setRefreshTable(false);
+    setIsExpandAll(!isExpandAll);
+    setTimeout(() => {
+      setRefreshTable(true);
+    }, 0);
   };
 
-  // 获取所有部门(扁平化)
-  const getAllDepartments = (nodes: DepartmentNode[], result: DepartmentNode[] = []): DepartmentNode[] => {
-    nodes.forEach(node => {
-      result.push(node);
-      if (node.children) {
-        getAllDepartments(node.children, result);
-      }
-    });
-    return result;
+  // 搜索按钮操作
+  const handleQuery = () => {
+    getList();
   };
 
-  const allDepartments = getAllDepartments(departmentTree);
-  
-  // 根据选中的部门ID过滤显示的部门列表
-  const getFilteredDepartments = () => {
-    if (!selectedDeptId) return allDepartments;
-    
-    // 找到选中的部门
-    const selectedDept = allDepartments.find(d => d.id === selectedDeptId);
-    if (!selectedDept) return allDepartments;
-    
-    // 如果有子部门,只显示直接子部门
-    if (selectedDept.children && selectedDept.children.length > 0) {
-      return selectedDept.children;
-    }
-    
-    // 如果没有子部门,返回自己
-    return [selectedDept];
+  // 重置按钮操作
+  const resetQuery = () => {
+    const resetParams = {
+      pageNo: 1,
+      pageSize: 100,
+      name: '',
+      status: undefined as number | undefined,
+    };
+    setQueryParams(resetParams);
+    // 立即使用重置后的参数获取列表
+    setLoading(true);
+    deptApi.getDeptPage(resetParams)
+      .then((data) => {
+        const treeData = handleTree(data || []);
+        setList(treeData);
+      })
+      .catch((error: any) => {
+        console.error('获取部门列表失败:', error);
+        toast.error(error.message || '获取部门列表失败');
+        setList([]);
+      })
+      .finally(() => {
+        setLoading(false);
+      });
   };
 
-  const displayedDepartments = getFilteredDepartments().filter((dept) =>
-    dept.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
-    dept.manager?.toLowerCase().includes(searchTerm.toLowerCase())
-  );
+  // 添加/修改操作
+  const openForm = (type: string, id?: number) => {
+    formRef.current?.open(type, id);
+  };
 
-  // 递归渲染树形节点
-  const renderTreeNode = (node: DepartmentNode, level: number = 0) => {
-    const isExpanded = expandedDeptIds.includes(node.id);
-    const isSelected = selectedDeptId === node.id;
-    const hasChildren = node.children && node.children.length > 0;
+  // 删除按钮操作
+  const handleDelete = async (id: number) => {
+    if (!confirm('确定要删除这个部门吗?')) {
+      return;
+    }
 
-    return (
-      <div key={node.id}>
-        <div
-          className={`flex items-center gap-2 px-3 py-2.5 cursor-pointer transition-all rounded-lg ${
-            isSelected 
-              ? 'bg-blue-100 text-blue-700' 
-              : 'hover:bg-gray-100 text-gray-700'
-          }`}
-          style={{ paddingLeft: `${level * 16 + 12}px` }}
-          onClick={() => setSelectedDeptId(node.id)}
-        >
-          {hasChildren ? (
-            <button
-              onClick={(e) => {
-                e.stopPropagation();
-                toggleDeptExpand(node.id);
-              }}
-              className="p-0.5 hover:bg-gray-200 rounded transition-colors"
-            >
-              {isExpanded ? (
-                <ChevronDown className="w-4 h-4" />
-              ) : (
-                <ChevronRight className="w-4 h-4" />
-              )}
-            </button>
-          ) : (
-            <span className="w-5"></span>
-          )}
-          <Building2 className="w-4 h-4" />
-          <span className="text-sm flex-1">{node.name}</span>
-          {node.memberCount && (
-            <span className="text-xs px-2 py-0.5 bg-gray-200 rounded">
-              {node.memberCount}人
-            </span>
-          )}
-        </div>
-        
-        {isExpanded && hasChildren && (
-          <div>
-            {node.children!.map(child => renderTreeNode(child, level + 1))}
-          </div>
-        )}
-      </div>
-    );
+    try {
+      await deptApi.deleteDept(id);
+      toast.success('删除成功');
+      await getList();
+    } catch (error: any) {
+      toast.error(error.message || '删除失败');
+    }
   };
 
-  const handleDelete = (id: number) => {
-    if (confirm('确定要删除这个部门吗?')) {
-      console.log('删除部门:', id);
-    }
+  // 获取负责人名称
+  const getLeaderName = (leaderUserId?: number): string => {
+    if (!leaderUserId) return '';
+    const user = userList.find((u) => u.id === leaderUserId);
+    return user?.nickname || '';
   };
 
-  const handleEdit = (item: any) => {
-    setEditingItem(item);
-    setShowAddModal(true);
+  // 递归渲染表格行
+  const renderTableRows = (nodes: TreeNode[], level: number = 0): React.ReactNode[] => {
+    return nodes.map((node) => {
+      const hasChildren = node.children && node.children.length > 0;
+      const isExpanded = isExpandAll; // 简化处理,实际可以根据状态控制
+
+      return (
+        <React.Fragment key={node.id}>
+          <TableRow className="hover:bg-gray-50">
+            <TableCell style={{ paddingLeft: `${level * 24 + 12}px` }} className="font-medium">
+              <div className="flex items-center gap-2">
+                {hasChildren ? (
+                  <button
+                    onClick={() => {
+                      // 这里可以实现单个节点的展开/折叠
+                    }}
+                    className="p-0.5 hover:bg-gray-200 rounded transition-colors"
+                  >
+                    {isExpanded ? (
+                      <ChevronDown className="w-4 h-4" />
+                    ) : (
+                      <ChevronRight className="w-4 h-4" />
+                    )}
+                  </button>
+                ) : (
+                  <span className="w-5"></span>
+                )}
+                <span>{(node as any).name || '未命名'}</span>
+              </div>
+            </TableCell>
+            <TableCell className="text-sm text-gray-600">{getLeaderName((node as any).leaderUserId) || '-'}</TableCell>
+            <TableCell className="text-center">{(node as any).sort || 0}</TableCell>
+            <TableCell className="text-center">
+              <span className="text-sm text-gray-600">
+                {getDictLabel(DICT_TYPE.COMMON_STATUS, (node as any).status) || '未知'}
+              </span>
+            </TableCell>
+            <TableCell className="text-sm text-gray-600">
+              {(node as any).createTime ? dateFormatter((node as any).createTime) : '-'}
+            </TableCell>
+            <TableCell>
+              <div className="flex items-center gap-2 justify-center">
+                <PermissionWrapper permission="system:dept:update">
+                  <Button
+                    variant="ghost"
+                    size="sm"
+                    onClick={() => openForm('update', node.id)}
+                    className="h-8 px-2"
+                  >
+                    <Edit2 className="w-4 h-4" />
+                  </Button>
+                </PermissionWrapper>
+                <PermissionWrapper permission="system:dept:delete">
+                  <Button
+                    variant="ghost"
+                    size="sm"
+                    onClick={() => handleDelete(node.id)}
+                    className="h-8 px-2 text-red-600 hover:text-red-700"
+                  >
+                    <Trash2 className="w-4 h-4" />
+                  </Button>
+                </PermissionWrapper>
+              </div>
+            </TableCell>
+          </TableRow>
+          {hasChildren && isExpanded && renderTableRows(node.children!, level + 1)}
+        </React.Fragment>
+      );
+    });
   };
 
+  // 初始化
+  useEffect(() => {
+    console.log('DepartmentManagement 组件已挂载,开始获取数据');
+    const initData = async () => {
+      try {
+        console.log('开始调用 getList() 获取部门列表');
+        await getList();
+        console.log('部门列表获取成功');
+      } catch (error) {
+        console.error('获取部门列表失败:', error);
+        // 即使失败也设置 loading 为 false,显示页面
+        setLoading(false);
+      }
+      // 获取用户列表
+      try {
+        console.log('开始获取用户列表');
+        const users = await userApi.getSimpleUserList();
+        setUserList(users);
+        console.log('用户列表获取成功,用户数量:', users.length);
+      } catch (error) {
+        console.error('获取用户列表失败:', error);
+      }
+    };
+    initData();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
   return (
-    <div className="flex gap-6 h-full">
-      {/* 左侧树形结构 */}
-      <div className="w-80 flex-shrink-0">
-        <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4 h-full flex flex-col">
-          <div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-200">
-            <h3 className="text-base text-gray-900">组织架构</h3>
-            <PermissionWrapper permission="system:dept:create">
-              <button
-                onClick={() => {
-                  setEditingItem(null);
-                  setShowAddModal(true);
-                }}
-                className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
-                title="新增部门"
-              >
-                <Plus className="w-4 h-4" />
-              </button>
-            </PermissionWrapper>
-          </div>
-          
-          <div className="flex-1 overflow-y-auto">
-            {departmentTree.map(node => renderTreeNode(node))}
+    <div className="space-y-4">
+      {/* 搜索栏 */}
+      <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
+        <div className="flex items-center justify-between gap-4">
+          {/* 搜索输入框 */}
+          <div className="flex items-center gap-3 flex-1 max-w-md">
+            <Label htmlFor="dept-name-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
+              部门名称
+            </Label>
+            <Input
+              id="dept-name-search"
+              placeholder="请输入部门名称"
+              value={queryParams.name || ''}
+              onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
+              onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
+              className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all"
+            />
           </div>
-        </div>
-      </div>
-
-      {/* 右侧列表 */}
-      <div className="flex-1 space-y-6">
-        {/* 工具栏 */}
-        <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4">
-          <div className="flex items-center justify-between">
-            <div className="relative w-80">
-              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
-              <input
-                type="text"
-                placeholder="搜索部门..."
-                value={searchTerm}
-                onChange={(e) => setSearchTerm(e.target.value)}
-                className="w-full h-10 pl-10 pr-4 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-              />
-            </div>
 
+          {/* 操作按钮组 */}
+          <div className="flex items-center gap-3">
+            <button
+              onClick={handleQuery}
+              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
+            >
+              <Search className="w-4 h-4" strokeWidth={2.5} />
+              <span className="text-sm">搜索</span>
+            </button>
+            
+            <button
+              onClick={resetQuery}
+              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
+            >
+              <RefreshCw className="w-4 h-4" strokeWidth={2.5} />
+              <span className="text-sm">重置</span>
+            </button>
+            
             <PermissionWrapper permission="system:dept:create">
               <button
-                onClick={() => {
-                  setEditingItem(null);
-                  setShowAddModal(true);
-                }}
+                onClick={() => openForm('create')}
                 className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
               >
                 <Plus className="w-4 h-4" strokeWidth={2.5} />
-                <span className="text-sm">新增部门</span>
+                <span className="text-sm">新增</span>
               </button>
             </PermissionWrapper>
-          </div>
-        </div>
-
-        {/* 表格 */}
-        <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
-          <div className="overflow-x-auto">
-            <table className="w-full">
-              <thead>
-                <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
-                  <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
-                    序号
-                  </th>
-                  <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
-                    部门名称
-                  </th>
-                  <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
-                    负责人
-                  </th>
-                  <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
-                    联系电话
-                  </th>
-                  <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '20%' }}>
-                    邮箱
-                  </th>
-                  <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
-                    成员数
-                  </th>
-                  <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
-                    创建时间
-                  </th>
-                  <th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
-                    操作
-                  </th>
-                </tr>
-              </thead>
-              <tbody className="divide-y divide-gray-100">
-                {displayedDepartments.map((dept, index) => (
-                  <tr
-                    key={dept.id}
-                    className="hover:bg-blue-50/30 transition-colors"
-                  >
-                    <td className="px-6 py-4 text-sm text-gray-900">
-                      {index + 1}
-                    </td>
-                    <td className="px-6 py-4 text-sm text-gray-900">{dept.name}</td>
-                    <td className="px-6 py-4 text-sm text-gray-900">{dept.manager}</td>
-                    <td className="px-6 py-4 text-sm text-gray-900">{dept.phone}</td>
-                    <td className="px-6 py-4 text-sm text-gray-900">{dept.email}</td>
-                    <td className="px-6 py-4 text-sm text-gray-900">{dept.memberCount}</td>
-                    <td className="px-6 py-4 text-sm text-gray-900">{dept.createTime}</td>
-                    <td className="px-6 py-4">
-                      <div className="flex items-center justify-center gap-2">
-                        <PermissionWrapper permission="system:dept:update">
-                          <button
-                            onClick={() => handleEdit(dept)}
-                            className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
-                            title="编辑"
-                          >
-                            <Edit2 className="w-4 h-4" />
-                          </button>
-                        </PermissionWrapper>
-                        <PermissionWrapper permission="system:dept:delete">
-                          <button
-                            onClick={() => handleDelete(dept.id)}
-                            className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
-                            title="删除"
-                          >
-                            <Trash2 className="w-4 h-4" />
-                          </button>
-                        </PermissionWrapper>
-                        <button
-                          className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
-                          title="更多"
-                        >
-                          <MoreVertical className="w-4 h-4" />
-                        </button>
-                      </div>
-                    </td>
-                  </tr>
-                ))}
-              </tbody>
-            </table>
-          </div>
-
-          {/* 分页 */}
-          <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200">
-            <div className="flex items-center justify-between">
-              <div className="text-sm text-gray-600">
-                共 <span className="text-blue-600">{displayedDepartments.length}</span> 个部门
-              </div>
-              <div className="flex gap-2">
-                <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
-                  上一页
-                </button>
-                <button className="px-4 py-2 text-sm text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors">
-                  1
-                </button>
-                <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
-                  下一页
-                </button>
-              </div>
-            </div>
+            
+            <button
+              onClick={toggleExpandAll}
+              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
+            >
+              {isExpandAll ? (
+                <>
+                  <ChevronDown className="w-4 h-4" strokeWidth={2.5} />
+                  <span className="text-sm">折叠</span>
+                </>
+              ) : (
+                <>
+                  <ChevronRight className="w-4 h-4" strokeWidth={2.5} />
+                  <span className="text-sm">展开</span>
+                </>
+              )}
+            </button>
           </div>
         </div>
       </div>
 
-      {/* 新增/编辑弹窗 */}
-      {showAddModal && (
-        <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
-          <div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 animate-in zoom-in duration-200">
-            {/* 弹窗标题 */}
-            <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
-              <h3 className="text-lg text-gray-900">
-                {editingItem ? '编辑部门' : '新增部门'}
-              </h3>
-              <button
-                onClick={() => setShowAddModal(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 className="px-6 py-6">
-              <div className="grid grid-cols-2 gap-4">
-                <div>
-                  <label className="block text-sm text-gray-700 mb-2">部门名称 *</label>
-                  <input
-                    type="text"
-                    className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-                    placeholder="请输入部门名称"
-                    defaultValue={editingItem?.name || ''}
-                  />
-                </div>
-                <div>
-                  <label className="block text-sm text-gray-700 mb-2">上级部门</label>
-                  <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
-                    <option value="">请选择上级部门</option>
-                    {allDepartments.map(dept => (
-                      <option key={dept.id} value={dept.id}>{dept.name}</option>
-                    ))}
-                  </select>
-                </div>
-                <div>
-                  <label className="block text-sm text-gray-700 mb-2">负责人</label>
-                  <input
-                    type="text"
-                    className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-                    placeholder="请输入负责人"
-                    defaultValue={editingItem?.manager || ''}
-                  />
-                </div>
-                <div>
-                  <label className="block text-sm text-gray-700 mb-2">联系电话</label>
-                  <input
-                    type="text"
-                    className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-                    placeholder="请输入联系电话"
-                    defaultValue={editingItem?.phone || ''}
-                  />
-                </div>
-                <div className="col-span-2">
-                  <label className="block text-sm text-gray-700 mb-2">邮箱</label>
-                  <input
-                    type="email"
-                    className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-                    placeholder="请输入邮箱"
-                    defaultValue={editingItem?.email || ''}
-                  />
-                </div>
-              </div>
-            </div>
+      {/* 表格 */}
+      <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
+        {refreshTable && (
+          <Table>
+            <TableHeader>
+              <TableRow>
+                <TableHead className="w-[250px]">部门名称</TableHead>
+                <TableHead className="w-[150px]">负责人</TableHead>
+                <TableHead className="w-[80px] text-center">排序</TableHead>
+                <TableHead className="w-[100px] text-center">状态</TableHead>
+                <TableHead className="w-[180px]">创建时间</TableHead>
+                <TableHead className="w-[160px] text-center">操作</TableHead>
+              </TableRow>
+            </TableHeader>
+            <TableBody>
+              {loading ? (
+                <TableRow>
+                  <TableCell colSpan={6} className="text-center py-8 text-gray-500">
+                    加载中...
+                  </TableCell>
+                </TableRow>
+              ) : list.length === 0 ? (
+                <TableRow>
+                  <TableCell colSpan={6} className="text-center py-8 text-gray-500">
+                    暂无数据
+                  </TableCell>
+                </TableRow>
+              ) : (
+                renderTableRows(list)
+              )}
+            </TableBody>
+          </Table>
+        )}
+      </div>
 
-            {/* 弹窗底部 */}
-            <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
-              <button
-                onClick={() => setShowAddModal(false)}
-                className="px-5 py-2.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
-              >
-                取消
-              </button>
-              <button
-                onClick={() => {
-                  console.log('保存部门:', editingItem);
-                  setShowAddModal(false);
-                }}
-                className="px-5 py-2.5 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all"
-              >
-                确定
-              </button>
-            </div>
-          </div>
-        </div>
-      )}
+      {/* 表单弹窗 */}
+      <DeptForm ref={formRef} onSuccess={getList} />
     </div>
   );
-}
+}

+ 4 - 2
src/components/SystemConfig.tsx

@@ -707,8 +707,10 @@ export default function SystemConfig({ subMenu }: SystemConfigProps) {
   }
 
   // 部门管理使用左右分栏布局
-  if (subMenu === '部门管理') {
-    return <DepartmentManagement />;
+  // 支持通过 key 或 name 来判断
+  if (subMenu === '部门管理' || subMenu === 'departmentManagement' || subMenu === 'dept') {
+    console.log('渲染部门管理组件,subMenu:', subMenu);
+    return <DepartmentManagement key="department-management" />;
   }
 
   // 其他菜单使用表格展示

+ 327 - 0
src/components/dept/DeptForm.tsx

@@ -0,0 +1,327 @@
+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 { deptApi, DeptVO } from '../../api/dept';
+import { userApi } from '../../api/user';
+import { UserVO } from '../../types';
+import { toast } from 'sonner';
+import { DICT_TYPE, getIntDictOptions } from '../../utils/dict';
+import { CommonStatusEnum } from '../../utils/constants';
+import { handleTree, TreeNode } from '../../utils/tree';
+
+interface DeptFormProps {
+  onSuccess?: () => void;
+}
+
+export interface DeptFormRef {
+  open: (type: string, id?: number) => void;
+}
+
+const DeptForm = forwardRef<DeptFormRef, DeptFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  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 [deptTree, setDeptTree] = useState<TreeNode[]>([]);
+  const [userList, setUserList] = useState<UserVO[]>([]);
+  const [statusOptions, setStatusOptions] = useState(getIntDictOptions(DICT_TYPE.COMMON_STATUS));
+
+  // 打开弹窗
+  const open = async (type: string, id?: number) => {
+    setDialogVisible(true);
+    setDialogTitle(type === 'create' ? '新增部门' : '编辑部门');
+    setFormType(type as 'create' | 'update');
+    resetForm();
+
+    // 加载用户列表
+    try {
+      const users = await userApi.getSimpleUserList();
+      setUserList(users);
+    } catch (error) {
+      console.error('加载用户列表失败:', error);
+    }
+
+    // 加载部门树
+    try {
+      const deptList = await deptApi.getSimpleDeptList();
+      const treeData = handleTree(deptList);
+      // 添加顶级部门选项
+      const rootDept: TreeNode = { id: 0, name: '顶级部门', children: treeData };
+      setDeptTree([rootDept]);
+    } catch (error) {
+      console.error('加载部门树失败:', error);
+    }
+
+    // 修改时,设置数据
+    if (id) {
+      setFormLoading(true);
+      try {
+        const deptData = await deptApi.getDept(id);
+        setFormData(deptData);
+      } catch (error) {
+        toast.error('获取部门信息失败');
+      } finally {
+        setFormLoading(false);
+      }
+    }
+  };
+
+  useImperativeHandle(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;
+      if (formType === 'create') {
+        await deptApi.createDept(data);
+        toast.success('创建成功');
+      } else {
+        await deptApi.updateDept(data);
+        toast.success('更新成功');
+      }
+      setDialogVisible(false);
+      onSuccess?.();
+    } catch (error: any) {
+      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>
+    ));
+  };
+
+  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>
+
+              {/* 负责人 */}
+              <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>
+
+              {/* 联系电话 */}
+              <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>
+
+              {/* 邮箱 */}
+              <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>
+
+              {/* 状态 */}
+              <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>
+
+        {/* 弹窗底部 */}
+        <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"
+          >
+            取消
+          </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"
+          >
+            确定
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+});
+
+DeptForm.displayName = 'DeptForm';
+
+export default DeptForm;
+

+ 2 - 2
src/components/ui/dialog.tsx

@@ -39,7 +39,7 @@ const DialogOverlay = React.forwardRef<
       ref={ref}
       data-slot="dialog-overlay"
       className={cn(
-        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[100] bg-black/50",
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[9998] bg-black/50",
         className,
       )}
       {...props}
@@ -59,7 +59,7 @@ const DialogContent = React.forwardRef<
         ref={ref}
         data-slot="dialog-content"
         className={cn(
-          "bg-white data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[101] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          "bg-white data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[9999] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
           className,
         )}
         {...props}

+ 11 - 0
src/utils/constants.ts

@@ -0,0 +1,11 @@
+/**
+ * 枚举类
+ */
+
+// ========== COMMON 模块 ==========
+// 全局通用状态枚举
+export const CommonStatusEnum = {
+  ENABLE: 0, // 开启
+  DISABLE: 1, // 禁用
+};
+

+ 166 - 0
src/utils/dict.ts

@@ -0,0 +1,166 @@
+/**
+ * 数据字典工具类
+ */
+
+export interface DictDataType {
+  dictType: string;
+  label: string;
+  value: string | number | boolean;
+  colorType?: string;
+  cssClass?: string;
+}
+
+export interface NumberDictDataType extends DictDataType {
+  value: number;
+}
+
+export interface StringDictDataType extends DictDataType {
+  value: string;
+}
+
+// 字典数据缓存(简化版,实际项目中可以从 API 获取)
+const dictCache: Map<string, DictDataType[]> = new Map();
+
+/**
+ * 获取 dictType 对应的数据字典数组
+ * @param dictType 数据类型
+ * @returns 数据字典数组
+ */
+export const getDictOptions = (dictType: string): DictDataType[] => {
+  // 如果缓存中有数据,直接返回
+  if (dictCache.has(dictType)) {
+    return dictCache.get(dictType) || [];
+  }
+
+  // 如果没有缓存,返回默认数据(用于开发测试)
+  if (dictType === DICT_TYPE.COMMON_STATUS) {
+    return [
+      { dictType, label: '启用', value: 0, colorType: 'success', cssClass: '' },
+      { dictType, label: '禁用', value: 1, colorType: 'danger', cssClass: '' },
+    ];
+  }
+
+  // 其他类型返回空数组
+  // 实际项目中可以在这里调用 API 获取字典数据
+  return [];
+};
+
+/**
+ * 设置字典数据(用于从 API 获取后缓存)
+ * @param dictType 字典类型
+ * @param data 字典数据
+ */
+export const setDictOptions = (dictType: string, data: DictDataType[]): void => {
+  dictCache.set(dictType, data);
+};
+
+/**
+ * 获取整数类型的字典选项
+ * @param dictType 字典类型
+ * @returns 整数类型的字典选项数组
+ */
+export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
+  const dictOptions: DictDataType[] = getDictOptions(dictType);
+  const dictOption: NumberDictDataType[] = [];
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: parseInt(String(dict.value)),
+    });
+  });
+  return dictOption;
+};
+
+/**
+ * 获取字符串类型的字典选项
+ * @param dictType 字典类型
+ * @returns 字符串类型的字典选项数组
+ */
+export const getStrDictOptions = (dictType: string): StringDictDataType[] => {
+  const dictOptions: DictDataType[] = getDictOptions(dictType);
+  const dictOption: StringDictDataType[] = [];
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: String(dict.value),
+    });
+  });
+  return dictOption;
+};
+
+/**
+ * 获取布尔类型的字典选项
+ * @param dictType 字典类型
+ * @returns 布尔类型的字典选项数组
+ */
+export const getBoolDictOptions = (dictType: string): DictDataType[] => {
+  const dictOption: DictDataType[] = [];
+  const dictOptions: DictDataType[] = getDictOptions(dictType);
+  dictOptions.forEach((dict: DictDataType) => {
+    dictOption.push({
+      ...dict,
+      value: String(dict.value) === 'true',
+    });
+  });
+  return dictOption;
+};
+
+/**
+ * 获取指定字典类型的指定值对应的字典对象
+ * @param dictType 字典类型
+ * @param value 字典值
+ * @returns 字典对象
+ */
+export const getDictObj = (dictType: string, value: any): DictDataType | undefined => {
+  const dictOptions: DictDataType[] = getDictOptions(dictType);
+  for (const dict of dictOptions) {
+    if (String(dict.value) === String(value)) {
+      return dict;
+    }
+  }
+  return undefined;
+};
+
+/**
+ * 获得字典数据的文本展示
+ * @param dictType 字典类型
+ * @param value 字典数据的值
+ * @returns 字典名称
+ */
+export const getDictLabel = (dictType: string, value: any): string => {
+  const dictOptions: DictDataType[] = getDictOptions(dictType);
+  for (const dict of dictOptions) {
+    if (String(dict.value) === String(value)) {
+      return dict.label;
+    }
+  }
+  return '';
+};
+
+/**
+ * 字典类型枚举
+ */
+export enum DICT_TYPE {
+  USER_TYPE = 'user_type',
+  COMMON_STATUS = 'common_status',
+  TERMINAL = 'terminal', // 终端
+  DATE_INTERVAL = 'date_interval', // 数据间隔
+
+  // ========== SYSTEM 模块 ==========
+  SYSTEM_USER_SEX = 'system_user_sex',
+  SYSTEM_MENU_TYPE = 'system_menu_type',
+  SYSTEM_ROLE_TYPE = 'system_role_type',
+  SYSTEM_DATA_SCOPE = 'system_data_scope',
+  SYSTEM_NOTICE_TYPE = 'system_notice_type',
+  SYSTEM_LOGIN_TYPE = 'system_login_type',
+  SYSTEM_LOGIN_RESULT = 'system_login_result',
+  SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
+  SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type',
+  SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status',
+  SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
+  SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
+  SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
+  SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
+  SYSTEM_SOCIAL_TYPE = 'system_social_type',
+}
+

+ 67 - 0
src/utils/formatTime.ts

@@ -0,0 +1,67 @@
+/**
+ * 时间日期格式化工具
+ */
+
+/**
+ * 时间日期转换
+ * @param date 当前时间,Date 格式或字符串
+ * @param format 需要转换的时间格式字符串,默认为 'YYYY-MM-DD HH:mm:ss'
+ * @returns 返回拼接后的时间字符串
+ */
+export function formatDate(date: Date | string | number | null | undefined, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
+  // 日期不存在,则返回空
+  if (!date && date !== 0) {
+    return '';
+  }
+
+  let d: Date;
+  
+  // 处理不同类型的输入
+  if (date instanceof Date) {
+    d = date;
+  } else if (typeof date === 'number') {
+    // 如果是数字,可能是时间戳(秒或毫秒)
+    d = new Date(date > 1000000000000 ? date : date * 1000);
+  } else if (typeof date === 'string') {
+    d = new Date(date);
+  } else {
+    return '';
+  }
+
+  // 检查日期是否有效
+  if (!(d instanceof Date) || isNaN(d.getTime())) {
+    return '';
+  }
+
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, '0');
+  const day = String(d.getDate()).padStart(2, '0');
+  const hours = String(d.getHours()).padStart(2, '0');
+  const minutes = String(d.getMinutes()).padStart(2, '0');
+  const seconds = String(d.getSeconds()).padStart(2, '0');
+
+  return format
+    .replace('YYYY', String(year))
+    .replace('MM', month)
+    .replace('DD', day)
+    .replace('HH', hours)
+    .replace('mm', minutes)
+    .replace('ss', seconds);
+}
+
+/**
+ * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD HH:mm:ss 格式
+ * @param cellValue 字段值
+ */
+export function dateFormatter(cellValue: any): string {
+  return cellValue ? formatDate(cellValue) : '';
+}
+
+/**
+ * element plus 的时间 Formatter 实现,使用 YYYY-MM-DD 格式
+ * @param cellValue 字段值
+ */
+export function dateFormatter2(cellValue: any): string {
+  return cellValue ? formatDate(cellValue, 'YYYY-MM-DD') : '';
+}
+