Files
smart-crop-ui/crop-x/src/app/(app)/central-config/user/department/page.tsx

515 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
);
}