/** * filekorolheader: 部门管理页面 - 企业部门树形结构管理页面 * 功能:部门树形管理、拖拽排序、增删改查、层级管理 * 路径:/central-config/user/department * 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式 */ 'use client'; import { useReducer, useEffect, useRef } from 'react'; import { toast } from 'sonner'; import { Department } from './types'; import { DepartmentHeader } from './components/DepartmentHeader'; import { DepartmentStatsCards } from './components/DepartmentStatsCards'; import { DepartmentTree } from './components/DepartmentTree'; import { DepartmentFormDialog } from './components/DepartmentFormDialog'; import { DepartmentDeleteDialog } from './components/DepartmentDeleteDialog'; import { DepartmentInstructions } from './components/DepartmentInstructions'; import { fetchDepartmentTree, transformDepartmentList, flattenDepartments, type DepartmentTreeState } from './components/departmentApi'; import { deleteDepartment } from './components/departmentCreateApi'; // 部门管理状态管理 interface DepartmentManagementState { departments: Department[]; expandedIds: Set; loading: boolean; error: string | null; showForm: boolean; showDeleteDialog: boolean; editingDepartment: Department | null; parentDepartment: Department | null; deletingDepartment: Department | null; draggedItem: { dept: Department; index: number; parentId?: string; } | null; dragOverItem: { index: number; parentId?: string; } | null; } type DepartmentManagementAction = | { type: 'SET_DEPARTMENTS'; payload: Department[] } | { type: 'SET_LOADING'; payload: boolean } | { type: 'SET_ERROR'; payload: string | null } | { type: 'TOGGLE_EXPAND'; payload: string } | { type: 'SET_EXPANDED_IDS'; payload: Set } | { type: 'TOGGLE_FORM'; payload: boolean } | { type: 'TOGGLE_DELETE_DIALOG'; payload: boolean } | { type: 'SET_EDITING_DEPARTMENT'; payload: Department | null } | { type: 'SET_PARENT_DEPARTMENT'; payload: Department | null } | { type: 'SET_DELETING_DEPARTMENT'; payload: Department | null } | { type: 'SET_DRAGGED_ITEM'; payload: { dept: Department; index: number; parentId?: string } | null } | { type: 'SET_DRAG_OVER_ITEM'; payload: { index: number; parentId?: string } | null } | { type: 'REFRESH_DATA' }; const departmentManagementReducer = (state: DepartmentManagementState, action: DepartmentManagementAction): DepartmentManagementState => { switch (action.type) { case 'SET_DEPARTMENTS': return { ...state, departments: action.payload, loading: false, error: null }; case 'SET_LOADING': return { ...state, loading: action.payload }; case 'SET_ERROR': return { ...state, error: action.payload, loading: false }; case 'TOGGLE_EXPAND': const newExpanded = new Set(state.expandedIds); if (newExpanded.has(action.payload)) { newExpanded.delete(action.payload); } else { newExpanded.add(action.payload); } return { ...state, expandedIds: newExpanded }; case 'SET_EXPANDED_IDS': return { ...state, expandedIds: action.payload }; case 'TOGGLE_FORM': return { ...state, showForm: !state.showForm }; case 'TOGGLE_DELETE_DIALOG': return { ...state, showDeleteDialog: !state.showDeleteDialog }; case 'SET_EDITING_DEPARTMENT': return { ...state, editingDepartment: action.payload }; case 'SET_PARENT_DEPARTMENT': return { ...state, parentDepartment: action.payload }; case 'SET_DELETING_DEPARTMENT': return { ...state, deletingDepartment: action.payload }; case 'SET_DRAGGED_ITEM': return { ...state, draggedItem: action.payload }; case 'SET_DRAG_OVER_ITEM': return { ...state, dragOverItem: action.payload }; case 'REFRESH_DATA': return { ...state, error: null }; default: return state; } }; const initialState: DepartmentManagementState = { departments: [], expandedIds: new Set(), loading: false, error: null, showForm: false, showDeleteDialog: false, editingDepartment: null, parentDepartment: null, deletingDepartment: null, draggedItem: null, dragOverItem: null, }; export default function DepartmentManagementPage() { const [state, dispatch] = useReducer(departmentManagementReducer, initialState); const isFirstLoad = useRef(true); // 加载部门数据 const loadDepartments = async () => { try { dispatch({ type: 'SET_LOADING', payload: true }); // 使用API调用获取部门树形数据 const response = await fetchDepartmentTree({ include_inactive: false, include_members: true, }); if (!response.success) { throw new Error(response.message || '获取部门数据失败'); } // 转换API数据为页面所需的格式 const departments = transformDepartmentList(response.data); // 递归转换部门数据为与现有页面兼容的数据格式 const transformDepartmentRecursive = (dept: any): Department => ({ id: dept.id, name: dept.name, code: dept.code, level: dept.level + 1, // API的level从0开始,页面从1开始 manager: dept.manager, // 从API的manager_name字段获取 phone: dept.phone, // 从API的manager_phone字段获取 email: dept.email, // 从API的manager_email字段获取 description: dept.description, sort: dept.sortOrder, status: dept.status as 'active' | 'inactive', parentId: dept.parentId || undefined, createdAt: dept.createdAt, updatedAt: dept.updatedAt, children: dept.children ? dept.children.map(transformDepartmentRecursive) : [], }); const compatibleDepartments: Department[] = departments.map(transformDepartmentRecursive); dispatch({ type: 'SET_DEPARTMENTS', payload: compatibleDepartments }); // 默认展开所有一级部门 dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set(compatibleDepartments.map(d => d.id)) }); toast.success(`成功加载 ${compatibleDepartments.length} 个部门`); } catch (error) { console.error('Failed to load departments:', error); dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : '加载部门数据失败' }); toast.error('加载部门数据失败'); } }; // 统计部门数量 const countDepartments = (depts: Department[]): { level1: number; level2: number; total: number } => { let level1 = 0; let level2 = 0; depts.forEach(dept => { if (!dept.parentId) { level1++; if (dept.children) { level2 += dept.children.length; } } }); return { level1, level2, total: level1 + level2 }; }; const stats = countDepartments(state.departments); // 展开/收起部门 const toggleExpand = (id: string) => { dispatch({ type: 'TOGGLE_EXPAND', payload: id }); }; // 展开全部 const expandAll = () => { const allIds = new Set(); const collectIds = (depts: Department[]) => { depts.forEach(dept => { allIds.add(dept.id); if (dept.children) { collectIds(dept.children); } }); }; collectIds(state.departments); dispatch({ type: 'SET_EXPANDED_IDS', payload: allIds }); }; // 刷新数据 const refreshData = () => { loadDepartments(); }; // 收起全部 const collapseAll = () => { dispatch({ type: 'SET_EXPANDED_IDS', payload: new Set() }); }; // 添加部门 const handleAdd = (parent?: Department) => { dispatch({ type: 'SET_EDITING_DEPARTMENT', payload: null }); dispatch({ type: 'SET_PARENT_DEPARTMENT', payload: parent || null }); dispatch({ type: 'TOGGLE_FORM', payload: true }); }; // 编辑部门 const handleEdit = (dept: Department) => { dispatch({ type: 'SET_EDITING_DEPARTMENT', payload: dept }); dispatch({ type: 'SET_PARENT_DEPARTMENT', payload: null }); dispatch({ type: 'TOGGLE_FORM', payload: true }); }; // 删除部门 const handleDelete = (dept: Department) => { if (dept.children && dept.children.length > 0) { toast.error('请先删除该部门下的子部门'); return; } dispatch({ type: 'SET_DELETING_DEPARTMENT', payload: dept }); dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: true }); }; // 保存部门 const handleSave = (formData: Partial) => { if (!formData.name || !formData.code) { toast.error('请填写必填项'); return; } const now = new Date().toISOString(); if (state.editingDepartment) { // 更新部门 const updateInTree = (items: Department[]): Department[] => { return items.map(item => { if (item.id === state.editingDepartment!.id) { return { ...item, ...formData, updatedAt: now, children: item.children, } as Department; } if (item.children) { return { ...item, children: updateInTree(item.children), }; } return item; }); }; const updated = updateInTree(state.departments); dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); toast.success('部门更新成功'); } else { // 新增部门 const newDept: Department = { id: `dept-${Date.now()}`, ...formData as Department, createdAt: now, updatedAt: now, }; if (state.parentDepartment) { // 添加到父部门下 const addToParent = (items: Department[]): Department[] => { return items.map(item => { if (item.id === state.parentDepartment!.id) { return { ...item, children: [...(item.children || []), newDept], }; } if (item.children) { return { ...item, children: addToParent(item.children), }; } return item; }); }; const updated = addToParent(state.departments); dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); dispatch({ type: 'TOGGLE_EXPAND', payload: state.parentDepartment.id }); } else { // 添加为一级部门 const updated = [...state.departments, newDept]; dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); } toast.success('部门添加成功'); } dispatch({ type: 'TOGGLE_FORM', payload: false }); }; // 确认删除 const confirmDelete = async () => { if (!state.deletingDepartment) return; try { // 调用删除API await deleteDepartment(state.deletingDepartment.id); // 删除成功后,刷新部门列表 await loadDepartments(); toast.success('部门删除成功'); // 关闭删除对话框 dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: false }); dispatch({ type: 'SET_DELETING_DEPARTMENT', payload: null }); } catch (error) { console.error('Failed to delete department:', error); const errorMessage = error instanceof Error ? error.message : '删除部门失败'; toast.error(errorMessage); } }; // 拖拽功能 const handleDragStart = (dept: Department, index: number, parentId?: string) => { dispatch({ type: 'SET_DRAGGED_ITEM', payload: { dept, index, parentId } }); }; const handleDragEnd = () => { dispatch({ type: 'SET_DRAGGED_ITEM', payload: null }); dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); }; const handleDragOver = (e: React.DragEvent, index: number, parentId?: string) => { e.preventDefault(); if (state.draggedItem && state.draggedItem.parentId === parentId) { dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: { index, parentId } }); } }; const handleDragLeave = () => { dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); }; const handleDrop = (e: React.DragEvent, hoverIndex: number, parentId?: string) => { e.preventDefault(); if (!state.draggedItem) return; if (state.draggedItem.parentId !== parentId) { toast.error('不能跨级别拖动部门'); dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); return; } const dragIndex = state.draggedItem.index; if (dragIndex === hoverIndex) { dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); return; } let updated: Department[]; if (!parentId) { // 一级部门 const newDepts = [...state.departments]; const [removed] = newDepts.splice(dragIndex, 1); newDepts.splice(hoverIndex, 0, removed); updated = newDepts.map((item, index) => ({ ...item, sort: index + 1, })); } else { // 二级部门 const updateInTree = (items: Department[]): Department[] => { return items.map(item => { if (item.id === parentId && item.children) { const newChildren = [...item.children]; const [removed] = newChildren.splice(dragIndex, 1); newChildren.splice(hoverIndex, 0, removed); return { ...item, children: newChildren.map((child, index) => ({ ...child, sort: index + 1, })), }; } if (item.children) { return { ...item, children: updateInTree(item.children), }; } return item; }); }; updated = updateInTree(state.departments); } dispatch({ type: 'SET_DEPARTMENTS', payload: updated }); toast.success('部门顺序已更新'); dispatch({ type: 'SET_DRAG_OVER_ITEM', payload: null }); }; // 合并所有状态变化,统一处理数据加载 useEffect(() => { if (isFirstLoad.current) { // 首次加载 isFirstLoad.current = false; loadDepartments(); } }, []); return (
{/* 页面标题 */} handleAdd()} onRefresh={refreshData} loading={state.loading} /> {/* 统计卡片 */} {/* 部门树 */} {/* 表单对话框 */} dispatch({ type: 'TOGGLE_FORM', payload: open })} editingDepartment={state.editingDepartment} parentDepartment={state.parentDepartment} onSave={handleSave} refreshDepartmentTree={refreshData} /> {/* 删除确认对话框 */} dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: open })} deletingDepartment={state.deletingDepartment} onConfirm={confirmDelete} /> {/* 功能说明 */} {/* 错误显示 */} {state.error && (
{state.error}
)}
); }