gongxiangp2/components/DepartmentPermissionsTab.tsx
chenpei ea9076df45 feat: 实现统一的下载错误处理系统并修复产品全案数据库Schema
- 创建统一的下载错误处理模块 (lib/download-errors.ts)
- 实现13种错误类型分类和中文错误消息
- 后端统一使用 createDownloadError 创建标准化错误响应
- 前端统一使用 parseDownloadError 解析错误并显示
- 修复正式系统数据库Schema,添加产品全案相关字段和表
- 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段
- 创建 material_assets, api_keys, api_call_logs 表
2026-01-05 17:52:42 +08:00

868 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

'use client'
import { useEffect, useState, useMemo } from 'react'
interface DepartmentPermission {
department: string
materialType: string
canView: boolean
canDownload: boolean
canShare: boolean
linkedToDepartment: string | null
}
interface Department {
id: string
name: string
parentId?: string | null
code?: string | null
sortOrder?: number
}
interface DepartmentPermissionsTabProps {
showToast: (message: string, type?: 'success' | 'error') => void
departments: Array<{ id: string; name: string; parentId?: string | null; code?: string | null; sortOrder?: number }>
}
export default function DepartmentPermissionsTab({
showToast,
departments,
}: DepartmentPermissionsTabProps) {
const [loading, setLoading] = useState(true)
const [searchKeyword, setSearchKeyword] = useState('')
const [expandedDepartments, setExpandedDepartments] = useState<Set<string>>(new Set())
const [permissions, setPermissions] = useState<Record<string, Record<string, DepartmentPermission>>>({})
const [editedPermissions, setEditedPermissions] = useState<Record<string, Record<string, DepartmentPermission>>>({})
const [dirtyDepartments, setDirtyDepartments] = useState<Set<string>>(new Set())
const [linkedDepartments, setLinkedDepartments] = useState<Record<string, string>>({})
const [hasCustomPermissions, setHasCustomPermissions] = useState<Record<string, boolean>>({})
const [materialTypes, setMaterialTypes] = useState<string[]>([])
const [savingDept, setSavingDept] = useState<string | null>(null)
// 对话框状态
const [showCopyDialog, setShowCopyDialog] = useState(false)
const [copySourceDept, setCopySourceDept] = useState<string>('')
const [selectedCopyTargets, setSelectedCopyTargets] = useState<string[]>([])
const [showLinkDialog, setShowLinkDialog] = useState(false)
const [linkTargetDept, setLinkTargetDept] = useState<string>('')
const [selectedLinkSource, setSelectedLinkSource] = useState<string>('')
// 构建部门树形结构(与部门管理和用户管理逻辑统一)
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 as Department)
}
})
// 排序子部门
childrenMap.forEach((children) => {
children.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)
})
})
return { topLevelDepts: topLevelDepts as Department[], childrenMap }
}, [departments])
useEffect(() => {
fetchPermissions()
}, [])
const fetchPermissions = async () => {
try {
setLoading(true)
const url = searchKeyword
? `/api/admin/department-permissions?search=${encodeURIComponent(searchKeyword)}`
: '/api/admin/department-permissions'
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
setPermissions(data.permissions || {})
setEditedPermissions(data.permissions || {})
setDirtyDepartments(new Set())
setLinkedDepartments(data.linkedDepartments || {})
setHasCustomPermissions(data.hasCustomPermissions || {})
setMaterialTypes(data.materialTypes || [])
} else {
showToast('获取权限配置失败', 'error')
}
} catch (error) {
console.error('Error fetching permissions:', error)
showToast('获取权限配置失败', 'error')
} finally {
setLoading(false)
}
}
const toggleDepartment = (deptId: string) => {
setExpandedDepartments((prev) => {
const newSet = new Set(prev)
if (newSet.has(deptId)) {
newSet.delete(deptId)
} else {
newSet.add(deptId)
}
return newSet
})
}
const markDirty = (department: string) => {
setDirtyDepartments((prev) => {
const next = new Set(prev)
next.add(department)
return next
})
}
const buildPermission = (
department: string,
materialType: string,
base?: DepartmentPermission
): DepartmentPermission => {
return (
base || {
department,
materialType,
canView: false,
canDownload: false,
canShare: true,
linkedToDepartment: linkedDepartments[department] || null,
}
)
}
const updateLocalPermission = (
department: string,
materialType: string,
field: 'canView' | 'canDownload' | 'canShare',
value: boolean
) => {
setEditedPermissions((prev) => {
const deptPerms = { ...(prev[department] || permissions[department] || {}) }
const current = buildPermission(department, materialType, deptPerms[materialType])
deptPerms[materialType] = { ...current, [field]: value }
return { ...prev, [department]: deptPerms }
})
markDirty(department)
}
const handleToggleAll = (
department: string,
field: 'canView' | 'canDownload' | 'canShare',
value: boolean
) => {
if (linkedDepartments[department]) return
setEditedPermissions((prev) => {
const deptPerms = { ...(prev[department] || permissions[department] || {}) }
materialTypes.forEach((mt) => {
const current = buildPermission(department, mt, deptPerms[mt])
deptPerms[mt] = { ...current, [field]: value }
})
return { ...prev, [department]: deptPerms }
})
markDirty(department)
}
const handleSaveDepartment = async (department: string) => {
if (linkedDepartments[department]) {
showToast('已关联的部门不可单独保存,请先取消关联', 'error')
return
}
if (!dirtyDepartments.has(department)) {
showToast('没有需要保存的更改', 'error')
return
}
const deptPerms = editedPermissions[department] || {}
const payload = materialTypes.map((mt) => {
const perm = buildPermission(department, mt, deptPerms[mt])
return {
materialType: mt,
canView: perm.canView,
canDownload: perm.canDownload,
canShare: perm.canShare,
}
})
try {
setSavingDept(department)
const response = await fetch('/api/admin/department-permissions/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
department,
permissions: payload,
}),
})
if (response.ok) {
showToast('已保存')
await fetchPermissions()
} else {
const error = await response.json()
showToast(error.error || '保存失败', 'error')
}
} catch (error) {
console.error('Error saving permissions:', error)
showToast('保存失败', 'error')
} finally {
setSavingDept(null)
}
}
const handleCopyPermissions = (sourceDept: string) => {
const targetDepts = departments
.map((d) => d.name)
.filter((name) => name !== sourceDept)
if (targetDepts.length === 0) {
showToast('没有可复制的目标部门', 'error')
return
}
setCopySourceDept(sourceDept)
setSelectedCopyTargets([])
setShowCopyDialog(true)
}
const handleConfirmCopy = async () => {
if (selectedCopyTargets.length === 0) {
showToast('请至少选择一个目标部门', 'error')
return
}
try {
const response = await fetch('/api/admin/department-permissions/copy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sourceDepartment: copySourceDept,
targetDepartments: selectedCopyTargets,
}),
})
if (response.ok) {
await fetchPermissions()
showToast('权限复制成功')
setShowCopyDialog(false)
} else {
const error = await response.json()
showToast(error.error || '复制失败', 'error')
}
} catch (error) {
console.error('Error copying permissions:', error)
showToast('复制失败', 'error')
}
}
const handleLinkPermissions = (targetDept: string) => {
const sourceDepts = departments
.map((d) => d.name)
.filter((name) => name !== targetDept)
if (sourceDepts.length === 0) {
showToast('没有可关联的源部门', 'error')
return
}
setLinkTargetDept(targetDept)
setSelectedLinkSource('')
setShowLinkDialog(true)
}
const handleConfirmLink = async () => {
if (!selectedLinkSource) {
showToast('请选择一个源部门', 'error')
return
}
try {
const response = await fetch('/api/admin/department-permissions/link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
targetDepartment: linkTargetDept,
sourceDepartment: selectedLinkSource,
}),
})
if (response.ok) {
await fetchPermissions()
showToast('权限关联成功')
setShowLinkDialog(false)
} else {
const error = await response.json()
showToast(error.error || '关联失败', 'error')
}
} catch (error) {
console.error('Error linking permissions:', error)
showToast('关联失败', 'error')
}
}
const handleUnlinkPermissions = async (department: string) => {
const confirmed = window.confirm(`确定要取消"${department}"的权限关联吗?`)
if (!confirmed) return
try {
const response = await fetch(
`/api/admin/department-permissions/link?department=${encodeURIComponent(department)}`,
{ method: 'DELETE' }
)
if (response.ok) {
await fetchPermissions()
showToast('已取消关联')
} else {
const error = await response.json()
showToast(error.error || '取消失败', 'error')
}
} catch (error) {
console.error('Error unlinking permissions:', error)
showToast('取消失败', 'error')
}
}
// 获取父部门名称
const getParentDepartmentName = (deptName: string): string | null => {
const dept = departments.find((d) => d.name === deptName)
if (!dept?.parentId) return null
const parentDept = departments.find((d) => d.id === dept.parentId)
return parentDept?.name || null
}
// 判断权限是否继承自父部门
const isInheritedFromParent = (deptName: string): boolean => {
// 如果已关联到其他部门,不是继承
if (linkedDepartments[deptName]) return false
// 如果有自定义权限配置,不是继承
if (hasCustomPermissions[deptName]) return false
// 如果有父部门,且没有自定义配置,则是继承
return !!getParentDepartmentName(deptName)
}
// 使用父部门权限(删除自定义配置,恢复继承)
const handleUseParentPermissions = async (department: string) => {
const parentDept = getParentDepartmentName(department)
if (!parentDept) {
showToast('该部门没有父部门,无法使用父部门权限', 'error')
return
}
const confirmed = window.confirm(
`确定要将"${department}"的权限还原为继承自"${parentDept}"吗?\n这将删除所有自定义权限配置。`
)
if (!confirmed) return
try {
const response = await fetch(
`/api/admin/department-permissions/inherit?department=${encodeURIComponent(department)}`,
{ method: 'DELETE' }
)
if (response.ok) {
await fetchPermissions()
showToast('已恢复为继承父部门权限')
} else {
const error = await response.json()
showToast(error.error || '操作失败', 'error')
}
} catch (error) {
console.error('Error using parent permissions:', error)
showToast('操作失败', 'error')
}
}
// 创建独立配置(基于父部门权限)
const handleCreateIndependentPermissions = async (department: string) => {
const parentDept = getParentDepartmentName(department)
if (!parentDept) {
showToast('该部门没有父部门,无法基于父部门创建配置', 'error')
return
}
if (hasCustomPermissions[department]) {
showToast('该部门已有自定义权限配置', 'error')
return
}
try {
const response = await fetch('/api/admin/department-permissions/inherit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ department }),
})
if (response.ok) {
await fetchPermissions()
showToast('已创建独立配置,可以开始编辑')
} else {
const error = await response.json()
showToast(error.error || '创建失败', 'error')
}
} catch (error) {
console.error('Error creating independent permissions:', error)
showToast('创建失败', 'error')
}
}
// 递归检查部门或其子部门是否匹配搜索关键词
const hasMatchingDepartment = (dept: Department): boolean => {
if (searchKeyword && dept.name.toLowerCase().includes(searchKeyword.toLowerCase())) {
return true
}
const children = departmentTree.childrenMap.get(dept.id) || []
return children.some(hasMatchingDepartment)
}
// 递归渲染部门树
const renderDepartmentNode = (dept: Department, level: number = 0): React.ReactNode | null => {
const children = departmentTree.childrenMap.get(dept.id) || []
const hasChildren = children.length > 0
const isExpanded = expandedDepartments.has(dept.id)
const deptName = dept.name
const linkedTo = linkedDepartments[deptName]
const deptPermissions = editedPermissions[deptName] || {}
const isDirty = dirtyDepartments.has(deptName)
const indent = level * 24
// 如果有关键词搜索,检查是否匹配(包括子部门)
if (searchKeyword && !hasMatchingDepartment(dept)) {
return null
}
return (
<div key={dept.id}>
<div
className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
style={{ marginLeft: level > 0 ? '16px' : '0', marginTop: level > 0 ? '8px' : '0' }}
>
<div
className="px-4 py-3 bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center justify-between"
style={{ paddingLeft: `${indent + 16}px` }}
onClick={() => toggleDepartment(dept.id)}
>
<div className="flex items-center gap-2 flex-1">
{/* 展开/折叠图标 */}
{hasChildren ? (
<div className="flex-shrink-0 w-5 h-5 flex items-center justify-center">
<svg
className={`w-3 h-3 transform transition-transform ${isExpanded ? 'rotate-90' : ''}`}
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 text-blue-600 dark:text-blue-400 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>
<span className="font-medium text-gray-900 dark:text-white">{deptName}</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>
)}
{/* 权限状态显示 */}
{linkedTo ? (
<span className="text-xs text-blue-600 dark:text-blue-400">
(🔗 关联到: {linkedTo})
</span>
) : isInheritedFromParent(deptName) ? (
<span className="text-xs text-green-600 dark:text-green-400">
(🔗 继承自: {getParentDepartmentName(deptName)})
</span>
) : hasCustomPermissions[deptName] ? (
<span className="text-xs text-gray-600 dark:text-gray-400">
(✏️ 自定义配置)
</span>
) : null}
</div>
<div className="flex gap-2 flex-wrap" onClick={(e) => e.stopPropagation()}>
{/* 主要操作:权限继承相关 */}
{!linkedTo && (
<>
{isInheritedFromParent(deptName) ? (
<button
onClick={() => handleCreateIndependentPermissions(deptName)}
className="px-3 py-1 text-sm bg-purple-500 text-white rounded hover:bg-purple-600 transition-colors"
title="创建独立配置,基于父部门权限"
>
独立配置
</button>
) : hasCustomPermissions[deptName] ? (
<button
onClick={() => handleUseParentPermissions(deptName)}
className="px-3 py-1 text-sm bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors"
title="还原为继承父部门权限"
>
还原为继承
</button>
) : null}
</>
)}
{/* 高级操作:复制和关联 */}
{linkedTo ? (
<button
onClick={() => handleUnlinkPermissions(deptName)}
className="px-3 py-1 text-sm bg-orange-500 text-white rounded hover:bg-orange-600 transition-colors"
title="取消关联"
>
取消关联
</button>
) : (
<>
<button
onClick={() => handleCopyPermissions(deptName)}
className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
title="复制权限到其他部门"
>
复制到
</button>
<button
onClick={() => handleLinkPermissions(deptName)}
className="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
title="关联到其他部门"
>
关联到
</button>
</>
)}
</div>
</div>
{isExpanded && (
<div className="p-4 bg-white dark:bg-gray-900">
<div className="flex flex-wrap items-center gap-2 mb-3">
<span className="text-sm text-gray-600 dark:text-gray-400">批量操作:</span>
<button
disabled={!!linkedTo}
onClick={() => handleToggleAll(deptName, 'canView', true)}
className="px-3 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50"
>
全选可见
</button>
<button
disabled={!!linkedTo}
onClick={() => handleToggleAll(deptName, 'canView', false)}
className="px-3 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50"
>
取消可见
</button>
<button
disabled={!!linkedTo}
onClick={() => handleToggleAll(deptName, 'canDownload', true)}
className="px-3 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50"
>
全选下载
</button>
<button
disabled={!!linkedTo}
onClick={() => handleToggleAll(deptName, 'canDownload', false)}
className="px-3 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50"
>
取消下载
</button>
<button
disabled={!!linkedTo}
onClick={() => handleToggleAll(deptName, 'canShare', true)}
className="px-3 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50"
>
全选分享
</button>
<button
disabled={!!linkedTo}
onClick={() => handleToggleAll(deptName, 'canShare', false)}
className="px-3 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50"
>
取消分享
</button>
<div className="flex-1" />
<button
disabled={!!linkedTo || !isDirty || savingDept === deptName}
onClick={() => handleSaveDepartment(deptName)}
className="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
>
{savingDept === deptName ? '保存中...' : isDirty ? '保存更改' : '已保存'}
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 px-3 text-gray-700 dark:text-gray-300">
素材类型
</th>
<th className="text-center py-2 px-3 text-gray-700 dark:text-gray-300">
可见
</th>
<th className="text-center py-2 px-3 text-gray-700 dark:text-gray-300">
下载
</th>
<th className="text-center py-2 px-3 text-gray-700 dark:text-gray-300">
分享
</th>
</tr>
</thead>
<tbody>
{materialTypes.map((materialType) => {
const perm = deptPermissions[materialType]
const isLinked = !!linkedTo
const isInherited = isInheritedFromParent(deptName)
const canView = perm?.canView ?? false
const canDownload = perm?.canDownload ?? false
const canShare = perm?.canShare ?? true
const isDisabled = isLinked || isInherited
return (
<tr
key={materialType}
className="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800"
>
<td className="py-2 px-3 text-gray-900 dark:text-gray-100">
<div className="flex items-center gap-2">
<span>{materialType}</span>
{isInherited && (
<span className="text-xs text-green-600 dark:text-green-400" title="继承自父部门">
🔗
</span>
)}
</div>
</td>
<td className="py-2 px-3 text-center">
<input
type="checkbox"
checked={canView}
disabled={isDisabled}
onChange={(e) =>
updateLocalPermission(deptName, materialType, 'canView', e.target.checked)
}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
title={isDisabled ? (isLinked ? '已关联,无法修改' : '继承状态,请先独立配置') : ''}
/>
</td>
<td className="py-2 px-3 text-center">
<input
type="checkbox"
checked={canDownload}
disabled={isDisabled}
onChange={(e) =>
updateLocalPermission(deptName, materialType, 'canDownload', e.target.checked)
}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
title={isDisabled ? (isLinked ? '已关联,无法修改' : '继承状态,请先独立配置') : ''}
/>
</td>
<td className="py-2 px-3 text-center">
<input
type="checkbox"
checked={canShare}
disabled={isDisabled}
onChange={(e) =>
updateLocalPermission(deptName, materialType, 'canShare', e.target.checked)
}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
title={isDisabled ? (isLinked ? '已关联,无法修改' : '继承状态,请先独立配置') : ''}
/>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* 递归渲染子部门 */}
{isExpanded && hasChildren && (
<div>
{children.map(child => renderDepartmentNode(child, level + 1))}
</div>
)}
</div>
)
}
if (loading) {
return (
<div className="card-apple">
<div className="p-8 text-center text-gray-500 dark:text-gray-400">加载中...</div>
</div>
)
}
return (
<div className="card-apple">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">部门权限配置</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
配置各部门对不同素材类型的查看、下载、分享权限
</p>
</div>
</div>
<div className="flex gap-2">
<input
type="text"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
fetchPermissions()
}
}}
placeholder="搜索部门名称..."
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
/>
<button
onClick={fetchPermissions}
className="px-4 py-2 bg-gray-900 dark:bg-gray-700 text-white rounded-lg hover:bg-gray-800 dark:hover:bg-gray-600 transition-colors"
>
搜索
</button>
</div>
</div>
<div className="p-6 space-y-2">
{departmentTree.topLevelDepts.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
{searchKeyword ? '未找到匹配的部门' : '暂无部门'}
</div>
) : (
departmentTree.topLevelDepts.map(dept => renderDepartmentNode(dept, 0))
)}
</div>
{/* 复制权限对话框 */}
{showCopyDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
复制权限配置
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
将 <span className="font-medium">{copySourceDept}</span> 的权限配置复制到以下部门:
</p>
<div className="mb-4">
<div className="border border-gray-300 dark:border-gray-600 rounded-lg p-3 max-h-64 overflow-y-auto bg-white dark:bg-gray-900">
{departments
.filter((d) => d.name !== copySourceDept)
.map((dept) => (
<label
key={dept.id}
className="flex items-center py-2 px-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded cursor-pointer"
>
<input
type="checkbox"
checked={selectedCopyTargets.includes(dept.name)}
onChange={(e) => {
if (e.target.checked) {
setSelectedCopyTargets([...selectedCopyTargets, dept.name])
} else {
setSelectedCopyTargets(selectedCopyTargets.filter((n) => n !== dept.name))
}
}}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="ml-2 text-gray-900 dark:text-white">{dept.name}</span>
</label>
))}
</div>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowCopyDialog(false)}
className="px-4 py-2 text-sm rounded bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
取消
</button>
<button
onClick={handleConfirmCopy}
className="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors"
>
确定
</button>
</div>
</div>
</div>
)}
{/* 关联权限对话框 */}
{showLinkDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
关联权限配置
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
将 <span className="font-medium">{linkTargetDept}</span> 的权限关联到以下部门:
</p>
<div className="mb-4">
<select
value={selectedLinkSource}
onChange={(e) => setSelectedLinkSource(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none"
>
<option value="">请选择部门</option>
{departments
.filter((d) => d.name !== linkTargetDept)
.map((dept) => (
<option key={dept.id} value={dept.name}>
{dept.name}
</option>
))}
</select>
</div>
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-xs text-blue-800 dark:text-blue-200">
💡 关联后,{linkTargetDept} 的权限将实时跟随目标部门,且无法独立修改权限配置。
</p>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowLinkDialog(false)}
className="px-4 py-2 text-sm rounded bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
取消
</button>
<button
onClick={handleConfirmLink}
disabled={!selectedLinkSource}
className="px-4 py-2 text-sm rounded bg-green-600 text-white hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
确定
</button>
</div>
</div>
</div>
)}
</div>
)
}