gongxiangp2/app/admin/page.tsx
chenpei 6c6c282203 feat: 全站视觉统一白皮书语言 + 个人档案改造 + 顶栏平台化
视觉统一(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 对齐)
2026-08-15 14:16:20 +08:00

471 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 { 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>
)
}