435 lines
16 KiB
TypeScript
435 lines
16 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'
|
||
|
||
// 强制设置全局标记,确保新代码已加载(在页面加载时立即执行)
|
||
if (typeof window !== 'undefined') {
|
||
;(window as any).__ADMIN_PAGE_LOADED__ = true
|
||
;(window as any).__ADMIN_PAGE_VERSION__ = '2025-11-23-v4'
|
||
console.log('[AdminPage] 🚀 页面已加载,版本:', (window as any).__ADMIN_PAGE_VERSION__)
|
||
}
|
||
|
||
// 类型断言函数:安全地获取 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, session, router])
|
||
|
||
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">
|
||
|
||
{/* Tabs - Apple风格动画(使用统一导航样式) */}
|
||
<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?.role === 'ADMIN' && (
|
||
<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>
|
||
|
||
{/* Template Management */}
|
||
{activeTab === 'templates' && (
|
||
<TabErrorBoundary tabKey="templates" onRetry={fetchData}>
|
||
<TemplateManager showToast={showToast} />
|
||
</TabErrorBoundary>
|
||
)}
|
||
|
||
{/* Materials Management */}
|
||
{activeTab === 'materials' && (
|
||
<TabErrorBoundary tabKey="materials">
|
||
<MaterialsTab showToast={showToast} />
|
||
</TabErrorBoundary>
|
||
)}
|
||
|
||
{/* Categories Management */}
|
||
{activeTab === 'categories' && (
|
||
<TabErrorBoundary tabKey="categories" onRetry={fetchData}>
|
||
<CategoriesTab showToast={showToast} onDataChange={fetchData} />
|
||
</TabErrorBoundary>
|
||
)}
|
||
|
||
{/* Invitation Management */}
|
||
{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>
|
||
)
|
||
}
|
||
|