| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- import React, { useState } from 'react';
- import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown, Building2 } from 'lucide-react';
- 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[];
- }
- 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 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 toggleDeptExpand = (id: number) => {
- setExpandedDeptIds(prev =>
- prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
- );
- };
- // 获取所有部门(扁平化)
- const getAllDepartments = (nodes: DepartmentNode[], result: DepartmentNode[] = []): DepartmentNode[] => {
- nodes.forEach(node => {
- result.push(node);
- if (node.children) {
- getAllDepartments(node.children, result);
- }
- });
- return result;
- };
- 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 displayedDepartments = getFilteredDepartments().filter((dept) =>
- dept.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- dept.manager?.toLowerCase().includes(searchTerm.toLowerCase())
- );
- // 递归渲染树形节点
- 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;
- 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>
- );
- };
- const handleDelete = (id: number) => {
- if (confirm('确定要删除这个部门吗?')) {
- console.log('删除部门:', id);
- }
- };
- const handleEdit = (item: any) => {
- setEditingItem(item);
- setShowAddModal(true);
- };
- 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>
- </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>
- <PermissionWrapper permission="system:dept:create">
- <button
- onClick={() => {
- setEditingItem(null);
- setShowAddModal(true);
- }}
- 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>
- </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>
- </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="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>
- )}
- </div>
- );
- }
|