'use client' import { useEffect, useState, useMemo } from 'react' import { parseFetchError, handleError } from '@/lib/error-handler' interface DepartmentPermission { department: string materialType: string canView: boolean canDownload: boolean canDownloadSource: 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>(new Set()) const [permissions, setPermissions] = useState>>({}) const [editedPermissions, setEditedPermissions] = useState>>({}) const [dirtyDepartments, setDirtyDepartments] = useState>(new Set()) const [linkedDepartments, setLinkedDepartments] = useState>({}) const [hasCustomPermissions, setHasCustomPermissions] = useState>({}) const [materialTypes, setMaterialTypes] = useState([]) const [savingDept, setSavingDept] = useState(null) // 对话框状态 const [showCopyDialog, setShowCopyDialog] = useState(false) const [copySourceDept, setCopySourceDept] = useState('') const [selectedCopyTargets, setSelectedCopyTargets] = useState([]) const [showLinkDialog, setShowLinkDialog] = useState(false) const [linkTargetDept, setLinkTargetDept] = useState('') const [selectedLinkSource, setSelectedLinkSource] = useState('') // 构建部门树形结构(与部门管理和用户管理逻辑统一) 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 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 { const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error fetching permissions:', error) const { message } = handleError(error) showToast(message, '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, canDownloadSource: false, canShare: true, linkedToDepartment: linkedDepartments[department] || null, } ) } const updateLocalPermission = ( department: string, materialType: string, field: 'canView' | 'canDownload' | 'canDownloadSource' | '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' | 'canDownloadSource' | '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, canDownloadSource: perm.canDownloadSource, 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() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error saving permissions:', error) const { message } = handleError(error) showToast(message, '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() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error copying permissions:', error) const { message } = handleError(error) showToast(message, '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() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error linking permissions:', error) const { message } = handleError(error) showToast(message, '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() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error unlinking permissions:', error) const { message } = handleError(error) showToast(message, '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() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error using parent permissions:', error) const { message } = handleError(error) showToast(message, '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() const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error creating independent permissions:', error) const { message } = handleError(error) showToast(message, '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 (
0 ? '16px' : '0', marginTop: level > 0 ? '8px' : '0' }} >
toggleDepartment(dept.id)} >
{/* 展开/折叠图标 */} {hasChildren ? (
) : (
)} {/* 部门图标 */} {deptName} {dept.code && ( {dept.code} )} {/* 权限状态显示 */} {linkedTo ? ( (🔗 关联到: {linkedTo}) ) : isInheritedFromParent(deptName) ? ( (🔗 继承自: {getParentDepartmentName(deptName)}) ) : hasCustomPermissions[deptName] ? ( (✏️ 自定义配置) ) : null}
e.stopPropagation()}> {/* 主要操作:权限继承相关 */} {!linkedTo && ( <> {isInheritedFromParent(deptName) ? ( ) : hasCustomPermissions[deptName] ? ( ) : null} )} {/* 高级操作:复制和关联 */} {linkedTo ? ( ) : ( <> )}
{isExpanded && (
批量操作:
{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 canDownloadSource = perm?.canDownloadSource ?? false const canShare = perm?.canShare ?? true const isDisabled = isLinked || isInherited return ( ) })}
素材类型 可见 下载 源文件 分享
{materialType} {isInherited && ( 🔗 )}
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 ? '已关联,无法修改' : '继承状态,请先独立配置') : ''} /> 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 ? '已关联,无法修改' : '继承状态,请先独立配置') : ''} /> updateLocalPermission(deptName, materialType, 'canDownloadSource', 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 ? '已关联,无法修改' : '继承状态,请先独立配置') : '控制 PSD/PSB/TIF 等源文件下载权限'} /> 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 ? '已关联,无法修改' : '继承状态,请先独立配置') : ''} />
)}
{/* 递归渲染子部门 */} {isExpanded && hasChildren && (
{children.map(child => renderDepartmentNode(child, level + 1))}
)}
) } if (loading) { return (
加载中...
) } return (

部门权限配置

配置各部门对不同素材类型的查看、下载、分享权限

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" />
{departmentTree.topLevelDepts.length === 0 ? (
{searchKeyword ? '未找到匹配的部门' : '暂无部门'}
) : ( departmentTree.topLevelDepts.map(dept => renderDepartmentNode(dept, 0)) )}
{/* 复制权限对话框 */} {showCopyDialog && (

复制权限配置

将 {copySourceDept} 的权限配置复制到以下部门:

{departments .filter((d) => d.name !== copySourceDept) .map((dept) => ( ))}
)} {/* 关联权限对话框 */} {showLinkDialog && (

关联权限配置

将 {linkTargetDept} 的权限关联到以下部门:

💡 关联后,{linkTargetDept} 的权限将实时跟随目标部门,且无法独立修改权限配置。

)}
) }