gongxiangp2/components/admin/DepartmentManagement.tsx

759 lines
29 KiB
TypeScript
Raw Permalink 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.

/**
* 部门管理组件(阶段三更新:支持层级结构、编辑和创建子部门)
* 提取自 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>
)}
</>
)
}