515 lines
16 KiB
TypeScript
515 lines
16 KiB
TypeScript
/**
|
||
* 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';
|
||
|
||
// 部门管理状态管理
|
||
interface DepartmentManagementState {
|
||
departments: Department[];
|
||
expandedIds: Set<string>;
|
||
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<string> }
|
||
| { 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 compatibleDepartments: Department[] = departments.map(dept => ({
|
||
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.map(child => ({
|
||
id: child.id,
|
||
name: child.name,
|
||
code: child.code,
|
||
level: child.level + 1,
|
||
manager: child.manager, // 从API的manager_name字段获取
|
||
phone: child.phone, // 从API的manager_phone字段获取
|
||
email: child.email, // 从API的manager_email字段获取
|
||
description: child.description,
|
||
sort: child.sortOrder,
|
||
status: child.status as 'active' | 'inactive',
|
||
parentId: child.parentId || undefined,
|
||
createdAt: child.createdAt,
|
||
updatedAt: child.updatedAt,
|
||
})),
|
||
}));
|
||
|
||
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<string>();
|
||
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<Department>) => {
|
||
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 = () => {
|
||
if (!state.deletingDepartment) return;
|
||
|
||
const deleteFromTree = (items: Department[]): Department[] => {
|
||
return items
|
||
.filter(item => item.id !== state.deletingDepartment!.id)
|
||
.map(item => {
|
||
if (item.children) {
|
||
return {
|
||
...item,
|
||
children: deleteFromTree(item.children),
|
||
};
|
||
}
|
||
return item;
|
||
});
|
||
};
|
||
|
||
const updated = deleteFromTree(state.departments);
|
||
dispatch({ type: 'SET_DEPARTMENTS', payload: updated });
|
||
toast.success('部门删除成功');
|
||
|
||
dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: false });
|
||
dispatch({ type: 'SET_DELETING_DEPARTMENT', payload: null });
|
||
};
|
||
|
||
// 拖拽功能
|
||
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 (
|
||
<div className="space-y-6">
|
||
{/* 页面标题 */}
|
||
<DepartmentHeader onAdd={() => handleAdd()} onRefresh={refreshData} loading={state.loading} />
|
||
|
||
{/* 统计卡片 */}
|
||
<DepartmentStatsCards stats={stats} />
|
||
|
||
{/* 部门树 */}
|
||
<DepartmentTree
|
||
departments={state.departments}
|
||
expandedIds={state.expandedIds}
|
||
loading={state.loading}
|
||
draggedItem={state.draggedItem}
|
||
dragOverItem={state.dragOverItem}
|
||
onToggleExpand={toggleExpand}
|
||
onExpandAll={expandAll}
|
||
onCollapseAll={collapseAll}
|
||
onAdd={handleAdd}
|
||
onEdit={handleEdit}
|
||
onDelete={handleDelete}
|
||
onDragStart={handleDragStart}
|
||
onDragEnd={handleDragEnd}
|
||
onDragOver={handleDragOver}
|
||
onDragLeave={handleDragLeave}
|
||
onDrop={handleDrop}
|
||
/>
|
||
|
||
{/* 表单对话框 */}
|
||
<DepartmentFormDialog
|
||
open={state.showForm}
|
||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_FORM', payload: open })}
|
||
editingDepartment={state.editingDepartment}
|
||
parentDepartment={state.parentDepartment}
|
||
onSave={handleSave}
|
||
refreshDepartmentTree={refreshData}
|
||
/>
|
||
|
||
{/* 删除确认对话框 */}
|
||
<DepartmentDeleteDialog
|
||
open={state.showDeleteDialog}
|
||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: open })}
|
||
deletingDepartment={state.deletingDepartment}
|
||
onConfirm={confirmDelete}
|
||
/>
|
||
|
||
{/* 功能说明 */}
|
||
<DepartmentInstructions />
|
||
|
||
{/* 错误显示 */}
|
||
{state.error && (
|
||
<div className="p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
||
<span>{state.error}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
} |