import { useState, useCallback, useEffect, useRef } from 'react'; import { Tree, Button, Space, Form, Input, InputNumber, Table, Popconfirm, Empty, Tag, } from 'antd'; import { PlusOutlined, DeleteOutlined, EditOutlined, ApartmentOutlined, } from '@ant-design/icons'; import type { DataNode } from 'antd/es/tree'; import { useThemeMode } from '../hooks/useThemeMode'; import { DrawerForm } from '../components/DrawerForm'; import { useCrudDrawer } from '../hooks/useCrudDrawer'; import { useApiRequest } from '../hooks/useApiRequest'; import { listOrgTree, createOrg, updateOrg, deleteOrg, listDeptTree, createDept, deleteDept, listPositions, createPosition, deletePosition, type OrganizationInfo, type DepartmentInfo, type PositionInfo, } from '../api/orgs'; export default function Organizations() { const isDark = useThemeMode(); const { execute } = useApiRequest(); const cardStyle = { background: isDark ? '#111827' : '#FFFFFF', borderRadius: 12, border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`, }; // --- Org tree state --- const [orgTree, setOrgTree] = useState([]); const [selectedOrg, setSelectedOrg] = useState(null); // --- Department tree state --- const [deptTree, setDeptTree] = useState([]); const [selectedDept, setSelectedDept] = useState(null); // --- Position list state --- const [positions, setPositions] = useState([]); // --- Ref for drawer onSuccess callback (avoids before-declaration issue) --- const refreshOrgTreeRef = useRef<() => void>(() => {}); // --- Fetch org tree --- const fetchOrgTree = useCallback(async () => { try { const tree = await listOrgTree(); setOrgTree(tree); if (selectedOrg) { const stillExists = findOrgInTree(tree, selectedOrg.id); if (!stillExists) { setSelectedOrg(null); setDeptTree([]); setPositions([]); } } } catch { /* silent */ } }, [selectedOrg]); refreshOrgTreeRef.current = () => { fetchOrgTree(); }; useEffect(() => { fetchOrgTree(); }, [fetchOrgTree]); // --- Dept Drawer --- const [deptDrawerOpen, setDeptDrawerOpen] = useState(false); // --- Position Drawer --- const [positionDrawerOpen, setPositionDrawerOpen] = useState(false); // --- Org Drawer (uses ref to avoid before-declaration) --- const orgDrawer = useCrudDrawer({ getId: (r) => r.id, onCreate: async (values) => { await createOrg({ ...(values as { name: string; code?: string; sort_order?: number }), parent_id: selectedOrg?.id }); }, onUpdate: async (id, values) => { await updateOrg(id, values as { name: string; code?: string; sort_order?: number; version: number }); }, onSuccess: () => { refreshOrgTreeRef.current(); }, }); // --- Fetch dept tree --- const fetchDeptTree = useCallback(async () => { if (!selectedOrg) return; try { const tree = await listDeptTree(selectedOrg.id); setDeptTree(tree); if (selectedDept) { const stillExists = findDeptInTree(tree, selectedDept.id); if (!stillExists) { setSelectedDept(null); setPositions([]); } } } catch { /* silent */ } }, [selectedOrg, selectedDept]); useEffect(() => { fetchDeptTree(); }, [fetchDeptTree]); // --- Fetch positions --- const fetchPositions = useCallback(async () => { if (!selectedDept) return; try { setPositions(await listPositions(selectedDept.id)); } catch { /* silent */ } }, [selectedDept]); useEffect(() => { fetchPositions(); }, [fetchPositions]); // --- Org handlers --- const handleDeleteOrg = async (id: string) => { await execute(() => deleteOrg(id), '组织已删除'); setSelectedOrg(null); setDeptTree([]); setPositions([]); fetchOrgTree(); }; // --- Dept handlers --- const handleCreateDept = async (values: Record) => { if (!selectedOrg) return; await execute(() => createDept(selectedOrg.id, { name: values.name as string, code: values.code as string | undefined, parent_id: selectedDept?.id, sort_order: values.sort_order as number | undefined, }), '部门创建成功'); setDeptDrawerOpen(false); fetchDeptTree(); }; const handleDeleteDept = async (id: string) => { await execute(() => deleteDept(id), '部门已删除'); setSelectedDept(null); setPositions([]); fetchDeptTree(); }; // --- Position handlers --- const handleCreatePosition = async (values: Record) => { if (!selectedDept) return; await execute(() => createPosition(selectedDept.id, { name: values.name as string, code: values.code as string | undefined, level: values.level as number | undefined, sort_order: values.sort_order as number | undefined, }), '岗位创建成功'); setPositionDrawerOpen(false); fetchPositions(); }; const handleDeletePosition = async (id: string) => { await execute(() => deletePosition(id), '岗位已删除'); fetchPositions(); }; // --- Tree node converters --- const convertOrgTree = (items: OrganizationInfo[]): DataNode[] => items.map((item) => ({ key: item.id, title: {item.name} {item.code && {item.code}}, children: convertOrgTree(item.children), })); const convertDeptTree = (items: DepartmentInfo[]): DataNode[] => items.map((item) => ({ key: item.id, title: {item.name} {item.code && {item.code}}, children: convertDeptTree(item.children), })); const onSelectOrg = (selectedKeys: React.Key[]) => { if (selectedKeys.length === 0) { setSelectedOrg(null); setDeptTree([]); setSelectedDept(null); setPositions([]); return; } setSelectedOrg(findOrgInTree(orgTree, selectedKeys[0] as string)); setSelectedDept(null); setPositions([]); }; const onSelectDept = (selectedKeys: React.Key[]) => { if (selectedKeys.length === 0) { setSelectedDept(null); setPositions([]); return; } setSelectedDept(findDeptInTree(deptTree, selectedKeys[0] as string)); }; const positionColumns = [ { title: '岗位名称', dataIndex: 'name', key: 'name' }, { title: '编码', dataIndex: 'code', key: 'code', render: (v?: string) => v || '-' }, { title: '级别', dataIndex: 'level', key: 'level' }, { title: '排序', dataIndex: 'sort_order', key: 'sort_order' }, { title: '操作', key: 'actions', render: (_: unknown, record: PositionInfo) => ( handleDeletePosition(record.id)}> ), }, ]; return (

组织架构管理

管理组织、部门和岗位的层级结构
{/* 左栏:组织树 */}
组织
{orgTree.length > 0 ? ( ) : }
{/* 中栏:部门树 */}
{selectedOrg ? `${selectedOrg.name} · 部门` : '部门'} {selectedOrg && (
{selectedOrg ? ( deptTree.length > 0 ? ( ) : ) : }
{/* 右栏:岗位表 */}
{selectedDept ? `${selectedDept.name} · 岗位` : '岗位'} {selectedDept && ( )}
{selectedDept ? ( ) :
} {/* Org Drawer */} {/* Dept Drawer */} setDeptDrawerOpen(false)} onSubmit={handleCreateDept} initialValues={{ sort_order: 0 }} loading={false} width={480} columns={1} > {/* Position Drawer */} setPositionDrawerOpen(false)} onSubmit={handleCreatePosition} initialValues={{ level: 1, sort_order: 0 }} loading={false} width={480} columns={1} > ); } function findOrgInTree(tree: OrganizationInfo[], id: string): OrganizationInfo | null { for (const item of tree) { if (item.id === id) return item; const found = findOrgInTree(item.children, id); if (found) return found; } return null; } function findDeptInTree(tree: DepartmentInfo[], id: string): DepartmentInfo | null { for (const item of tree) { if (item.id === id) return item; const found = findDeptInTree(item.children, id); if (found) return found; } return null; }