759 lines
29 KiB
TypeScript
759 lines
29 KiB
TypeScript
|
||
/**
|
||
* 部门管理组件(阶段三更新:支持层级结构、编辑和创建子部门)
|
||
* 提取自 app/admin/page.tsx 的部门管理功能
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import Select from '@/components/ui/Select'
|
||
|
||
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<React.SetStateAction<Department[]>>
|
||
showToast: (message: string, type?: 'success' | 'error') => void
|
||
fetchDepartments: () => Promise<void>
|
||
}
|
||
|
||
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<Set<string>>(new Set())
|
||
// 默认所有部门收起(expandedDepts为空Set即表示全部收起)
|
||
|
||
// 编辑和创建子部门相关状态
|
||
const [editingDept, setEditingDept] = useState<Department | null>(null)
|
||
const [creatingChildDept, setCreatingChildDept] = useState<Department | null>(null)
|
||
const [users, setUsers] = useState<User[]>([])
|
||
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<string, Department[]>()
|
||
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 (
|
||
<div key={dept.id} className={level > 0 ? 'ml-4 mt-1' : ''}>
|
||
<div
|
||
className={`flex items-center justify-between p-4 border rounded-lg ${getLevelStyles()} hover:shadow-sm transition-all ${hasChildren ? 'cursor-pointer' : ''}`}
|
||
style={{ paddingLeft: `${indent + 16}px` }}
|
||
onClick={() => hasChildren && toggleExpand(dept.id)}
|
||
>
|
||
<div className="flex items-center gap-3 flex-1">
|
||
{/* 展开/折叠按钮 */}
|
||
{hasChildren ? (
|
||
<div
|
||
className="flex-shrink-0 w-5 h-5 flex items-center justify-center"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<svg
|
||
className={`w-3 h-3 transform transition-transform ${isExpanded ? 'rotate-90' : ''} ${getIconColor()}`}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</div>
|
||
) : (
|
||
<div className="w-5" />
|
||
)}
|
||
|
||
{/* 部门图标(根据层级改变颜色) */}
|
||
<svg className={`w-5 h-5 ${getIconColor()} flex-shrink-0`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||
</svg>
|
||
|
||
{/* 部门信息 */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="font-medium text-gray-900 dark:text-white">{dept.name}</span>
|
||
|
||
{/* 部门编码(阶段三) */}
|
||
{dept.code && (
|
||
<span className="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded">
|
||
{dept.code}
|
||
</span>
|
||
)}
|
||
|
||
{/* 层级标识(阶段三) */}
|
||
{dept.level !== undefined && dept.level > 1 && (
|
||
<span className="px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
|
||
L{dept.level}
|
||
</span>
|
||
)}
|
||
|
||
{/* 状态标识(阶段三) */}
|
||
{dept.status && dept.status !== 'active' && (
|
||
<span className={`px-2 py-0.5 text-xs rounded ${
|
||
dept.status === 'disabled'
|
||
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200'
|
||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||
}`}>
|
||
{dept.status === 'disabled' ? '已禁用' : dept.status === 'archived' ? '已归档' : dept.status}
|
||
</span>
|
||
)}
|
||
|
||
{/* 部门管理员(阶段三) */}
|
||
{dept.manager && (
|
||
<span className="px-2 py-0.5 text-xs bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded">
|
||
负责人: {dept.manager.realName || dept.manager.username}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 部门描述(阶段三) */}
|
||
{dept.description && (
|
||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">
|
||
{dept.description}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex items-center gap-2 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
|
||
<button
|
||
onClick={() => handleCreateChild(dept)}
|
||
className="p-2 text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/30 rounded-lg transition-colors"
|
||
title="创建子部门"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
onClick={() => handleEditDepartment(dept)}
|
||
className="p-2 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-lg transition-colors"
|
||
title="编辑部门"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteDepartment(dept.id, dept.name)}
|
||
className="p-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"
|
||
title="删除部门"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 子部门(递归渲染,改进层级视觉区分) */}
|
||
{hasChildren && isExpanded && (
|
||
<div className="mt-2 space-y-1">
|
||
{children.map(child => renderDepartmentNode(child, level + 1))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div className="card-apple">
|
||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">部门管理</h2>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||
管理系统中的所有部门(阶段三:支持层级结构、编辑和创建子部门)
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-6">
|
||
{/* 添加部门 */}
|
||
<div className="flex gap-3 mb-6">
|
||
<input
|
||
type="text"
|
||
placeholder="输入新部门名称(将创建为顶级部门)"
|
||
value={newDepartmentName}
|
||
onChange={(e) => 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()
|
||
}
|
||
}}
|
||
/>
|
||
<button
|
||
onClick={handleAddDepartment}
|
||
disabled={addingDepartment}
|
||
className="px-6 py-2 bg-gray-900 dark:bg-gray-700 text-white rounded-lg hover:bg-gray-800 dark:hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||
>
|
||
{addingDepartment ? (
|
||
<>
|
||
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
添加中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
添加部门
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* 部门树形列表(阶段三) */}
|
||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||
{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))
|
||
) : (
|
||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||
暂无部门,请添加新部门
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 编辑部门弹窗 */}
|
||
{editingDept && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||
编辑部门:{editingDept.name}
|
||
</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门名称 <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={editForm.name}
|
||
onChange={(e) => 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="请输入部门名称"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门编码
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={editForm.code}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
状态
|
||
</label>
|
||
<Select
|
||
value={editForm.status}
|
||
onChange={(e) => setEditForm({ ...editForm, status: 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"
|
||
>
|
||
<option value="active">启用</option>
|
||
<option value="disabled">禁用</option>
|
||
<option value="archived">归档</option>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
父部门
|
||
</label>
|
||
<DepartmentTreeSelect
|
||
departments={getAvailableParents(editingDept.id)}
|
||
value={editForm.parentId}
|
||
onChange={(deptId) => setEditForm({ ...editForm, parentId: deptId })}
|
||
placeholder="请选择父部门"
|
||
allowEmpty={true}
|
||
emptyLabel="无(顶级部门)"
|
||
className="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门管理员
|
||
</label>
|
||
<Select
|
||
value={editForm.managerId}
|
||
onChange={(e) => setEditForm({ ...editForm, managerId: 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"
|
||
disabled={loadingUsers}
|
||
>
|
||
<option value="">无</option>
|
||
{users.map(user => (
|
||
<option key={user.id} value={user.id}>
|
||
{user.realName || user.username} ({user.email})
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门描述
|
||
</label>
|
||
<textarea
|
||
value={editForm.description}
|
||
onChange={(e) => setEditForm({ ...editForm, description: 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"
|
||
rows={3}
|
||
placeholder="请输入部门描述"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
排序顺序
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={editForm.sortOrder}
|
||
onChange={(e) => setEditForm({ ...editForm, sortOrder: parseInt(e.target.value) || 0 })}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white"
|
||
min="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-3 mt-6">
|
||
<button
|
||
onClick={() => setEditingDept(null)}
|
||
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={handleSaveEdit}
|
||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||
>
|
||
保存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 创建子部门弹窗 */}
|
||
{creatingChildDept && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||
创建子部门(父部门:{creatingChildDept.name})
|
||
</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门名称 <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={childForm.name}
|
||
onChange={(e) => setChildForm({ ...childForm, 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="请输入部门名称"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门编码
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={childForm.code}
|
||
onChange={(e) => setChildForm({ ...childForm, 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"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
排序顺序
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={childForm.sortOrder}
|
||
onChange={(e) => setChildForm({ ...childForm, sortOrder: parseInt(e.target.value) || 0 })}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white"
|
||
min="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门管理员
|
||
</label>
|
||
<Select
|
||
value={childForm.managerId}
|
||
onChange={(e) => setChildForm({ ...childForm, managerId: 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"
|
||
disabled={loadingUsers}
|
||
>
|
||
<option value="">无</option>
|
||
{users.map(user => (
|
||
<option key={user.id} value={user.id}>
|
||
{user.realName || user.username} ({user.email})
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
部门描述
|
||
</label>
|
||
<textarea
|
||
value={childForm.description}
|
||
onChange={(e) => setChildForm({ ...childForm, description: 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"
|
||
rows={3}
|
||
placeholder="请输入部门描述"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-3 mt-6">
|
||
<button
|
||
onClick={() => {
|
||
setCreatingChildDept(null)
|
||
setChildForm({ name: '', code: '', description: '', managerId: '', sortOrder: 0 })
|
||
}}
|
||
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={handleSaveChild}
|
||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||
>
|
||
创建
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)
|
||
}
|