gongxiangp2/app/admin/page.tsx

474 lines
17 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 MaterialsHeader from '@/components/board/MaterialsHeader'
import DepartmentPermissionsTab from '@/components/DepartmentPermissionsTab'
import SuperAdminsTab from '@/components/SuperAdminsTab'
import MaterialManagement from '@/components/admin/MaterialManagement'
import CategoryManagement from '@/components/admin/CategoryManagement'
import DataStatisticsTab from '@/components/admin/DataStatisticsTab'
import PublicTrafficTab from '@/components/admin/PublicTrafficTab'
// 类型定义
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' | 'categories' | 'dataStats' | 'permissions' | 'superAdmins' | 'publicTraffic'>('dataStats')
const [loading, setLoading] = useState(true)
// 数据状态
const [materials, setMaterials] = useState<Material[]>([])
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 // 增大分页大小,减少请求次数
// 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 () => {
// 阶段三:获取包含层级关系的部门数据
const response = await fetch('/api/departments?includeHierarchy=true')
if (!response.ok) {
throw new Error('部门数据加载失败')
}
const data = await response.json()
setDepartments(data)
}, [])
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) throw new Error('素材数据加载失败')
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 === '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 || !scenesRes.ok || !platformsRes.ok || !productCategoriesRes.ok) {
throw new Error('分类数据加载失败')
}
setCategories(await categoriesRes.json())
setScenes(await scenesRes.json())
setPlatforms(await platformsRes.json())
setProductCategories(await productCategoriesRes.json())
} else if (activeTab === 'permissions') {
await fetchDepartments()
}
} catch (error) {
console.error('Error fetching data:', error)
showToast(error instanceof Error ? error.message : '数据加载失败', 'error')
} finally {
setLoading(false)
}
}, [activeTab, canManageSystemTabs, fetchDepartments, showRecycleBin, showToast])
// 权限检查
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])
// 加载状态
if (status === 'loading' || loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-xl text-[#86898e]">加载中...</div>
</div>
)
}
if (!session || !canAccessAdminPage) {
return null
}
return (
<div className="min-h-screen bg-[#fafafa]">
<MaterialsHeader session={session} sectionLabel="治理与设置" />
{/* Toast通知 */}
{toast && (
<div className={`fixed right-4 top-20 z-50 border border-[#e7e8ea] px-5 py-3 text-sm ${
toast.type === 'success'
? 'bg-[#f1fbf5] text-[#157a4b]'
: 'bg-[#fff5f4] text-[#a23c36]'
} animate-fade-in`}>
{toast.message}
</div>
)}
<main className="container mx-auto max-w-7xl px-4 py-10 sm:py-14">
<div className="mb-10 flex items-end justify-between gap-4 border-b border-[#e7e8ea] pb-5">
<div>
<p className="font-en text-[11px] uppercase tracking-[0.14em] text-[#86898e]">Governance</p>
<h1 className="mt-2 text-[clamp(28px,4vw,42px)] font-semibold tracking-[-0.03em] text-[#1a1c1d]">治理与设置</h1>
</div>
{canManageSystemTabs && (
<button
type="button"
onClick={() => router.push('/admin/settings')}
className="inline-flex min-h-10 shrink-0 items-center gap-2 rounded-lg bg-[#1a1c1d] px-4 text-[13px] font-semibold text-white transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#fafafa]"
>
<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="mb-8 flex gap-4 overflow-x-auto border-b border-[#e7e8ea] [&>button]:shrink-0">
<button
type="button"
onClick={() => setActiveTab('dataStats')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'dataStats'
? 'text-[#1a1c1d] border-b-2 border-[#1a1c1d]'
: 'text-[#6b7075] hover:text-[#1a1c1d]'
}`}
>
数据统计
</button>
{canManageSystemTabs && (
<>
<button
type="button"
onClick={() => setActiveTab('materials')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'materials'
? 'text-[#1a1c1d] border-b-2 border-[#1a1c1d]'
: 'text-[#6b7075] hover:text-[#1a1c1d]'
}`}
>
素材管理
</button>
<button
type="button"
onClick={() => setActiveTab('categories')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'categories'
? 'text-[#1a1c1d] border-b-2 border-[#1a1c1d]'
: 'text-[#6b7075] hover:text-[#1a1c1d]'
}`}
>
分类管理
</button>
{sessionUser?.isSuperAdmin && (
<>
<button
type="button"
onClick={() => setActiveTab('permissions')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'permissions'
? 'text-[#1a1c1d] border-b-2 border-[#1a1c1d]'
: 'text-[#6b7075] hover:text-[#1a1c1d]'
}`}
>
部门权限配置
</button>
<button
type="button"
onClick={() => setActiveTab('superAdmins')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'superAdmins'
? 'text-[#1a1c1d] border-b-2 border-[#1a1c1d]'
: 'text-[#6b7075] hover:text-[#1a1c1d]'
}`}
>
超级管理员
</button>
<button
type="button"
onClick={() => setActiveTab('publicTraffic')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'publicTraffic'
? 'text-[#1a1c1d] border-b-2 border-[#1a1c1d]'
: 'text-[#6b7075] hover:text-[#1a1c1d]'
}`}
>
公网流量
</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 === 'categories' && (
<CategoryManagement
categories={categories}
setCategories={setCategories}
scenes={scenes}
setScenes={setScenes}
platforms={platforms}
setPlatforms={setPlatforms}
productCategories={productCategories}
setProductCategories={setProductCategories}
showToast={showToast}
/>
)}
{/* 部门权限配置 Tab */}
{canManageSystemTabs && activeTab === 'permissions' && sessionUser?.isSuperAdmin && (
<DepartmentPermissionsTab
showToast={showToast}
departments={departments}
/>
)}
{/* 超级管理员 Tab */}
{canManageSystemTabs && activeTab === 'superAdmins' && sessionUser?.isSuperAdmin && (
<SuperAdminsTab
showToast={showToast}
/>
)}
{/* 公网流量 Tab */}
{canManageSystemTabs && activeTab === 'publicTraffic' && sessionUser?.isSuperAdmin && (
<PublicTrafficTab
showToast={showToast}
/>
)}
</main>
</div>
)
}