/** * 用户管理组件 * 提取自 app/admin/page.tsx 的用户管理功能 */ 'use client' import { useState, useCallback, useRef, useEffect, useMemo } from 'react' import { format } from 'date-fns' import type { User, Department } from '@/hooks/useAdminState' import DepartmentTreeSelect from '@/components/DepartmentTreeSelect' import { buildDepartmentDescendants } from '@/lib/departments' import { parseFetchError, handleError } from '@/lib/error-handler' // 用户分组类型(参考 flnxi_sucai) interface DepartmentUserGrouping { managers: User[] regulars: User[] } interface UserManagementProps { users: User[] setUsers: React.Dispatch> departments: Department[] userSearchTerm: string setUserSearchTerm: React.Dispatch> collapsedDepartments: Set collapsedAdmins: Set toggleDepartmentCollapse: (dept: string) => void toggleAdminCollapse: (adminId: string) => void showToast: (message: string, type?: 'success' | 'error') => void fetchData: () => Promise currentUserEmail?: string } export default function UserManagement({ users, setUsers, departments, userSearchTerm, setUserSearchTerm, collapsedDepartments, collapsedAdmins, toggleDepartmentCollapse, toggleAdminCollapse, showToast, fetchData, currentUserEmail, }: UserManagementProps) { // 编辑用户部门 const [editingUser, setEditingUser] = useState(null) const [selectedDepartment, setSelectedDepartment] = useState('') // 组织用户树形结构数据(根据部门层级结构,与部门管理逻辑统一) const getUserTreeData = useCallback(() => { // 构建部门ID到部门的映射 const deptMap = new Map() departments.forEach(dept => { deptMap.set(dept.id, dept) }) // 构建部门名称到部门的映射(用于匹配用户的department字段) const deptNameMap = new Map() departments.forEach(dept => { deptNameMap.set(dept.name, dept) }) // 构建用户到部门的映射(通过department或departmentId) const getUserDepartment = (user: User): Department | null => { const userAny = user as any // 优先使用departmentRef.id(新字段,来自API的include) if (userAny.departmentRef?.id) { const dept = deptMap.get(userAny.departmentRef.id) if (dept) return dept } // 其次使用departmentId(新字段) if (userAny.departmentId) { const dept = deptMap.get(userAny.departmentId) if (dept) return dept } // 回退到department名称(旧字段) // 注意:department 可能是空字符串 '',需要检查是否为 truthy if (user.department && user.department.trim() !== '') { const dept = deptNameMap.get(user.department) if (dept) return dept } return null } // 获取部门的所有用户(包括管理员和普通用户)- 只包括直接属于该部门的用户 const getUsersByDepartment = (dept: Department): User[] => { return users.filter(u => { const userDept = getUserDepartment(u) return userDept && userDept.id === dept.id }) } // 获取部门的管理员(通过managerId匹配) const getDepartmentManager = (dept: Department): User | null => { if (!dept.managerId) return null return users.find(u => u.id === dept.managerId) || null } // 构建部门用户分组映射(参考 flnxi_sucai 的 departmentUsersMap 逻辑) const departmentUsersMap = (() => { const map = new Map() // 确保每个部门都有条目 const ensureEntry = (deptName: string): DepartmentUserGrouping => { if (!map.has(deptName)) { map.set(deptName, { managers: [], regulars: [] }) } return map.get(deptName)! } // 用户排序函数 const sortUsers = (a: User, b: User): number => { const userAnyA = a as any const userAnyB = b as any // 超级管理员优先 if (userAnyA.isSuperAdmin && !userAnyB.isSuperAdmin) return -1 if (!userAnyA.isSuperAdmin && userAnyB.isSuperAdmin) return 1 // 管理员角色优先 if ((userAnyA.role === 'SYSTEM_ADMIN' || userAnyA.role === 'DEPT_ADMIN' || userAnyA.role === 'ADMIN') && (userAnyB.role !== 'SYSTEM_ADMIN' && userAnyB.role !== 'DEPT_ADMIN' && userAnyB.role !== 'ADMIN')) return -1 if ((userAnyA.role !== 'SYSTEM_ADMIN' && userAnyA.role !== 'DEPT_ADMIN' && userAnyA.role !== 'ADMIN') && (userAnyB.role === 'SYSTEM_ADMIN' || userAnyB.role === 'DEPT_ADMIN' || userAnyB.role === 'ADMIN')) 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 userDept = getUserDepartment(user) const deptName = userDept ? userDept.name : '未分配部门' const entry = ensureEntry(deptName) const userAny = user as any // 判断是否为管理员:超级管理员、部门管理员(通过managerId匹配)、管理员角色 const isManager = userAny.isSuperAdmin || userDept?.managerId === user.id || userAny.role === 'SYSTEM_ADMIN' || userAny.role === 'DEPT_ADMIN' || userAny.role === 'ADMIN' if (isManager) { 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 })() // 构建部门树形结构(与部门管理逻辑统一:找出所有顶级部门,然后使用childrenMap) const departmentTree = (() => { // 找出所有顶级部门(没有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) } }) // 排序子部门 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, childrenMap } })() // 递归获取部门及其所有子部门的用户总数(用于显示) const getTotalUsersCountIncludingChildren = (deptId: string): number => { let count = getUsersByDepartment({ id: deptId } as Department).length const children = departmentTree.childrenMap.get(deptId) || [] children.forEach(child => { count += getTotalUsersCountIncludingChildren(child.id) }) return count } // 获取未分配部门的用户(包括department为空字符串、null、undefined或找不到匹配部门的情况) const usersWithoutDepartment = users.filter(u => { const userDept = getUserDepartment(u) // 如果找不到匹配的部门,则认为是未分配部门的用户 if (!userDept) return true return false }) return { departmentTree, getUsersByDepartment, getTotalUsersCountIncludingChildren, getDepartmentManager, getUserDepartment, usersWithoutDepartment, departmentUsersMap, } }, [users, departments]) // 编辑用户部门 const handleEditUserDepartment = (user: User) => { setEditingUser(user) // 获取用户的部门名称(API接受名称) setSelectedDepartment(user.department || '') } // 更新用户部门 const handleUpdateUserDepartment = async () => { if (!editingUser) return try { const response = await fetch('/api/admin/users/update-department', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: editingUser.id, department: selectedDepartment, }), }) if (response.ok) { // 重新获取数据以同步更新 await fetchData() setEditingUser(null) setSelectedDepartment('') showToast('部门更新成功') } else { 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 handleDeleteUser = async (id: string) => { if (!confirm('确定要归档这个用户吗?归档后账号无法登录,素材和历史记录会保留。')) return try { const response = await fetch(`/api/admin/users?id=${id}`, { method: 'DELETE', }) if (response.ok) { setUsers(prevUsers => prevUsers.map(user => ( user.id === id ? { ...user, status: 'archived', isActive: false, archivedAt: new Date().toISOString() } : user ))) showToast('用户已归档') } else { const parsedError = await parseFetchError(response) showToast(parsedError.message, 'error') } } catch (error) { console.error('Error deleting user:', error) const { message } = handleError(error) showToast(message, 'error') } } // 切换下载权限 const handleToggleDownload = async (userId: string, currentCanDownload: boolean) => { const action = currentCanDownload ? '关闭' : '开启' if (!confirm(`确定要${action}该用户的下载权限吗?`)) return try { const response = await fetch('/api/admin/users/toggle-download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId }), }) if (response.ok) { setUsers(prevUsers => prevUsers.map(user => user.id === userId ? { ...user, canDownload: !currentCanDownload } : user ) ) showToast(`下载权限已${action}`) } else { const data = await response.json() showToast(data.error || '操作失败', 'error') } } catch (error) { console.error('Error toggling download permission:', error) showToast('操作失败', 'error') } } // 切换激活状态 const handleToggleActive = async (userId: string, currentStatus: boolean) => { const action = currentStatus ? '禁用' : '激活' if (!confirm(`确定要${action}这个用户吗?`)) return try { const response = await fetch('/api/admin/users/toggle-active', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, isActive: !currentStatus, }), }) if (response.ok) { setUsers(prevUsers => prevUsers.map(user => user.id === userId ? { ...user, isActive: !currentStatus } : user ) ) showToast(`${action}成功`) } else { const data = await response.json() showToast(data.error || `${action}失败`, 'error') } } catch (error) { console.error('Toggle active error:', error) showToast(`${action}失败`, 'error') } } // 角色选项配置(阶段四:支持所有角色) const roleOptions = [ { value: 'USER', label: '普通用户', description: '基础权限,可上传和查看素材' }, { value: 'ADMIN', label: '管理员', description: '系统管理员(向后兼容,建议使用系统管理员)' }, { value: 'DEPT_ADMIN', label: '部门管理员', description: '管理指定部门的用户和素材' }, { value: 'SYSTEM_ADMIN', label: '系统管理员', description: '拥有系统级管理权限' }, ] // 获取角色标签 const getRoleLabel = (role: string) => { const option = roleOptions.find(r => r.value === role) return option?.label || role } // 设置角色(阶段四:支持所有角色) const handleSetRole = async (userId: string, newRole: string) => { if (!confirm(`确定要将用户角色设置为"${getRoleLabel(newRole)}"吗?`)) return try { const response = await fetch('/api/admin/users/set-role', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, role: newRole, }), }) if (response.ok) { setUsers(prevUsers => prevUsers.map(user => user.id === userId ? { ...user, role: newRole } : user ) ) showToast(`角色已设置为"${getRoleLabel(newRole)}"`) } else { const data = await response.json() showToast(data.error || '设置角色失败', 'error') } } catch (error) { console.error('Set role error:', error) showToast('设置角色失败', 'error') } } const treeData = getUserTreeData() const filteredUsers = useMemo(() => { if (!userSearchTerm) return users const searchLower = userSearchTerm.toLowerCase() return users.filter(user => user.username.toLowerCase().includes(searchLower) || (user.department && user.department.toLowerCase().includes(searchLower)) || ((user as any).realName && (user as any).realName.toLowerCase().includes(searchLower)) || (user.email && user.email.toLowerCase().includes(searchLower)) ) }, [users, userSearchTerm]) // 当有搜索条件时,自动展开包含匹配用户的部门 useEffect(() => { if (!userSearchTerm) return // 找出包含匹配用户的部门ID const departmentsWithMatches = new Set() filteredUsers.forEach(user => { const userDept = treeData.getUserDepartment(user) if (userDept) { // 添加该部门及其所有父部门 let currentDept: Department | null = userDept while (currentDept) { departmentsWithMatches.add(currentDept.id) // 查找父部门 currentDept = currentDept.parentId ? departments.find(d => d.id === currentDept!.parentId) || null : null } } }) // 自动展开包含匹配用户的部门 departmentsWithMatches.forEach(deptId => { if (collapsedDepartments.has(deptId)) { toggleDepartmentCollapse(deptId) } }) // 如果有未分配部门的匹配用户,展开未分配部门 const hasUnassignedMatches = treeData.usersWithoutDepartment.some(u => filteredUsers.some(fu => fu.id === u.id) ) if (hasUnassignedMatches && collapsedDepartments.has('__UNASSIGNED__')) { toggleDepartmentCollapse('__UNASSIGNED__') } // eslint-disable-next-line react-hooks/exhaustive-deps }, [userSearchTerm, filteredUsers]) // 检查部门或其子部门是否有匹配的用户(用于搜索时过滤部门) const hasMatchingUsersInDepartment = useCallback((dept: Department): boolean => { if (!userSearchTerm) return true // 没有搜索条件时,显示所有部门 // 检查该部门是否有匹配的用户 const userGrouping = treeData.departmentUsersMap.get(dept.name) || { managers: [], regulars: [] } const allDeptUsers = [...userGrouping.managers, ...userGrouping.regulars] const hasMatchingUsers = allDeptUsers.some(u => filteredUsers.some(fu => fu.id === u.id)) if (hasMatchingUsers) return true // 检查子部门是否有匹配的用户(递归) const children = treeData.departmentTree.childrenMap.get(dept.id) || [] for (const childDept of children) { if (hasMatchingUsersInDepartment(childDept)) { return true } } return false }, [userSearchTerm, filteredUsers, treeData]) // 递归渲染部门树(使用用户分组逻辑,参考 flnxi_sucai) const renderDepartmentNode = (dept: Department, level: number = 0): React.ReactNode => { // 如果有搜索条件,只显示包含匹配用户的部门 if (userSearchTerm && !hasMatchingUsersInDepartment(dept)) { return null } const children = treeData.departmentTree.childrenMap.get(dept.id) || [] const hasChildren = children.length > 0 // 使用分组后的用户数据 const userGrouping = treeData.departmentUsersMap.get(dept.name) || { managers: [], regulars: [] } const managers = userGrouping.managers const regulars = userGrouping.regulars // 过滤用户(如果有搜索条件) const filteredManagers = !userSearchTerm ? managers : managers.filter(u => filteredUsers.some(fu => fu.id === u.id)) const filteredRegulars = !userSearchTerm ? regulars : regulars.filter(u => filteredUsers.some(fu => fu.id === u.id)) // 获取包括子部门在内的总用户数 const totalUsersCount = treeData.getTotalUsersCountIncludingChildren(dept.id) // 统计未激活人数(统计该部门的所有用户,包括管理员和普通成员) const allDeptUsers = [...managers, ...regulars] const inactiveCount = allDeptUsers.filter(u => { const userAny = u as any const isActive = userAny.isActive !== false && userAny.status !== 'inactive' && userAny.status !== 'disabled' return !isActive }).length // 检查是否有用户(包括子部门的用户)或有子部门,如果有就可以展开 const hasUsers = filteredManagers.length > 0 || filteredRegulars.length > 0 const hasUsersInChildren = hasChildren && totalUsersCount > allDeptUsers.length const canExpand = hasUsers || hasUsersInChildren || hasChildren const isCollapsed = collapsedDepartments.has(dept.id) const indent = level * 24 const paddingLeftStyle = { paddingLeft: `${indent + 16}px` } // 渲染用户组(管理员或普通成员) const renderUserGroup = (userList: User[], isManagerGroup: boolean, hasManagers: boolean) => { if (userList.length === 0) return null return (
{isManagerGroup && (
管理员
)} {!isManagerGroup && userList.length > 0 && hasManagers && (
成员
)}
{userList.map((user) => (
))}
) } return (
0 ? 'mt-2' : ''}`} > {/* 部门标题行 */}
canExpand && toggleDepartmentCollapse(dept.id)} >
{canExpand && (
e.stopPropagation()} >
)}
{dept.name} {dept.code && ( ({dept.code}) )} {managers.length > 0 && (
管理员: {managers.slice(0, 3).map((mgr) => ( {mgr.username} ))} {managers.length > 3 && ( 等{managers.length}人 )}
)} ({totalUsersCount} 人 {hasChildren && totalUsersCount > allDeptUsers.length && ( ,含子部门 )} {inactiveCount > 0 && ( ,{inactiveCount} 人未激活 )} )
{/* 部门用户列表(使用分组显示) */} {!isCollapsed && (
{/* 显示"暂无成员"提示 */} {filteredManagers.length === 0 && filteredRegulars.length === 0 && (
暂无成员
)} {/* 管理员组 */} {renderUserGroup(filteredManagers, true, filteredManagers.length > 0)} {/* 普通成员组 */} {renderUserGroup(filteredRegulars, false, filteredManagers.length > 0)}
)}
{/* 递归渲染子部门 */} {!isCollapsed && hasChildren && (
{children.map(child => renderDepartmentNode(child, level + 1))}
)}
) } return (
{/* 用户搜索框 */}
setUserSearchTerm(e.target.value)} className="w-full px-4 py-2 pl-10 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none" />
{/* 树形结构显示 */}
{/* 根据部门层级结构显示(与部门管理逻辑统一) */} {treeData.departmentTree.topLevelDepts.map(dept => renderDepartmentNode(dept, 0))} {/* 未分配部门的用户 */} {treeData.usersWithoutDepartment.length > 0 && (() => { // 如果有搜索条件,只显示匹配的用户 const unassignedUsers = !userSearchTerm ? treeData.usersWithoutDepartment : treeData.usersWithoutDepartment.filter(u => filteredUsers.some(fu => fu.id === u.id)) // 如果有搜索条件但没有匹配的用户,不显示未分配部门 if (userSearchTerm && unassignedUsers.length === 0) { return null } const isCollapsed = collapsedDepartments.has('__UNASSIGNED__') // 统计未激活人数 const inactiveCount = treeData.usersWithoutDepartment.filter(u => { const userAny = u as any const isActive = userAny.isActive !== false && userAny.status !== 'inactive' && userAny.status !== 'disabled' return !isActive }).length return (
unassignedUsers.length > 0 && toggleDepartmentCollapse('__UNASSIGNED__')} >
{unassignedUsers.length > 0 && (
e.stopPropagation()} >
)}
未分配部门 ({treeData.usersWithoutDepartment.length} 人 {inactiveCount > 0 && ( ,{inactiveCount} 人未激活 )} )
{!isCollapsed && unassignedUsers.map((user) => (
))}
) })()} {users.length === 0 && (
暂无用户
)}
{/* 编辑部门弹窗 */} {editingUser && (

编辑用户部门

用户: {editingUser.username}

{ // 将部门ID转换为部门名称(API接受名称) const dept = departments.find(d => d.id === deptId || d.name === deptId) setSelectedDepartment(dept ? dept.name : '') }} placeholder="请选择部门" allowEmpty={true} emptyLabel="未分配" className="w-full" />
)}
) } // 用户行组件 interface UserRowProps { user: User indentLevel: number onEditDepartment: (user: User) => void onSetRole: (userId: string, newRole: string) => void onToggleActive: (userId: string, currentStatus: boolean) => void onToggleDownload: (userId: string, currentCanDownload: boolean) => void onDelete: (userId: string) => void roleOptions: Array<{ value: string; label: string; description: string }> getRoleLabel: (role: string) => string isManager?: boolean } function UserRow({ user, indentLevel, onEditDepartment, onSetRole, onToggleActive, onToggleDownload, onDelete, roleOptions, getRoleLabel, isManager = false, }: UserRowProps) { const [showRoleMenu, setShowRoleMenu] = useState(false) // 获取角色标签颜色 const getRoleColor = (role: string) => { switch (role) { case 'SYSTEM_ADMIN': return 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200' case 'ADMIN': return 'bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200' case 'DEPT_ADMIN': return 'bg-cyan-100 dark:bg-cyan-900 text-cyan-800 dark:text-cyan-200' case 'USER': default: return 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300' } } return (
{user.username} {/* 管理员标识 */} {isManager && ( 部门管理员 )} {/* 角色标签(阶段四新增) */} {getRoleLabel(user.role)} {user.isActive ? '已激活' : '未激活'} {user.canDownload ? '可下载' : '禁止下载'}
素材: {user._count.materials} | 注册: {format(new Date(user.createdAt), 'yyyy-MM-dd')}
{/* 角色选择下拉菜单(阶段四更新) */}
{showRoleMenu && ( <>
setShowRoleMenu(false)} />
{roleOptions.map((option) => ( ))}
)}
) }