|
|
@@ -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>
|
|
|
);
|
|
|
-}
|
|
|
+}
|