视觉统一(Zen Minimal × Future Organic 白皮书语言,全站浅色单主题): - 登录页:品牌→由壹点·品牌中心,白纸底+发丝线+柔光光晕,黑按钮 - 作品过稿:深灰头→白皮书,返回链接→返回创意资产库 - 绩效考核:桌面+移动双头部换白皮书,标题改绩效考核,返回改链接 - 组织架构:BoardShell+页面 76 处调色板全量替换(#eef1f4→#fafafa 等) - admin 后台:MaterialsHeader(面包屑治理与设置),Tab 栏墨色高亮 - 深色主题废弃:globals.css 移除媒体查询,tailwind darkMode:'class' 不挂 .dark - 设计文档同步更新:AGENTS.md UI 基线/DESIGN.md/PRODUCT.md/README.md/design-qa.md 个人档案(原个人中心): - API 合并权威人员档案(getEmployeeByUserId → 花名/工号/部门/职位/M-P等级/主管/状态) - 页面白皮书化:权威档案卡(只读 HR 字段)+ 账号区(头像上传)+ 素材/收藏 顶栏平台化: - MaterialsHeader 双模式:首页(五板块导航+全套工具+菜单)/板块(面包屑) - 菜单重排:我的档案/绩效考核/作品过稿看板‖更新记录/提交反馈‖治理与设置‖退出 - 菜单头身份行:花名+职位·部门(权威档案) - 首页排序:01 创意资产库(全员)→ 02 组织管理(管理员)→ 03-05 规划中 - 素材库改名创意资产库,全站同步(索引/导航/卡片/按钮/页脚) - 排行榜换白皮书(白底发丝线+琥珀前三名) 构建修复: - tsconfig 排除 output/(本地审计临时文件不再卡类型检查) - 存储边界门禁豁免 output/(与 .dockerignore 对齐)
471 lines
16 KiB
TypeScript
471 lines
16 KiB
TypeScript
'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 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-2xl font-semibold text-[#1a1c1d]">治理与设置</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 gap-4 overflow-x-auto border-b border-[#e7e8ea] mb-8 [&>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>
|
||
)
|
||
}
|