/** * UsersTab 组件 * * 用户管理Tab组件 * 从主页面拆分出来,负责用户管理功能 * 包括用户列表、搜索、编辑、删除、权限管理等 * 使用三层架构样式系统,支持深浅色主题 * * @created 2025-01-XX(拆分自 page.tsx) * @component */ 'use client' import { useCallback, useMemo, useState } from 'react' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' import { useDialog } from '@/components/ui/DialogProvider' import type { Department, DepartmentNode, DepartmentUserGrouping, User, UserTreeNode } from './types' import { buildDepartmentTree } from '../_utils/departmentTree' import DepartmentHierarchy from './users/DepartmentHierarchy' import UserTree from './users/UserTree' import { useModalState } from '../_hooks/useModalState' import UserEditModal from './users/UserEditModal' import TransferSuperAdminModal from './users/TransferSuperAdminModal' interface UsersTabProps { users: User[] departments: Department[] currentUserId?: string currentUserEmail?: string currentIsSuperAdmin?: boolean canManageUser: (user: User) => boolean onToggleActive: (userId: string, current: boolean) => Promise | void onToggleDownload: (userId: string, current: boolean) => Promise | void onSetRole: (userId: string, currentRole: User['role']) => Promise | void onUpdateUser: (user: User, payload: { username: string; department: string }) => Promise | boolean onDeleteUser: (userId: string) => Promise | boolean onTransferSuperAdmin: (targetEmail: string, password: string) => Promise | boolean } export default function UsersTab({ users, departments, currentUserId, currentUserEmail, currentIsSuperAdmin, canManageUser, onToggleActive, onToggleDownload, onSetRole, onUpdateUser, onDeleteUser, onTransferSuperAdmin, }: UsersTabProps) { const styles = useThemeStyles() const theme = useTheme() const [searchTerm, setSearchTerm] = useState('') const [collapsedUserNodeIds, setCollapsedUserNodeIds] = useState>(new Set()) const [collapsedDepartments, setCollapsedDepartments] = useState>(new Set()) const [editingUsername, setEditingUsername] = useState('') const [selectedDepartment, setSelectedDepartment] = useState('') const [transferPassword, setTransferPassword] = useState('') const editingUserModal = useModalState() const passwordModal = useModalState() const editingUser = editingUserModal.data // 根据主题选择颜色类名 const badgeActive = theme === 'dark' ? 'bg-green-900/30 text-green-300' : 'bg-green-100 text-green-700' const badgeInactive = theme === 'dark' ? 'bg-rose-900/30 text-rose-300' : 'bg-rose-100 text-rose-700' const badgeRole = theme === 'dark' ? 'bg-blue-900/30 text-blue-300' : 'bg-blue-100 text-blue-700' const badgeNoRole = theme === 'dark' ? 'bg-slate-800 text-slate-200' : 'bg-slate-200 text-slate-700' const textDesc = theme === 'dark' ? 'text-slate-400' : 'text-slate-500' const textSuperAdmin = theme === 'dark' ? 'text-indigo-100 group-hover:text-indigo-200' : 'text-indigo-900 group-hover:text-indigo-700' const textNormal = theme === 'dark' ? 'text-white group-hover:text-slate-200' : 'text-slate-900 group-hover:text-slate-700' const userLookup = useMemo(() => { const map = new Map() users.forEach((user) => map.set(user.id, user)) return map }, [users]) const { treeRoots } = useMemo(() => { if (!users.length) { return { treeRoots: [] as UserTreeNode[] } } const childMap = new Map() users.forEach((user) => { if (user?.id) { childMap.set(user.id, []) } }) const rootIds: string[] = [] users.forEach((user) => { if (!user?.id) return const parentId = user.parentId if (parentId && childMap.has(parentId)) { childMap.get(parentId)!.push(user.id) } else { rootIds.push(user.id) } }) const compareUsers = (a: User, b: User) => { if (a.isSuperAdmin && !b.isSuperAdmin) return -1 if (!a.isSuperAdmin && b.isSuperAdmin) return 1 if (a.role === 'ADMIN' && b.role !== 'ADMIN') return -1 if (a.role !== 'ADMIN' && b.role === 'ADMIN') return 1 const labelA = (a.username || a.email || '').toLowerCase() const labelB = (b.username || b.email || '').toLowerCase() return labelA.localeCompare(labelB, 'zh-CN') } const buildNodes = (ids: string[]): UserTreeNode[] => ids .map((id) => { const user = userLookup.get(id) if (!user) return null return { user, children: buildNodes(childMap.get(id) || []), } }) .filter((node): node is UserTreeNode => Boolean(node)) .sort((a, b) => compareUsers(a.user, b.user)) return { treeRoots: buildNodes(rootIds) } }, [userLookup, users]) const toggleUserNodeCollapse = useCallback((nodeId: string) => { setCollapsedUserNodeIds((prev) => { const next = new Set(prev) if (next.has(nodeId)) { next.delete(nodeId) } else { next.add(nodeId) } return next }) }, []) const toggleDepartmentCollapse = useCallback((deptName: string) => { setCollapsedDepartments((prev) => { const next = new Set(prev) if (next.has(deptName)) { next.delete(deptName) } else { next.add(deptName) } return next }) }, []) const handleEditUser = useCallback( (user: User) => { editingUserModal.open(user) setSelectedDepartment(user.department || '') setEditingUsername(user.username || user.email || '') }, [editingUserModal], ) const handleCloseTransferModal = useCallback(() => { passwordModal.close() setTransferPassword('') }, [passwordModal]) const handleCloseEditModal = useCallback(() => { editingUserModal.close() setSelectedDepartment('') setEditingUsername('') handleCloseTransferModal() }, [editingUserModal, handleCloseTransferModal]) const handleSaveUser = useCallback(async () => { if (!editingUser) return const success = await onUpdateUser(editingUser, { username: editingUsername, department: selectedDepartment, }) if (success) { handleCloseEditModal() } }, [editingUser, editingUsername, onUpdateUser, selectedDepartment, handleCloseEditModal]) const handleModalRole = useCallback(async () => { if (!editingUser) return await onSetRole(editingUser.id, editingUser.role) handleCloseEditModal() }, [editingUser, onSetRole, handleCloseEditModal]) const handleModalActive = useCallback(async () => { if (!editingUser) return await onToggleActive(editingUser.id, editingUser.isActive) handleCloseEditModal() }, [editingUser, onToggleActive, handleCloseEditModal]) const handleModalDownload = useCallback(async () => { if (!editingUser) return await onToggleDownload(editingUser.id, editingUser.canDownload) handleCloseEditModal() }, [editingUser, onToggleDownload, handleCloseEditModal]) const { confirm } = useDialog() const handleDeleteUserInternal = useCallback(async () => { if (!editingUser) return const confirmed = await confirm({ title: '确认删除', message: '确定要删除这个用户吗?该用户的所有素材也会被删除。', variant: 'danger', }) if (!confirmed) return const success = await onDeleteUser(editingUser.id) if (success) { handleCloseEditModal() } }, [editingUser, onDeleteUser, handleCloseEditModal, confirm]) const showTransferButton = useMemo( () => Boolean(currentIsSuperAdmin && editingUser && editingUser.email !== currentUserEmail), [currentIsSuperAdmin, editingUser, currentUserEmail] ) const handleOpenTransferModal = useCallback(() => { if (!editingUser || !showTransferButton) return setTransferPassword('') passwordModal.open() }, [editingUser, passwordModal, showTransferButton]) const handleConfirmTransfer = useCallback(async () => { if (!editingUser) return const success = await onTransferSuperAdmin(editingUser.email, transferPassword) if (success) { handleCloseTransferModal() handleCloseEditModal() } }, [editingUser, onTransferSuperAdmin, transferPassword, handleCloseTransferModal, handleCloseEditModal]) const treeToRender = useMemo(() => { const term = searchTerm.trim().toLowerCase() if (!term) return treeRoots const matchUser = (user: User) => { const username = user.username?.toLowerCase() || '' const email = user.email?.toLowerCase() || '' const department = user.department?.toLowerCase() || '' return username.includes(term) || email.includes(term) || department.includes(term) } const filterNodes = (nodes: UserTreeNode[]): UserTreeNode[] => nodes .map((node) => { const filteredChildren = filterNodes(node.children) return matchUser(node.user) || filteredChildren.length ? { user: node.user, children: filteredChildren } : null }) .filter((node): node is UserTreeNode => Boolean(node)) return filterNodes(treeRoots) }, [treeRoots, searchTerm]) const departmentTree = useMemo( () => buildDepartmentTree(departments), [departments], ) const departmentUsersMap = useMemo(() => { const map = new Map() const ensureEntry = (name: string) => { if (!map.has(name)) { map.set(name, { managers: [], regulars: [] }) } return map.get(name)! } const sortUsers = (a: User, b: User) => { if (a.isSuperAdmin && !b.isSuperAdmin) return -1 if (!a.isSuperAdmin && b.isSuperAdmin) return 1 const labelA = (a.username || a.email || '').toLowerCase() const labelB = (b.username || b.email || '').toLowerCase() return labelA.localeCompare(labelB, 'zh-CN') } users.forEach((user) => { const deptName = (user.department && user.department.trim()) || '未分配部门' const entry = ensureEntry(deptName) if (user.isSuperAdmin || user.managedDepartment || user.role === 'ADMIN') { entry.managers.push(user) } else { entry.regulars.push(user) } }) departments.forEach((dept) => ensureEntry(dept.name)) ensureEntry('未分配部门') map.forEach((entry) => { entry.managers.sort(sortUsers) entry.regulars.sort(sortUsers) }) return map }, [users, departments]) const departmentMaterialCounts = useMemo(() => { const counts = new Map() users.forEach((user) => { const deptName = (user.department && user.department.trim()) || '未分配部门' const current = counts.get(deptName) || 0 counts.set(deptName, current + (user._count?.materials || 0)) }) return counts }, [users]) const hasDepartments = departments.length > 0 const editingUserDepartment = editingUser?.department ?? '' const managerNames = useMemo(() => { if (!editingUserDepartment) return [] return users .filter((candidate) => candidate.managedDepartment === editingUserDepartment) .map((manager) => manager.username) .filter(Boolean) as string[] }, [editingUserDepartment, users]) const renderStatusBadges = (user: User) => ( <> {user.isActive ? '已激活' : '未激活'} {user.canDownload ? '可下载' : '禁止下载'} ) const renderActionButtons = useCallback((user: User, options?: { showRoleToggle?: boolean }) => { const allowed = canManageUser(user) if (!allowed && user.id !== currentUserId) { return 无权限修改 } const actionButton = ( label: string, className: string, onClick: () => void, visible: boolean = true, ) => { if (!visible) return null return ( ) } return (
{actionButton( '编辑', styles.buttonStyles.secondary, () => handleEditUser(user), )} {actionButton( user.isActive ? '禁用' : '激活', user.isActive ? styles.buttonStyles.danger : styles.buttonStyles.success, () => onToggleActive(user.id, user.isActive), )} {actionButton( user.canDownload ? '禁止下载' : '允许下载', styles.buttonStyles.secondary, () => onToggleDownload(user.id, user.canDownload), )} {actionButton( user.role === 'ADMIN' ? '取消管理员' : '设为管理员', styles.buttonStyles.rainbow, () => onSetRole(user.id, user.role), options?.showRoleToggle !== false && !user.isSuperAdmin, )}
) }, [canManageUser, currentUserId, handleEditUser, onToggleActive, onToggleDownload, onSetRole]) const renderUserRow = useCallback( (user: User, isManager = false) => (
{user.username} {user.isSuperAdmin && ( 最高权限 )} {isManager && !user.isSuperAdmin && ( 管理员 )} {renderStatusBadges(user)}
素材:{user._count.materials}
{renderActionButtons(user)}
), [renderActionButtons], ) const showSearchResults = Boolean(searchTerm.trim()) return ( <>

成员目录

结合组织树与搜索框,可按姓名、邮箱或部门快速定位管理员与成员。

setSearchTerm(e.target.value)} className={cn(styles.inputStyles.base, 'pl-11 pr-10')} /> {searchTerm && ( )}
{showSearchResults ? ( treeToRender.length > 0 ? ( ) : (
暂无匹配的用户
) ) : ( )}
) }