DepartmentManagement.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { Plus, Search, RefreshCw, ChevronRight, ChevronDown, Edit2, Trash2 } from 'lucide-react';
  3. import { Modal } from 'antd';
  4. import { ExclamationCircleOutlined } from '@ant-design/icons';
  5. import { Button } from './ui/button';
  6. import { Button as AntButton, Input, Space } from 'antd';
  7. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
  8. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
  9. import PermissionWrapper from './PermissionWrapper';
  10. import { deptApi, DeptVO } from '../api/dept';
  11. import { userApi } from '../api/user';
  12. import { UserVO } from '../types';
  13. import { toast } from 'sonner';
  14. import { DICT_TYPE, getIntDictOptions, getDictLabel } from '../utils/dict';
  15. import { dateFormatter } from '../utils/formatTime';
  16. import { handleTree, TreeNode } from '../utils/tree';
  17. import DeptForm, { DeptFormRef } from './dept/DeptForm';
  18. export default function DepartmentManagement() {
  19. const [loading, setLoading] = useState(true);
  20. const [list, setList] = useState<TreeNode[]>([]);
  21. const [queryParams, setQueryParams] = useState({
  22. pageNo: 1,
  23. pageSize: 100,
  24. name: '',
  25. status: undefined as number | undefined,
  26. });
  27. const [isExpandAll, setIsExpandAll] = useState(true);
  28. const [refreshTable, setRefreshTable] = useState(true);
  29. const [userList, setUserList] = useState<UserVO[]>([]);
  30. const [statusOptions] = useState(() => {
  31. try {
  32. return getIntDictOptions(DICT_TYPE.COMMON_STATUS);
  33. } catch (error) {
  34. console.error('获取字典选项失败:', error);
  35. return [];
  36. }
  37. });
  38. const formRef = useRef<DeptFormRef>(null);
  39. // 查询部门列表
  40. const getList = async () => {
  41. setLoading(true);
  42. try {
  43. const data = await deptApi.getDeptPage(queryParams);
  44. // 过滤掉没有 id 的项,并确保 id 存在
  45. const validDeptList = (data || []).filter((dept): dept is DeptVO & { id: number } => !!dept.id);
  46. const treeData = handleTree(validDeptList);
  47. setList(treeData);
  48. } catch (error: any) {
  49. console.error('获取部门列表失败:', error);
  50. toast.error(error.message || '获取部门列表失败');
  51. setList([]); // 设置空数组,确保页面能显示
  52. } finally {
  53. setLoading(false);
  54. }
  55. };
  56. // 展开/折叠操作
  57. const toggleExpandAll = () => {
  58. setRefreshTable(false);
  59. setIsExpandAll(!isExpandAll);
  60. setTimeout(() => {
  61. setRefreshTable(true);
  62. }, 0);
  63. };
  64. // 搜索按钮操作
  65. const handleQuery = () => {
  66. getList();
  67. };
  68. // 重置按钮操作
  69. const resetQuery = () => {
  70. const resetParams = {
  71. pageNo: 1,
  72. pageSize: 100,
  73. name: '',
  74. status: undefined as number | undefined,
  75. };
  76. setQueryParams(resetParams);
  77. // 立即使用重置后的参数获取列表
  78. setLoading(true);
  79. deptApi.getDeptPage(resetParams)
  80. .then((data) => {
  81. // 过滤掉没有 id 的项,并确保 id 存在
  82. const validDeptList = (data || []).filter((dept): dept is DeptVO & { id: number } => !!dept.id);
  83. const treeData = handleTree(validDeptList);
  84. setList(treeData);
  85. })
  86. .catch((error: any) => {
  87. console.error('获取部门列表失败:', error);
  88. toast.error(error.message || '获取部门列表失败');
  89. setList([]);
  90. })
  91. .finally(() => {
  92. setLoading(false);
  93. });
  94. };
  95. // 添加/修改操作
  96. const openForm = (type: string, id?: number) => {
  97. formRef.current?.open(type, id);
  98. };
  99. // 删除按钮操作
  100. const handleDelete = async (id: number, name: string) => {
  101. Modal.confirm({
  102. title: '确认删除',
  103. icon: <ExclamationCircleOutlined />,
  104. content: (
  105. <div>
  106. <p>确定要删除部门 <strong>"{name}"</strong> 吗?</p>
  107. <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
  108. </div>
  109. ),
  110. okText: '确定删除',
  111. okType: 'danger',
  112. cancelText: '取消',
  113. onOk: async () => {
  114. try {
  115. await deptApi.deleteDept(id);
  116. toast.success('删除成功');
  117. await getList();
  118. } catch (error: any) {
  119. toast.error(error.message || '删除失败');
  120. }
  121. },
  122. });
  123. };
  124. // 获取负责人名称
  125. const getLeaderName = (leaderUserId?: number): string => {
  126. if (!leaderUserId) return '';
  127. const user = userList.find((u) => u.id === leaderUserId);
  128. return user?.nickname || '';
  129. };
  130. // 递归渲染表格行
  131. const renderTableRows = (nodes: TreeNode[], level: number = 0): React.ReactNode[] => {
  132. return nodes.map((node) => {
  133. const hasChildren = node.children && node.children.length > 0;
  134. const isExpanded = isExpandAll; // 简化处理,实际可以根据状态控制
  135. return (
  136. <React.Fragment key={node.id}>
  137. <TableRow className="hover:bg-gray-50">
  138. <TableCell style={{ paddingLeft: `${level * 24 + 12}px` }} className="font-medium">
  139. <div className="flex items-center gap-2">
  140. {hasChildren ? (
  141. <button
  142. onClick={() => {
  143. // 这里可以实现单个节点的展开/折叠
  144. }}
  145. className="p-0.5 hover:bg-gray-200 rounded transition-colors"
  146. >
  147. {isExpanded ? (
  148. <ChevronDown className="w-4 h-4" />
  149. ) : (
  150. <ChevronRight className="w-4 h-4" />
  151. )}
  152. </button>
  153. ) : (
  154. <span className="w-5"></span>
  155. )}
  156. <span>{(node as any).name || '未命名'}</span>
  157. </div>
  158. </TableCell>
  159. <TableCell className="text-sm text-gray-600">{getLeaderName((node as any).leaderUserId) || '-'}</TableCell>
  160. <TableCell className="text-center">{(node as any).sort || 0}</TableCell>
  161. <TableCell className="text-center">
  162. <span className="text-sm text-gray-600">
  163. {getDictLabel(DICT_TYPE.COMMON_STATUS, (node as any).status) || '未知'}
  164. </span>
  165. </TableCell>
  166. <TableCell className="text-sm text-gray-600">
  167. {(node as any).createTime ? dateFormatter((node as any).createTime) : '-'}
  168. </TableCell>
  169. <TableCell>
  170. <div className="flex items-center gap-2 justify-center">
  171. <PermissionWrapper permission="system:dept:update">
  172. <Button
  173. variant="ghost"
  174. size="sm"
  175. onClick={() => openForm('update', node.id)}
  176. className="h-8 px-2"
  177. >
  178. <Edit2 className="w-4 h-4" />
  179. <span className="ml-1">编辑</span>
  180. </Button>
  181. </PermissionWrapper>
  182. <PermissionWrapper permission="system:dept:delete">
  183. <Button
  184. variant="ghost"
  185. size="sm"
  186. onClick={() => handleDelete(node.id, (node as any).name || '未命名部门')}
  187. className="h-8 px-2 text-red-600 hover:text-red-700"
  188. >
  189. <Trash2 className="w-4 h-4"/>
  190. <span className="ml-1">删除</span>
  191. </Button>
  192. </PermissionWrapper>
  193. </div>
  194. </TableCell>
  195. </TableRow>
  196. {hasChildren && isExpanded && renderTableRows(node.children!, level + 1)}
  197. </React.Fragment>
  198. );
  199. });
  200. };
  201. // 初始化
  202. useEffect(() => {
  203. console.log('DepartmentManagement 组件已挂载,开始获取数据');
  204. const initData = async () => {
  205. try {
  206. console.log('开始调用 getList() 获取部门列表');
  207. await getList();
  208. console.log('部门列表获取成功');
  209. } catch (error) {
  210. console.error('获取部门列表失败:', error);
  211. // 即使失败也设置 loading 为 false,显示页面
  212. setLoading(false);
  213. }
  214. // 获取用户列表
  215. try {
  216. console.log('开始获取用户列表');
  217. const users = await userApi.getSimpleUserList();
  218. setUserList(users);
  219. console.log('用户列表获取成功,用户数量:', users.length);
  220. } catch (error) {
  221. console.error('获取用户列表失败:', error);
  222. }
  223. };
  224. initData();
  225. // eslint-disable-next-line react-hooks/exhaustive-deps
  226. }, []);
  227. return (
  228. <div className="space-y-4">
  229. {/* 搜索栏 */}
  230. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-4 lg:p-5">
  231. <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
  232. {/* 搜索输入框 */}
  233. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  234. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  235. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">部门名称:</label>
  236. <Input
  237. value={queryParams.name || ''}
  238. onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
  239. onPressEnter={handleQuery}
  240. placeholder="请输入部门名称"
  241. className="min-w-[150px] max-w-[200px]"
  242. allowClear
  243. />
  244. </div>
  245. </div>
  246. {/* 操作按钮组 */}
  247. <Space className="flex-shrink-0">
  248. <PermissionWrapper permission="system:dept:query">
  249. <AntButton
  250. type="primary"
  251. icon={<Search className="w-4 h-4" />}
  252. onClick={handleQuery}
  253. >
  254. 搜索
  255. </AntButton>
  256. </PermissionWrapper>
  257. <PermissionWrapper permission="system:dept:query">
  258. <AntButton
  259. icon={<RefreshCw className="w-4 h-4" />}
  260. onClick={resetQuery}
  261. >
  262. 重置
  263. </AntButton>
  264. </PermissionWrapper>
  265. <PermissionWrapper permission="system:dept:create">
  266. <AntButton
  267. type="primary"
  268. icon={<Plus className="w-4 h-4" />}
  269. onClick={() => openForm('create')}
  270. >
  271. 新增
  272. </AntButton>
  273. </PermissionWrapper>
  274. <AntButton
  275. icon={isExpandAll ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
  276. onClick={toggleExpandAll}
  277. >
  278. {isExpandAll ? '折叠' : '展开'}
  279. </AntButton>
  280. </Space>
  281. </div>
  282. </div>
  283. {/* 表格 */}
  284. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  285. {refreshTable && (
  286. <Table>
  287. <TableHeader>
  288. <TableRow>
  289. <TableHead className="w-[250px]">部门名称</TableHead>
  290. <TableHead className="w-[150px]">负责人</TableHead>
  291. <TableHead className="w-[80px] text-center">排序</TableHead>
  292. <TableHead className="w-[100px] text-center">状态</TableHead>
  293. <TableHead className="w-[180px]">创建时间</TableHead>
  294. <TableHead className="w-[160px] text-center">操作</TableHead>
  295. </TableRow>
  296. </TableHeader>
  297. <TableBody>
  298. {loading ? (
  299. <TableRow>
  300. <TableCell colSpan={6} className="text-center py-8 text-gray-500">
  301. 加载中...
  302. </TableCell>
  303. </TableRow>
  304. ) : list.length === 0 ? (
  305. <TableRow>
  306. <TableCell colSpan={6} className="text-center py-8 text-gray-500">
  307. 暂无数据
  308. </TableCell>
  309. </TableRow>
  310. ) : (
  311. renderTableRows(list)
  312. )}
  313. </TableBody>
  314. </Table>
  315. )}
  316. </div>
  317. {/* 表单弹窗 */}
  318. <DeptForm ref={formRef} onSuccess={getList} />
  319. </div>
  320. );
  321. }