flnxi-Wu/app/admin/page.tsx
WuYongJie d1c6efc79d
Some checks failed
CI Check / check (push) Has been cancelled
feat: NAS 部署与 Harbor 推送流程
2025-12-03 14:39:06 +08:00

435 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
)
}