/** * 部门管理组件(阶段三更新:支持层级结构、编辑和创建子部门) * 提取自 app/admin/page.tsx 的部门管理功能 */ 'use client' import { useState, useMemo, useEffect } from 'react' import type { Department } from '@/hooks/useAdminState' import DepartmentTreeSelect from '@/components/DepartmentTreeSelect' import { getDepartmentCollator } from '@/lib/departments' import { parseFetchError, handleError } from '@/lib/error-handler' interface DepartmentManagementProps { departments: Department[] setDepartments: React.Dispatch> showToast: (message: string, type?: 'success' | 'error') => void fetchDepartments: () => Promise } interface User { id: string username: string realName?: string | null email: string } export default function DepartmentManagement({ departments, setDepartments, showToast, fetchDepartments, }: DepartmentManagementProps) { const [newDepartmentName, setNewDepartmentName] = useState('') const [addingDepartment, setAddingDepartment] = useState(false) const [expandedDepts, setExpandedDepts] = useState>(new Set()) // 默认所有部门收起(expandedDepts为空Set即表示全部收起) // 编辑和创建子部门相关状态 const [editingDept, setEditingDept] = useState(null) const [creatingChildDept, setCreatingChildDept] = useState(null) const [users, setUsers] = useState([]) const [loadingUsers, setLoadingUsers] = useState(false) // 编辑表单状态 const [editForm, setEditForm] = useState({ name: '', code: '', parentId: '', status: 'active', description: '', managerId: '', sortOrder: 0, }) // 创建子部门表单状态 const [childForm, setChildForm] = useState({ name: '', code: '', description: '', managerId: '', sortOrder: 0, }) // 加载用户列表(用于选择部门管理员) useEffect(() => { const loadUsers = async () => { setLoadingUsers(true) try { const response = await fetch('/api/admin/users') if (response.ok) { const data = await response.json() setUsers(data) } } catch (error) { console.error('Error loading users:', error) } finally { setLoadingUsers(false) } } loadUsers() }, []) // 构建部门树形结构(阶段三) const departmentTree = useMemo(() => { // 找出所有顶级部门(没有parentId或parentId不在列表中) const topLevelDepts = departments.filter(dept => !dept.parentId || !departments.find(d => d.id === dept.parentId)) // 构建子部门映射 const childrenMap = new Map() departments.forEach(dept => { if (dept.parentId) { if (!childrenMap.has(dept.parentId)) { childrenMap.set(dept.parentId, []) } childrenMap.get(dept.parentId)!.push(dept) } }) // 排序子部门(使用中文拼音排序,参考 flnxi_sucai) const collator = getDepartmentCollator() childrenMap.forEach((children) => { children.sort((a, b) => { const sortA = a.sortOrder ?? 0 const sortB = b.sortOrder ?? 0 if (sortA !== sortB) return sortA - sortB return collator.compare(a.name, b.name) }) }) return { topLevelDepts, childrenMap } }, [departments]) // 获取所有可用父部门(排除自己和自己的子部门) const getAvailableParents = (excludeId?: string): Department[] => { return departments.filter(dept => { if (excludeId && dept.id === excludeId) return false if (excludeId && dept.parentId === excludeId) return false // 简单检查:排除可能是子部门的部门(更严格的检查在API端) return true }) } // 切换部门展开/折叠 const toggleExpand = (deptId: string) => { setExpandedDepts(prev => { const newSet = new Set(prev) if (newSet.has(deptId)) { newSet.delete(deptId) } else { newSet.add(deptId) } return newSet }) } // 打开编辑弹窗 const handleEditDepartment = (dept: Department) => { setEditingDept(dept) setEditForm({ name: dept.name, code: dept.code || '', parentId: dept.parentId || '', status: dept.status || 'active', description: dept.description || '', managerId: dept.managerId || '', sortOrder: dept.sortOrder || 0, }) } // 保存编辑 const handleSaveEdit = async () => { if (!editingDept) return if (!editForm.name.trim()) { showToast('部门名称不能为空', 'error') return } try { const response = await fetch(`/api/departments/${editingDept.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: editForm.name.trim(), code: editForm.code.trim() || null, parentId: editForm.parentId || null, status: editForm.status, description: editForm.description.trim() || null, managerId: editForm.managerId || null, sortOrder: parseInt(String(editForm.sortOrder)) || 0, }), }) if (response.ok) { await fetchDepartments() setEditingDept(null) showToast('部门更新成功') } else { const error = await response.json() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Update department error:', error) const { message } = handleError(error) showToast(message, 'error') } } // 打开创建子部门弹窗 const handleCreateChild = (parentDept: Department) => { setCreatingChildDept(parentDept) setChildForm({ name: '', code: '', description: '', managerId: '', sortOrder: 0, }) } // 保存创建子部门 const handleSaveChild = async () => { if (!creatingChildDept) return if (!childForm.name.trim()) { showToast('部门名称不能为空', 'error') return } try { const response = await fetch('/api/departments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: childForm.name.trim(), parentId: creatingChildDept.id, code: childForm.code.trim() || null, description: childForm.description.trim() || null, managerId: childForm.managerId || null, sortOrder: parseInt(String(childForm.sortOrder)) || 0, }), }) if (response.ok) { await fetchDepartments() setCreatingChildDept(null) setChildForm({ name: '', code: '', description: '', managerId: '', sortOrder: 0 }) // 自动展开父部门 setExpandedDepts(prev => new Set(prev).add(creatingChildDept.id)) showToast('子部门创建成功') } else { const error = await response.json() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Create child department error:', error) const { message } = handleError(error) showToast(message, 'error') } } // 添加部门(顶级部门) const handleAddDepartment = async () => { if (!newDepartmentName.trim()) { showToast('请输入部门名称', 'error') return } setAddingDepartment(true) try { const response = await fetch('/api/departments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newDepartmentName.trim() }), }) if (response.ok) { await fetchDepartments() setNewDepartmentName('') showToast('部门添加成功') } else { const error = await response.json() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Add department error:', error) const { message } = handleError(error) showToast(message, 'error') } finally { setAddingDepartment(false) } } // 删除部门 const handleDeleteDepartment = async (id: string, name: string) => { if (!confirm(`确定要删除部门"${name}"吗?`)) return try { const response = await fetch(`/api/departments/${id}`, { method: 'DELETE', }) if (response.ok) { await fetchDepartments() showToast('部门删除成功') } else { const error = await response.json() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Delete department error:', error) const { message } = handleError(error) showToast(message, 'error') } } // 递归渲染部门树(优化层级视觉区分,参考 flnxi_sucai) const renderDepartmentNode = (dept: Department, level: number = 0) => { const children = departmentTree.childrenMap.get(dept.id) || [] const hasChildren = children.length > 0 const isExpanded = expandedDepts.has(dept.id) const indent = level * 24 // 根据层级应用不同的视觉样式(保持当前项目的视觉风格) const getLevelStyles = () => { if (level === 0) { return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800' } else if (level === 1) { return 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700' } else { return 'bg-white dark:bg-gray-900 border-gray-100 dark:border-gray-700' } } // 根据层级设置图标颜色 const getIconColor = () => { if (level === 0) { return 'text-blue-600 dark:text-blue-400' } else if (level === 1) { return 'text-purple-600 dark:text-purple-400' } else { return 'text-gray-600 dark:text-gray-400' } } return (
0 ? 'ml-4 mt-1' : ''}>
hasChildren && toggleExpand(dept.id)} >
{/* 展开/折叠按钮 */} {hasChildren ? (
e.stopPropagation()} >
) : (
)} {/* 部门图标(根据层级改变颜色) */} {/* 部门信息 */}
{dept.name} {/* 部门编码(阶段三) */} {dept.code && ( {dept.code} )} {/* 层级标识(阶段三) */} {dept.level !== undefined && dept.level > 1 && ( L{dept.level} )} {/* 状态标识(阶段三) */} {dept.status && dept.status !== 'active' && ( {dept.status === 'disabled' ? '已禁用' : dept.status === 'archived' ? '已归档' : dept.status} )} {/* 部门管理员(阶段三) */} {dept.manager && ( 负责人: {dept.manager.realName || dept.manager.username} )}
{/* 部门描述(阶段三) */} {dept.description && (
{dept.description}
)}
{/* 操作按钮 */}
e.stopPropagation()}>
{/* 子部门(递归渲染,改进层级视觉区分) */} {hasChildren && isExpanded && (
{children.map(child => renderDepartmentNode(child, level + 1))}
)}
) } return ( <>

部门管理

管理系统中的所有部门(阶段三:支持层级结构、编辑和创建子部门)

{/* 添加部门 */}
setNewDepartmentName(e.target.value)} className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-gray-400 outline-none" onKeyDown={(e) => { if (e.key === 'Enter' && !addingDepartment) { handleAddDepartment() } }} />
{/* 部门树形列表(阶段三) */}
{departmentTree.topLevelDepts.length > 0 ? ( departmentTree.topLevelDepts .sort((a, b) => { const sortA = a.sortOrder ?? 0 const sortB = b.sortOrder ?? 0 if (sortA !== sortB) return sortA - sortB return a.name.localeCompare(b.name) }) .map(dept => renderDepartmentNode(dept, 0)) ) : (
暂无部门,请添加新部门
)}
{/* 编辑部门弹窗 */} {editingDept && (

编辑部门:{editingDept.name}

setEditForm({ ...editForm, name: e.target.value })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white" placeholder="请输入部门名称" />
setEditForm({ ...editForm, code: e.target.value })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white" placeholder="如:DEPT-001" />
setEditForm({ ...editForm, parentId: deptId })} placeholder="请选择父部门" allowEmpty={true} emptyLabel="无(顶级部门)" className="w-full" />