flnxi-Wu/app/admin/page.tsx
2026-05-28 19:34:17 +08:00

427 lines
15 KiB
TypeScript

'use client'
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Header from '@/components/Header'
import TemplateManager from './_components/TemplateManager'
import MonitorTab from './_components/MonitorTab'
import CategoriesTab from './_components/CategoriesTab'
import InvitationsTab from './_components/InvitationsTab'
import MaterialsTab from './_components/MaterialsTab'
import DepartmentsTab from './_components/DepartmentsTab'
import UsersTab from './_components/UsersTab'
import TabErrorBoundary from './_components/TabErrorBoundary'
import LoadingState from './_components/ui/LoadingState'
import ErrorState from './_components/ui/ErrorState'
import EmptyState from './_components/ui/EmptyState'
import { getDepartmentCollator } from '@/lib/departments'
import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles'
import { animations, textAnimations } from '@/lib/ui-animations'
import type { User, AdminTab, Department, SessionUser, ApiErrorResponse, ApiSuccessResponse } from './_components/types'
import { apiRequest, parseErrorResponse, getErrorMessage } from './_utils/api'
import { validateDepartmentName, validateEmail, validatePassword } from './_utils/validators'
import usePermissions from './_hooks/usePermissions'
import { useToast } from './_hooks/useToast'
import { useDataFetch } from './_hooks/useDataFetch'
import { useUserOperations } from './_hooks/useUserOperations'
import { useDepartmentOperations } from './_hooks/useDepartmentOperations'
// 类型断言函数:安全地获取 SessionUser
function getSessionUser(session: { user?: unknown } | null): SessionUser | null {
if (!session?.user) return null
const user = session.user as SessionUser
return user
}
export default function AdminPage() {
const styles = useThemeStyles()
const theme = useTheme()
const tabIndicatorClass = theme === 'dark' ? 'bg-indigo-300' : 'bg-indigo-600'
const tabIndicatorLineClass = 'absolute bottom-0 left-3 right-3 h-[2px] rounded-full'
const tabFocusRingClass =
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900'
const { data: session, status } = useSession()
const sessionUser = getSessionUser(session)
const router = useRouter()
const [activeTab, setActiveTab] = useState<AdminTab>('users')
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const hasLoadedOnceRef = useRef(false)
const { toast, showToast, dismissToast } = useToast()
const [departments, setDepartments] = useState<Department[]>([])
const departmentCollator = useMemo(() => getDepartmentCollator(), [])
const { loadUsersData, fetchDepartments, fetchTabData } = useDataFetch({
showToast,
departmentCollator,
setUsers,
setDepartments,
})
const userLookup = useMemo(() => {
const map = new Map<string, User>()
users.forEach((user) => map.set(user.id, user))
return map
}, [users])
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login')
return
}
const isSuperAdmin = sessionUser?.isSuperAdmin
if (status === 'authenticated' && sessionUser?.role !== 'ADMIN' && !isSuperAdmin) {
router.push('/')
return
}
}, [status, router, sessionUser?.isSuperAdmin, sessionUser?.role])
const fetchData = useCallback(async () => {
const blockUI = !hasLoadedOnceRef.current
if (activeTab === 'templates' || activeTab === 'monitor') {
if (blockUI) {
setLoading(false)
hasLoadedOnceRef.current = true
}
return
}
if (blockUI) {
setLoading(true)
}
try {
await fetchTabData(activeTab)
} catch (error) {
console.error('Error fetching data:', error)
} finally {
if (blockUI) {
setLoading(false)
hasLoadedOnceRef.current = true
}
}
}, [activeTab, fetchTabData])
useEffect(() => {
if (status === 'authenticated') {
fetchData()
}
}, [status, fetchData])
const handleTransferSuperAdmin = async (targetEmail: string, password: string) => {
const emailValidation = validateEmail(targetEmail)
if (!emailValidation.valid) {
showToast(emailValidation.error || '请提供有效的目标用户邮箱', 'error')
return false
}
const passwordValidation = validatePassword(password)
if (!passwordValidation.valid) {
showToast(passwordValidation.error || '请输入密码', 'error')
return false
}
return transferSuperAdmin(targetEmail, password)
}
// 用户层级信息,用于处理上下级/权限等逻辑
const findDepartmentManagers = useCallback(
(departmentName: string | null | undefined) => {
if (!departmentName) return null
const result = users.filter(
(candidate) => candidate.managedDepartment && candidate.managedDepartment === departmentName
)
return result.length > 0 ? result : null
},
[users]
)
const { currentUserId, currentIsSuperAdmin, canManageUser } = usePermissions({
sessionUser,
users,
departments,
})
const { updateUser, deleteUser, toggleDownload, toggleActive, setRole, transferSuperAdmin } = useUserOperations({
users,
setUsers,
showToast,
fetchData,
findDepartmentManagers,
})
const { createDepartment, deleteDepartment, renameDepartment } = useDepartmentOperations({
departments,
setDepartments,
showToast,
fetchDepartments,
loadUsersData,
})
const handleUpdateUser = updateUser
const handleDeleteUser = deleteUser
const handleToggleDownload = toggleDownload
const handleToggleActive = toggleActive
const handleSetRole = setRole
if (status === 'loading' || loading) {
return <LoadingState message="加载中..." className="min-h-screen" />
}
return (
<div className={cn('min-h-screen', theme === 'dark' ? 'bg-slate-900' : 'bg-slate-50')}>
<Header session={session} />
{/* Toast通知 */}
{toast && (
<div
role="status"
aria-live="polite"
className={`fixed top-6 left-1/2 -translate-x-1/2 z-[100] px-6 py-3 rounded-lg shadow-lg flex items-center gap-3 ${
toast.type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white'
} animate-fade-in`}
>
<span>{toast.message}</span>
<button
onClick={dismissToast}
className="p-1 rounded-full hover:bg-white/20 transition-colors"
aria-label="关闭提示"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* 管理导航使用统一导航样式 */}
<nav role="tablist" aria-label="管理功能导航" className={cn('flex space-x-1 mb-8 overflow-x-auto')}>
{/* 监控仪表板已移除,统一通过头像下拉菜单访问 */}
<button
type="button"
role="tab"
aria-selected={activeTab === 'users'}
aria-controls="users-panel"
id="users-tab"
onClick={() => setActiveTab('users')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveTab('users')
}
}}
className={cn(
activeTab === 'users' ? styles.navItemStyles.tabActive : styles.navItemStyles.tabBase,
tabFocusRingClass
)}
>
<span className="relative z-10">用户管理</span>
{activeTab === 'users' && (
<span className={cn(tabIndicatorLineClass, tabIndicatorClass, animations.slideInFromLeft)} />
)}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'departments'}
aria-controls="departments-panel"
id="departments-tab"
onClick={() => setActiveTab('departments')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveTab('departments')
}
}}
className={cn(
activeTab === 'departments' ? styles.navItemStyles.tabActive : styles.navItemStyles.tabBase,
tabFocusRingClass
)}
>
<span className="relative z-10">架构管理</span>
{activeTab === 'departments' && (
<span className={cn(tabIndicatorLineClass, tabIndicatorClass, animations.slideInFromLeft)} />
)}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'templates'}
aria-controls="templates-panel"
id="templates-tab"
onClick={() => setActiveTab('templates')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveTab('templates')
}
}}
className={cn(
activeTab === 'templates' ? styles.navItemStyles.tabActive : styles.navItemStyles.tabBase,
tabFocusRingClass
)}
>
<span className="relative z-10">模板管理</span>
{activeTab === 'templates' && (
<span className={cn(tabIndicatorLineClass, tabIndicatorClass, animations.slideInFromLeft)} />
)}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'categories'}
aria-controls="categories-panel"
id="categories-tab"
onClick={() => setActiveTab('categories')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveTab('categories')
}
}}
className={cn(
activeTab === 'categories' ? styles.navItemStyles.tabActive : styles.navItemStyles.tabBase,
tabFocusRingClass
)}
>
<span className="relative z-10">分类管理</span>
{activeTab === 'categories' && (
<span className={cn(tabIndicatorLineClass, tabIndicatorClass, animations.slideInFromLeft)} />
)}
</button>
{/* 邀请管理 - 只有最高权限账号才能看到 */}
{sessionUser?.isSuperAdmin && (
<button
type="button"
role="tab"
aria-selected={activeTab === 'invitations'}
aria-controls="invitations-panel"
id="invitations-tab"
onClick={() => setActiveTab('invitations')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveTab('invitations')
}
}}
className={cn(
activeTab === 'invitations' ? styles.navItemStyles.tabActive : styles.navItemStyles.tabBase,
tabFocusRingClass
)}
>
<span className="relative z-10">邀请管理</span>
{activeTab === 'invitations' && (
<span className={cn(tabIndicatorLineClass, tabIndicatorClass, animations.slideInFromLeft)} />
)}
</button>
)}
<button
type="button"
role="tab"
aria-selected={activeTab === 'materials'}
aria-controls="materials-panel"
id="materials-tab"
onClick={() => setActiveTab('materials')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveTab('materials')
}
}}
className={cn(
activeTab === 'materials' ? styles.navItemStyles.tabActive : styles.navItemStyles.tabBase,
tabFocusRingClass
)}
>
<span className="relative z-10">素材管理</span>
{activeTab === 'materials' && (
<span className={cn(tabIndicatorLineClass, tabIndicatorClass, animations.slideInFromLeft)} />
)}
</button>
</nav>
{/* 模板管理 */}
{activeTab === 'templates' && (
<TabErrorBoundary tabKey="templates" onRetry={fetchData}>
<TemplateManager showToast={showToast} />
</TabErrorBoundary>
)}
{/* 素材管理 */}
{activeTab === 'materials' && (
<TabErrorBoundary tabKey="materials">
<MaterialsTab showToast={showToast} />
</TabErrorBoundary>
)}
{/* 分类管理 */}
{activeTab === 'categories' && (
<TabErrorBoundary tabKey="categories" onRetry={fetchData}>
<CategoriesTab showToast={showToast} onDataChange={fetchData} isSuperAdmin={currentIsSuperAdmin} />
</TabErrorBoundary>
)}
{/* 邀请管理 */}
{activeTab === 'invitations' && (
<TabErrorBoundary tabKey="invitations">
<InvitationsTab showToast={showToast} />
</TabErrorBoundary>
)}
{/* 架构管理 */}
{activeTab === 'departments' && (
<TabErrorBoundary tabKey="departments" onRetry={fetchData}>
<DepartmentsTab
departments={departments}
onCreateDepartment={async (name) => { await createDepartment(name) }}
onRenameDepartment={async (deptId, newName) => { await renameDepartment(deptId, newName) }}
onDeleteDepartment={async (id, name) => { await deleteDepartment(id, name) }}
showToast={showToast}
/>
</TabErrorBoundary>
)}
{/* Old Categories Management - 已迁移到 CategoriesTab - 备份代码已删除 */}
{activeTab === 'users' && (
<TabErrorBoundary tabKey="users" onRetry={fetchData}>
<div role="tabpanel" id="users-panel" aria-labelledby="users-tab">
<UsersTab
users={users}
departments={departments}
currentUserId={currentUserId}
currentUserEmail={sessionUser?.email}
currentIsSuperAdmin={currentIsSuperAdmin}
canManageUser={canManageUser}
onToggleActive={handleToggleActive}
onToggleDownload={handleToggleDownload}
onSetRole={handleSetRole}
onUpdateUser={handleUpdateUser}
onDeleteUser={handleDeleteUser}
onTransferSuperAdmin={handleTransferSuperAdmin}
/>
</div>
</TabErrorBoundary>
)}
{/* 监控仪表板 */}
{activeTab === 'monitor' && (
<TabErrorBoundary tabKey="monitor" onRetry={fetchData}>
<div role="tabpanel" id="monitor-panel" aria-labelledby="monitor-tab">
<MonitorTab showToast={showToast} />
</div>
</TabErrorBoundary>
)}
</main>
{/* 创建邀请链接模态框 */}
</div>
)
}