DepartmentManagement.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import React, { useState } from 'react';
  2. import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown, Building2 } from 'lucide-react';
  3. import PermissionWrapper from './PermissionWrapper';
  4. import { hasPermission } from '../utils/permission';
  5. interface DepartmentNode {
  6. id: number;
  7. name: string;
  8. manager?: string;
  9. phone?: string;
  10. email?: string;
  11. memberCount?: number;
  12. createTime?: string;
  13. parentId?: number;
  14. children?: DepartmentNode[];
  15. }
  16. export default function DepartmentManagement() {
  17. const [expandedDeptIds, setExpandedDeptIds] = useState<number[]>([1]); // 默认展开总公司
  18. const [selectedDeptId, setSelectedDeptId] = useState<number | null>(1); // 选中的部门ID
  19. const [searchTerm, setSearchTerm] = useState('');
  20. const [showAddModal, setShowAddModal] = useState(false);
  21. const [editingItem, setEditingItem] = useState<any>(null);
  22. // 部门树形数据
  23. const departmentTree: DepartmentNode[] = [
  24. {
  25. id: 1,
  26. name: '总公司',
  27. manager: '李总',
  28. phone: '138001380 00',
  29. email: 'ceo@company.com',
  30. memberCount: 120,
  31. createTime: '2024-01-01',
  32. children: [
  33. {
  34. id: 2,
  35. name: '技术中心',
  36. manager: '张三',
  37. phone: '13800138001',
  38. email: 'tech@company.com',
  39. memberCount: 45,
  40. createTime: '2024-01-05',
  41. parentId: 1,
  42. children: [
  43. {
  44. id: 21,
  45. name: '研发部',
  46. manager: '王研',
  47. phone: '13800138011',
  48. email: 'rd@company.com',
  49. memberCount: 25,
  50. createTime: '2024-01-10',
  51. parentId: 2
  52. },
  53. {
  54. id: 22,
  55. name: '测试部',
  56. manager: '赵测',
  57. phone: '13800138012',
  58. email: 'qa@company.com',
  59. memberCount: 12,
  60. createTime: '2024-01-10',
  61. parentId: 2
  62. },
  63. {
  64. id: 23,
  65. name: '运维部',
  66. manager: '李运',
  67. phone: '13800138013',
  68. email: 'ops@company.com',
  69. memberCount: 8,
  70. createTime: '2024-01-10',
  71. parentId: 2
  72. }
  73. ]
  74. },
  75. {
  76. id: 3,
  77. name: '市场中心',
  78. manager: '李四',
  79. phone: '13800138002',
  80. email: 'market@company.com',
  81. memberCount: 30,
  82. createTime: '2024-01-05',
  83. parentId: 1,
  84. children: [
  85. {
  86. id: 31,
  87. name: '市场部',
  88. manager: '钱市',
  89. phone: '13800138021',
  90. email: 'marketing@company.com',
  91. memberCount: 18,
  92. createTime: '2024-01-10',
  93. parentId: 3
  94. },
  95. {
  96. id: 32,
  97. name: '销售部',
  98. manager: '孙销',
  99. phone: '13800138022',
  100. email: 'sales@company.com',
  101. memberCount: 12,
  102. createTime: '2024-01-10',
  103. parentId: 3
  104. }
  105. ]
  106. },
  107. {
  108. id: 4,
  109. name: '行政中心',
  110. manager: '王五',
  111. phone: '13800138003',
  112. email: 'admin@company.com',
  113. memberCount: 25,
  114. createTime: '2024-01-05',
  115. parentId: 1,
  116. children: [
  117. {
  118. id: 41,
  119. name: '人力资源部',
  120. manager: '周人',
  121. phone: '13800138031',
  122. email: 'hr@company.com',
  123. memberCount: 10,
  124. createTime: '2024-01-10',
  125. parentId: 4
  126. },
  127. {
  128. id: 42,
  129. name: '财务部',
  130. manager: '吴财',
  131. phone: '13800138032',
  132. email: 'finance@company.com',
  133. memberCount: 8,
  134. createTime: '2024-01-10',
  135. parentId: 4
  136. },
  137. {
  138. id: 43,
  139. name: '行政部',
  140. manager: '郑行',
  141. phone: '13800138033',
  142. email: 'office@company.com',
  143. memberCount: 7,
  144. createTime: '2024-01-10',
  145. parentId: 4
  146. }
  147. ]
  148. },
  149. {
  150. id: 5,
  151. name: '安全部',
  152. manager: '赵六',
  153. phone: '13800138004',
  154. email: 'security@company.com',
  155. memberCount: 20,
  156. createTime: '2024-01-05',
  157. parentId: 1
  158. }
  159. ]
  160. }
  161. ];
  162. // 切换展开/收起
  163. const toggleDeptExpand = (id: number) => {
  164. setExpandedDeptIds(prev =>
  165. prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
  166. );
  167. };
  168. // 获取所有部门(扁平化)
  169. const getAllDepartments = (nodes: DepartmentNode[], result: DepartmentNode[] = []): DepartmentNode[] => {
  170. nodes.forEach(node => {
  171. result.push(node);
  172. if (node.children) {
  173. getAllDepartments(node.children, result);
  174. }
  175. });
  176. return result;
  177. };
  178. const allDepartments = getAllDepartments(departmentTree);
  179. // 根据选中的部门ID过滤显示的部门列表
  180. const getFilteredDepartments = () => {
  181. if (!selectedDeptId) return allDepartments;
  182. // 找到选中的部门
  183. const selectedDept = allDepartments.find(d => d.id === selectedDeptId);
  184. if (!selectedDept) return allDepartments;
  185. // 如果有子部门,只显示直接子部门
  186. if (selectedDept.children && selectedDept.children.length > 0) {
  187. return selectedDept.children;
  188. }
  189. // 如果没有子部门,返回自己
  190. return [selectedDept];
  191. };
  192. const displayedDepartments = getFilteredDepartments().filter((dept) =>
  193. dept.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
  194. dept.manager?.toLowerCase().includes(searchTerm.toLowerCase())
  195. );
  196. // 递归渲染树形节点
  197. const renderTreeNode = (node: DepartmentNode, level: number = 0) => {
  198. const isExpanded = expandedDeptIds.includes(node.id);
  199. const isSelected = selectedDeptId === node.id;
  200. const hasChildren = node.children && node.children.length > 0;
  201. return (
  202. <div key={node.id}>
  203. <div
  204. className={`flex items-center gap-2 px-3 py-2.5 cursor-pointer transition-all rounded-lg ${
  205. isSelected
  206. ? 'bg-blue-100 text-blue-700'
  207. : 'hover:bg-gray-100 text-gray-700'
  208. }`}
  209. style={{ paddingLeft: `${level * 16 + 12}px` }}
  210. onClick={() => setSelectedDeptId(node.id)}
  211. >
  212. {hasChildren ? (
  213. <button
  214. onClick={(e) => {
  215. e.stopPropagation();
  216. toggleDeptExpand(node.id);
  217. }}
  218. className="p-0.5 hover:bg-gray-200 rounded transition-colors"
  219. >
  220. {isExpanded ? (
  221. <ChevronDown className="w-4 h-4" />
  222. ) : (
  223. <ChevronRight className="w-4 h-4" />
  224. )}
  225. </button>
  226. ) : (
  227. <span className="w-5"></span>
  228. )}
  229. <Building2 className="w-4 h-4" />
  230. <span className="text-sm flex-1">{node.name}</span>
  231. {node.memberCount && (
  232. <span className="text-xs px-2 py-0.5 bg-gray-200 rounded">
  233. {node.memberCount}人
  234. </span>
  235. )}
  236. </div>
  237. {isExpanded && hasChildren && (
  238. <div>
  239. {node.children!.map(child => renderTreeNode(child, level + 1))}
  240. </div>
  241. )}
  242. </div>
  243. );
  244. };
  245. const handleDelete = (id: number) => {
  246. if (confirm('确定要删除这个部门吗?')) {
  247. console.log('删除部门:', id);
  248. }
  249. };
  250. const handleEdit = (item: any) => {
  251. setEditingItem(item);
  252. setShowAddModal(true);
  253. };
  254. return (
  255. <div className="flex gap-6 h-full">
  256. {/* 左侧树形结构 */}
  257. <div className="w-80 flex-shrink-0">
  258. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4 h-full flex flex-col">
  259. <div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-200">
  260. <h3 className="text-base text-gray-900">组织架构</h3>
  261. <PermissionWrapper permission="system:dept:create">
  262. <button
  263. onClick={() => {
  264. setEditingItem(null);
  265. setShowAddModal(true);
  266. }}
  267. className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
  268. title="新增部门"
  269. >
  270. <Plus className="w-4 h-4" />
  271. </button>
  272. </PermissionWrapper>
  273. </div>
  274. <div className="flex-1 overflow-y-auto">
  275. {departmentTree.map(node => renderTreeNode(node))}
  276. </div>
  277. </div>
  278. </div>
  279. {/* 右侧列表 */}
  280. <div className="flex-1 space-y-6">
  281. {/* 工具栏 */}
  282. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4">
  283. <div className="flex items-center justify-between">
  284. <div className="relative w-80">
  285. <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  286. <input
  287. type="text"
  288. placeholder="搜索部门..."
  289. value={searchTerm}
  290. onChange={(e) => setSearchTerm(e.target.value)}
  291. 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"
  292. />
  293. </div>
  294. <PermissionWrapper permission="system:dept:create">
  295. <button
  296. onClick={() => {
  297. setEditingItem(null);
  298. setShowAddModal(true);
  299. }}
  300. 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"
  301. >
  302. <Plus className="w-4 h-4" strokeWidth={2.5} />
  303. <span className="text-sm">新增部门</span>
  304. </button>
  305. </PermissionWrapper>
  306. </div>
  307. </div>
  308. {/* 表格 */}
  309. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  310. <div className="overflow-x-auto">
  311. <table className="w-full">
  312. <thead>
  313. <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
  314. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
  315. 序号
  316. </th>
  317. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
  318. 部门名称
  319. </th>
  320. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
  321. 负责人
  322. </th>
  323. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
  324. 联系电话
  325. </th>
  326. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '20%' }}>
  327. 邮箱
  328. </th>
  329. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
  330. 成员数
  331. </th>
  332. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
  333. 创建时间
  334. </th>
  335. <th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '15%' }}>
  336. 操作
  337. </th>
  338. </tr>
  339. </thead>
  340. <tbody className="divide-y divide-gray-100">
  341. {displayedDepartments.map((dept, index) => (
  342. <tr
  343. key={dept.id}
  344. className="hover:bg-blue-50/30 transition-colors"
  345. >
  346. <td className="px-6 py-4 text-sm text-gray-900">
  347. {index + 1}
  348. </td>
  349. <td className="px-6 py-4 text-sm text-gray-900">{dept.name}</td>
  350. <td className="px-6 py-4 text-sm text-gray-900">{dept.manager}</td>
  351. <td className="px-6 py-4 text-sm text-gray-900">{dept.phone}</td>
  352. <td className="px-6 py-4 text-sm text-gray-900">{dept.email}</td>
  353. <td className="px-6 py-4 text-sm text-gray-900">{dept.memberCount}</td>
  354. <td className="px-6 py-4 text-sm text-gray-900">{dept.createTime}</td>
  355. <td className="px-6 py-4">
  356. <div className="flex items-center justify-center gap-2">
  357. <PermissionWrapper permission="system:dept:update">
  358. <button
  359. onClick={() => handleEdit(dept)}
  360. className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
  361. title="编辑"
  362. >
  363. <Edit2 className="w-4 h-4" />
  364. </button>
  365. </PermissionWrapper>
  366. <PermissionWrapper permission="system:dept:delete">
  367. <button
  368. onClick={() => handleDelete(dept.id)}
  369. className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
  370. title="删除"
  371. >
  372. <Trash2 className="w-4 h-4" />
  373. </button>
  374. </PermissionWrapper>
  375. <button
  376. className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
  377. title="更多"
  378. >
  379. <MoreVertical className="w-4 h-4" />
  380. </button>
  381. </div>
  382. </td>
  383. </tr>
  384. ))}
  385. </tbody>
  386. </table>
  387. </div>
  388. {/* 分页 */}
  389. <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200">
  390. <div className="flex items-center justify-between">
  391. <div className="text-sm text-gray-600">
  392. 共 <span className="text-blue-600">{displayedDepartments.length}</span> 个部门
  393. </div>
  394. <div className="flex gap-2">
  395. <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">
  396. 上一页
  397. </button>
  398. <button className="px-4 py-2 text-sm text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors">
  399. 1
  400. </button>
  401. <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">
  402. 下一页
  403. </button>
  404. </div>
  405. </div>
  406. </div>
  407. </div>
  408. </div>
  409. {/* 新增/编辑弹窗 */}
  410. {showAddModal && (
  411. <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
  412. <div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 animate-in zoom-in duration-200">
  413. {/* 弹窗标题 */}
  414. <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
  415. <h3 className="text-lg text-gray-900">
  416. {editingItem ? '编辑部门' : '新增部门'}
  417. </h3>
  418. <button
  419. onClick={() => setShowAddModal(false)}
  420. className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
  421. >
  422. <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  423. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
  424. </svg>
  425. </button>
  426. </div>
  427. {/* 弹窗内容 */}
  428. <div className="px-6 py-6">
  429. <div className="grid grid-cols-2 gap-4">
  430. <div>
  431. <label className="block text-sm text-gray-700 mb-2">部门名称 *</label>
  432. <input
  433. type="text"
  434. 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"
  435. placeholder="请输入部门名称"
  436. defaultValue={editingItem?.name || ''}
  437. />
  438. </div>
  439. <div>
  440. <label className="block text-sm text-gray-700 mb-2">上级部门</label>
  441. <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">
  442. <option value="">请选择上级部门</option>
  443. {allDepartments.map(dept => (
  444. <option key={dept.id} value={dept.id}>{dept.name}</option>
  445. ))}
  446. </select>
  447. </div>
  448. <div>
  449. <label className="block text-sm text-gray-700 mb-2">负责人</label>
  450. <input
  451. type="text"
  452. 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"
  453. placeholder="请输入负责人"
  454. defaultValue={editingItem?.manager || ''}
  455. />
  456. </div>
  457. <div>
  458. <label className="block text-sm text-gray-700 mb-2">联系电话</label>
  459. <input
  460. type="text"
  461. 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"
  462. placeholder="请输入联系电话"
  463. defaultValue={editingItem?.phone || ''}
  464. />
  465. </div>
  466. <div className="col-span-2">
  467. <label className="block text-sm text-gray-700 mb-2">邮箱</label>
  468. <input
  469. type="email"
  470. 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"
  471. placeholder="请输入邮箱"
  472. defaultValue={editingItem?.email || ''}
  473. />
  474. </div>
  475. </div>
  476. </div>
  477. {/* 弹窗底部 */}
  478. <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
  479. <button
  480. onClick={() => setShowAddModal(false)}
  481. 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"
  482. >
  483. 取消
  484. </button>
  485. <button
  486. onClick={() => {
  487. console.log('保存部门:', editingItem);
  488. setShowAddModal(false);
  489. }}
  490. 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"
  491. >
  492. 确定
  493. </button>
  494. </div>
  495. </div>
  496. </div>
  497. )}
  498. </div>
  499. );
  500. }