| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- import React, { useState, useEffect, useRef } from 'react';
- import { Plus, Search, RefreshCw, ChevronRight, ChevronDown, Edit2, Trash2 } from 'lucide-react';
- import { Modal } from 'antd';
- import { ExclamationCircleOutlined } from '@ant-design/icons';
- import { Button } from './ui/button';
- import { Button as AntButton, Input, Space } from 'antd';
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
- import PermissionWrapper from './PermissionWrapper';
- 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 [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 getList = async () => {
- setLoading(true);
- try {
- const data = await deptApi.getDeptPage(queryParams);
- // 过滤掉没有 id 的项,并确保 id 存在
- const validDeptList = (data || []).filter((dept): dept is DeptVO & { id: number } => !!dept.id);
- const treeData = handleTree(validDeptList);
- setList(treeData);
- } catch (error: any) {
- console.error('获取部门列表失败:', error);
- toast.error(error.message || '获取部门列表失败');
- setList([]); // 设置空数组,确保页面能显示
- } finally {
- setLoading(false);
- }
- };
- // 展开/折叠操作
- const toggleExpandAll = () => {
- setRefreshTable(false);
- setIsExpandAll(!isExpandAll);
- setTimeout(() => {
- setRefreshTable(true);
- }, 0);
- };
- // 搜索按钮操作
- const handleQuery = () => {
- getList();
- };
- // 重置按钮操作
- const resetQuery = () => {
- const resetParams = {
- pageNo: 1,
- pageSize: 100,
- name: '',
- status: undefined as number | undefined,
- };
- setQueryParams(resetParams);
- // 立即使用重置后的参数获取列表
- setLoading(true);
- deptApi.getDeptPage(resetParams)
- .then((data) => {
- // 过滤掉没有 id 的项,并确保 id 存在
- const validDeptList = (data || []).filter((dept): dept is DeptVO & { id: number } => !!dept.id);
- const treeData = handleTree(validDeptList);
- setList(treeData);
- })
- .catch((error: any) => {
- console.error('获取部门列表失败:', error);
- toast.error(error.message || '获取部门列表失败');
- setList([]);
- })
- .finally(() => {
- setLoading(false);
- });
- };
- // 添加/修改操作
- const openForm = (type: string, id?: number) => {
- formRef.current?.open(type, id);
- };
- // 删除按钮操作
- const handleDelete = async (id: number, name: string) => {
- Modal.confirm({
- title: '确认删除',
- icon: <ExclamationCircleOutlined />,
- content: (
- <div>
- <p>确定要删除部门 <strong>"{name}"</strong> 吗?</p>
- <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
- </div>
- ),
- okText: '确定删除',
- okType: 'danger',
- cancelText: '取消',
- onOk: async () => {
- try {
- await deptApi.deleteDept(id);
- toast.success('删除成功');
- await getList();
- } catch (error: any) {
- toast.error(error.message || '删除失败');
- }
- },
- });
- };
- // 获取负责人名称
- const getLeaderName = (leaderUserId?: number): string => {
- if (!leaderUserId) return '';
- const user = userList.find((u) => u.id === leaderUserId);
- return user?.nickname || '';
- };
- // 递归渲染表格行
- 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" />
- <span className="ml-1">编辑</span>
- </Button>
- </PermissionWrapper>
- <PermissionWrapper permission="system:dept:delete">
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleDelete(node.id, (node as any).name || '未命名部门')}
- className="h-8 px-2 text-red-600 hover:text-red-700"
- >
- <Trash2 className="w-4 h-4"/>
- <span className="ml-1">删除</span>
- </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="space-y-4">
- {/* 搜索栏 */}
- <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-4 lg:p-5">
- <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
- {/* 搜索输入框 */}
- <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
- <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">部门名称:</label>
- <Input
- value={queryParams.name || ''}
- onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
- onPressEnter={handleQuery}
- placeholder="请输入部门名称"
- className="min-w-[150px] max-w-[200px]"
- allowClear
- />
- </div>
- </div>
- {/* 操作按钮组 */}
- <Space className="flex-shrink-0">
- <PermissionWrapper permission="system:dept:query">
- <AntButton
- type="primary"
- icon={<Search className="w-4 h-4" />}
- onClick={handleQuery}
- >
- 搜索
- </AntButton>
- </PermissionWrapper>
-
- <PermissionWrapper permission="system:dept:query">
- <AntButton
- icon={<RefreshCw className="w-4 h-4" />}
- onClick={resetQuery}
- >
- 重置
- </AntButton>
- </PermissionWrapper>
-
- <PermissionWrapper permission="system:dept:create">
- <AntButton
- type="primary"
- icon={<Plus className="w-4 h-4" />}
- onClick={() => openForm('create')}
- >
- 新增
- </AntButton>
- </PermissionWrapper>
-
- <AntButton
- icon={isExpandAll ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
- onClick={toggleExpandAll}
- >
- {isExpandAll ? '折叠' : '展开'}
- </AntButton>
- </Space>
- </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>
- {/* 表单弹窗 */}
- <DeptForm ref={formRef} onSuccess={getList} />
- </div>
- );
- }
|