gongxiangp2/app/admin/page.tsx
2026-03-06 14:14:50 +08:00

561 lines
19 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 { useCallback, useEffect, useState } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Header from '@/components/Header'
import DepartmentPermissionsTab from '@/components/DepartmentPermissionsTab'
import SuperAdminsTab from '@/components/SuperAdminsTab'
import MaterialManagement from '@/components/admin/MaterialManagement'
import UserManagement from '@/components/admin/UserManagement'
import CategoryManagement from '@/components/admin/CategoryManagement'
import DepartmentManagement from '@/components/admin/DepartmentManagement'
import DataStatisticsTab from '@/components/admin/DataStatisticsTab'
// 类型定义
interface User {
id: string
email: string
username: string
department: string
role: string
isActive: boolean
canDownload: boolean
managedDepartment?: string
createdAt: string
_count: {
materials: number
}
}
interface Material {
id: string
previewUrl: string
productModel: string
categoryName: string
sceneName: string
materialType: string
renderer: string
authorName: string
createdAt: string
videoCoverUrl?: string
videoPreviewClipUrl?: string
videoPreview540Url?: string
user: {
username: string
}
}
interface Category {
id: string
name: string
count: number
}
interface Scene {
id: string
name: string
count: number
}
interface Platform {
id: string
name: string
count: number
}
interface ProductCategory {
id: string
name: string
count: number
}
interface Department {
id: string
name: string
// 阶段三:部门层级结构字段
code?: string | null
parentId?: string | null
level?: number
managerId?: string | null
status?: string
description?: string | null
sortOrder?: number
createdAt?: string
updatedAt?: string
// 关联数据(可选)
parent?: Department | null
children?: Department[]
manager?: {
id: string
email: string
username: string
realName?: string | null
} | null
}
export default function AdminPage() {
const { data: session, status } = useSession()
const router = useRouter()
const sessionUser = session?.user as {
role?: string
managedDepartment?: string | null
department?: string | null
isSuperAdmin?: boolean
email?: string | null
} | undefined
const currentUserRole = sessionUser?.role || ''
const canAccessAdminPage = currentUserRole === 'ADMIN' || currentUserRole === 'SYSTEM_ADMIN' || currentUserRole === 'DEPT_ADMIN'
const canManageSystemTabs = currentUserRole === 'ADMIN' || currentUserRole === 'SYSTEM_ADMIN'
const departmentScopeName = String(sessionUser?.managedDepartment || sessionUser?.department || '').trim() || null
const canSelectDepartmentInStats = canManageSystemTabs
// Tab状态
const [activeTab, setActiveTab] = useState<'materials' | 'users' | 'categories' | 'departments' | 'dataStats' | 'permissions' | 'superAdmins'>('dataStats')
const [loading, setLoading] = useState(true)
// 数据状态
const [materials, setMaterials] = useState<Material[]>([])
const [users, setUsers] = useState<User[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [scenes, setScenes] = useState<Scene[]>([])
const [platforms, setPlatforms] = useState<Platform[]>([])
const [productCategories, setProductCategories] = useState<ProductCategory[]>([])
const [departments, setDepartments] = useState<Department[]>([])
// Toast状态
const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null)
// 素材管理相关状态
const [selectedMaterials, setSelectedMaterials] = useState<Set<string>>(new Set())
const [showRecycleBin, setShowRecycleBin] = useState(false)
const [materialSearchTerm, setMaterialSearchTerm] = useState('')
const [displayedMaterialsCount, setDisplayedMaterialsCount] = useState(24)
const [currentMaterialsPage, setCurrentMaterialsPage] = useState(1)
const [hasMoreMaterials, setHasMoreMaterials] = useState(true)
const [isLoadingMoreMaterials, setIsLoadingMoreMaterials] = useState(false)
const [totalMaterialsCount, setTotalMaterialsCount] = useState(0) // 素材总数(用于显示)
const materialsPageSize = 50 // 增大分页大小,减少请求次数
// 用户管理相关状态
const [userSearchTerm, setUserSearchTerm] = useState('')
const [collapsedDepartments, setCollapsedDepartments] = useState<Set<string>>(new Set())
const [collapsedAdmins, setCollapsedAdmins] = useState<Set<string>>(new Set())
// Toast自动隐藏
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000)
return () => clearTimeout(timer)
}
}, [toast])
const showToast = useCallback((message: string, type: 'success' | 'error' = 'success') => {
setToast({ message, type })
}, [])
const fetchDepartments = useCallback(async () => {
try {
// 阶段三:获取包含层级关系的部门数据
const response = await fetch('/api/departments?includeHierarchy=true')
if (response.ok) {
const data = await response.json()
setDepartments(data)
// 默认所有部门收起
setCollapsedDepartments(new Set(data.map((d: Department) => d.id)))
}
} catch (error) {
console.error('Error fetching departments:', error)
}
}, [])
const fetchData = useCallback(async () => {
setLoading(true)
try {
if (activeTab === 'dataStats' || !canManageSystemTabs) {
return
} else if (activeTab === 'materials') {
setCurrentMaterialsPage(1)
setDisplayedMaterialsCount(24)
setHasMoreMaterials(true)
const url = showRecycleBin
? `/api/materials?page=1&pageSize=${materialsPageSize}&deleted=true`
: `/api/materials?page=1&pageSize=${materialsPageSize}`
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
const materialsData = data.materials || data
setMaterials(materialsData)
setDisplayedMaterialsCount(materialsData.length)
setCurrentMaterialsPage(1)
setHasMoreMaterials(materialsData.length >= materialsPageSize)
// 保存总数(用于显示)
if (data.totalCount !== undefined) {
setTotalMaterialsCount(data.totalCount)
}
}
} else if (activeTab === 'users') {
const response = await fetch('/api/admin/users')
if (response.ok) {
const data = await response.json()
setUsers(data)
}
fetchDepartments()
} else if (activeTab === 'categories') {
const [categoriesRes, scenesRes, platformsRes, productCategoriesRes] = await Promise.all([
fetch('/api/categories'),
fetch('/api/scenes'),
fetch('/api/platforms'),
fetch('/api/product-categories')
])
if (categoriesRes.ok) setCategories(await categoriesRes.json())
if (scenesRes.ok) setScenes(await scenesRes.json())
if (platformsRes.ok) setPlatforms(await platformsRes.json())
if (productCategoriesRes.ok) setProductCategories(await productCategoriesRes.json())
} else if (activeTab === 'departments' || activeTab === 'permissions') {
fetchDepartments()
}
} catch (error) {
console.error('Error fetching data:', error)
} finally {
setLoading(false)
}
}, [activeTab, canManageSystemTabs, fetchDepartments, showRecycleBin])
// 权限检查
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login')
return
}
if (status === 'authenticated' && !canAccessAdminPage) {
router.push('/')
return
}
}, [status, canAccessAdminPage, router])
useEffect(() => {
if (status === 'authenticated' && !canManageSystemTabs && activeTab !== 'dataStats') {
setActiveTab('dataStats')
}
}, [activeTab, canManageSystemTabs, status])
// 数据加载
useEffect(() => {
if (status === 'authenticated') {
fetchData()
}
}, [status, fetchData])
// 切换回收站时清空选中状态和搜索词
useEffect(() => {
void showRecycleBin
setSelectedMaterials(new Set())
setMaterialSearchTerm('') // 清空搜索词,避免状态混乱
}, [showRecycleBin])
// 加载更多素材
const loadMoreMaterials = useCallback(async () => {
if (isLoadingMoreMaterials || !hasMoreMaterials) return
if (materialSearchTerm.trim().length > 0) return
setIsLoadingMoreMaterials(true)
try {
const nextPage = currentMaterialsPage + 1
const url = showRecycleBin
? `/api/materials?page=${nextPage}&pageSize=${materialsPageSize}&deleted=true`
: `/api/materials?page=${nextPage}&pageSize=${materialsPageSize}`
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
const newMaterials = data.materials || data
if (newMaterials.length > 0) {
// 去重:过滤掉已经存在的素材,并同步更新 displayedMaterialsCount
setMaterials(prev => {
const existingIds = new Set(prev.map(m => m.id))
const uniqueNewMaterials = newMaterials.filter((m: { id: string }) => !existingIds.has(m.id))
// 同步更新 displayedMaterialsCount(使用实际去重后的数量)
setDisplayedMaterialsCount(prev.length + uniqueNewMaterials.length)
return [...prev, ...uniqueNewMaterials]
})
setCurrentMaterialsPage(nextPage)
setHasMoreMaterials(newMaterials.length >= materialsPageSize)
} else {
setHasMoreMaterials(false)
}
}
} catch (error) {
console.error('加载更多素材失败:', error)
} finally {
setIsLoadingMoreMaterials(false)
}
}, [isLoadingMoreMaterials, hasMoreMaterials, currentMaterialsPage, showRecycleBin, materialSearchTerm])
// 切换部门折叠状态
const toggleDepartmentCollapse = useCallback((departmentName: string) => {
setCollapsedDepartments(prev => {
const newSet = new Set(prev)
if (newSet.has(departmentName)) {
newSet.delete(departmentName)
} else {
newSet.add(departmentName)
}
return newSet
})
}, [])
// 切换管理员折叠状态
const toggleAdminCollapse = useCallback((adminId: string) => {
setCollapsedAdmins(prev => {
const newSet = new Set(prev)
if (newSet.has(adminId)) {
newSet.delete(adminId)
} else {
newSet.add(adminId)
}
return newSet
})
}, [])
// 加载状态
if (status === 'loading' || loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-xl text-gray-600 dark:text-gray-400">加载中...</div>
</div>
)
}
if (!session || !canAccessAdminPage) {
return null
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
{/* Toast通知 */}
{toast && (
<div className={`fixed top-20 right-4 z-50 px-6 py-3 rounded-lg shadow-lg ${
toast.type === 'success'
? 'bg-green-500 text-white'
: 'bg-red-500 text-white'
} animate-fade-in`}>
{toast.message}
</div>
)}
<main className="container mx-auto px-4 py-8 max-w-7xl">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">系统管理</h1>
{canManageSystemTabs && (
<button
type="button"
onClick={() => router.push('/admin/settings')}
className="btn-primary flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<title>外观设置</title>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
外观设置
</button>
)}
</div>
{/* Tabs */}
<div className="flex space-x-4 border-b border-gray-200 dark:border-gray-700 mb-8">
<button
type="button"
onClick={() => setActiveTab('dataStats')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'dataStats'
? 'text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
数据统计
</button>
{canManageSystemTabs && (
<>
<button
type="button"
onClick={() => setActiveTab('materials')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'materials'
? 'text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
素材管理
</button>
<button
type="button"
onClick={() => setActiveTab('users')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'users'
? 'text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
用户管理
</button>
<button
type="button"
onClick={() => setActiveTab('categories')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'categories'
? 'text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
分类管理
</button>
<button
type="button"
onClick={() => setActiveTab('departments')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'departments'
? 'text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
部门管理
</button>
{sessionUser?.isSuperAdmin && (
<>
<button
type="button"
onClick={() => setActiveTab('permissions')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'permissions'
? 'text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
部门权限配置
</button>
<button
type="button"
onClick={() => setActiveTab('superAdmins')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'superAdmins'
? 'text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
超级管理员
</button>
</>
)}
</>
)}
</div>
{/* 数据统计 Tab */}
{activeTab === 'dataStats' && (
<DataStatisticsTab
showToast={showToast}
canViewLogs={canManageSystemTabs}
canSelectDepartment={canSelectDepartmentInStats}
departmentScopeName={departmentScopeName}
/>
)}
{/* 素材管理 Tab */}
{canManageSystemTabs && activeTab === 'materials' && (
<MaterialManagement
materials={materials}
setMaterials={setMaterials}
selectedMaterials={selectedMaterials}
setSelectedMaterials={setSelectedMaterials}
showRecycleBin={showRecycleBin}
setShowRecycleBin={setShowRecycleBin}
materialSearchTerm={materialSearchTerm}
setMaterialSearchTerm={setMaterialSearchTerm}
displayedMaterialsCount={displayedMaterialsCount}
setDisplayedMaterialsCount={setDisplayedMaterialsCount}
hasMoreMaterials={hasMoreMaterials}
setHasMoreMaterials={setHasMoreMaterials}
isLoadingMoreMaterials={isLoadingMoreMaterials}
showToast={showToast}
loadMoreMaterials={loadMoreMaterials}
fetchData={fetchData}
materialsPageSize={materialsPageSize}
totalMaterialsCount={totalMaterialsCount}
/>
)}
{/* 用户管理 Tab */}
{canManageSystemTabs && activeTab === 'users' && (
<UserManagement
users={users}
setUsers={setUsers}
departments={departments}
userSearchTerm={userSearchTerm}
setUserSearchTerm={setUserSearchTerm}
collapsedDepartments={collapsedDepartments}
collapsedAdmins={collapsedAdmins}
toggleDepartmentCollapse={toggleDepartmentCollapse}
toggleAdminCollapse={toggleAdminCollapse}
showToast={showToast}
fetchData={fetchData}
currentUserEmail={session?.user?.email || undefined}
/>
)}
{/* 分类管理 Tab */}
{canManageSystemTabs && activeTab === 'categories' && (
<CategoryManagement
categories={categories}
setCategories={setCategories}
scenes={scenes}
setScenes={setScenes}
platforms={platforms}
setPlatforms={setPlatforms}
productCategories={productCategories}
setProductCategories={setProductCategories}
showToast={showToast}
/>
)}
{/* 部门管理 Tab */}
{canManageSystemTabs && activeTab === 'departments' && (
<DepartmentManagement
departments={departments}
setDepartments={setDepartments}
showToast={showToast}
fetchDepartments={fetchDepartments}
/>
)}
{/* 部门权限配置 Tab */}
{canManageSystemTabs && activeTab === 'permissions' && sessionUser?.isSuperAdmin && (
<DepartmentPermissionsTab
showToast={showToast}
departments={departments}
/>
)}
{/* 超级管理员 Tab */}
{canManageSystemTabs && activeTab === 'superAdmins' && sessionUser?.isSuperAdmin && (
<SuperAdminsTab
showToast={showToast}
/>
)}
</main>
</div>
)
}