WuYongJie e4dcb6260a
Some checks failed
CI Check / check (push) Has been cancelled
feat: Admin页面重构 & UI三层架构系统
- 创建统一UI三层架构系统(基础层、颜色层、组合层)
- 拆分Admin页面为独立Tab组件(MonitorTab、UsersTab、DepartmentsTab等)
- 提取公共逻辑到Hooks(useToast、useDataFetch、useUserOperations等)
- 提取工具函数(validators、formatters、api等)
- 创建统一弹窗组件(Modal)和useModal Hook
- 添加错误边界和统一状态组件
- 完善类型定义和可访问性支持
- 更新文档(优化记录、优化计划、更新日志)
2025-11-22 15:17:10 +08:00

465 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 React, { useState, useEffect } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { cardStyles, buttonStyles, spacingStyles, cn } from '@/lib/ui-styles'
interface DashboardData {
overview: {
totalUsers: number
activeUsers: number
totalMaterials: number
newUsersThisMonth: number
newMaterialsThisMonth: number
}
trends: {
userGrowth: Array<{ date: string; count: number }>
materialUploads: Array<{ date: string; count: number }>
}
distribution: {
departments: Array<{ department: string; count: number }>
roles: Array<{ role: string; isSuperAdmin: boolean; count: number }>
}
systemInfo: {
registrationEnabled: boolean
userActivationRequired: boolean
siteName: string
}
recentActivity: Array<{
id: string
title: string
createdAt: string
uploadedBy: {
username: string
email: string
}
}>
status: {
database: string
timestamp: string
uptime: number
}
}
export default function SystemMonitor() {
const { data: session, status } = useSession()
const router = useRouter()
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null)
// 权限检查
useEffect(() => {
if (status === 'loading') return
const isSuperAdmin = (session?.user as any)?.isSuperAdmin
if (status === 'authenticated' && (session.user as any)?.role !== 'ADMIN' && !isSuperAdmin) {
router.push('/')
return
}
}, [session, status, router])
// 获取仪表板数据
const fetchDashboardData = async () => {
try {
setError(null)
const response = await fetch('/api/admin/system-status', {
credentials: 'include'
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
setDashboardData(data)
} catch (err) {
console.error('Failed to fetch dashboard data:', err)
setError(err instanceof Error ? err.message : '获取数据失败')
} finally {
setLoading(false)
}
}
// 初始化数据加载
useEffect(() => {
if (session) {
fetchDashboardData()
}
}, [session])
// 自动刷新(每30秒)
useEffect(() => {
if (session) {
const interval = setInterval(fetchDashboardData, 30000)
setRefreshInterval(interval)
return () => {
if (interval) clearInterval(interval)
}
}
}, [session])
// 手动刷新
const handleRefresh = () => {
setLoading(true)
fetchDashboardData()
}
// 格式化时间
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric'
})
}
// 格式化时间戳
const formatTimestamp = (timestamp: string) => {
return new Date(timestamp).toLocaleString('zh-CN')
}
// 计算增长百分比
const calculateGrowth = (current: number, previous: number) => {
if (previous === 0) return current > 0 ? 100 : 0
return Math.round(((current - previous) / previous) * 100)
}
if (status === 'loading' || loading) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-slate-600">加载系统监控数据...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center">
<div className="text-center">
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<strong className="font-bold">错误:</strong>
<span className="block sm:inline">{error}</span>
</div>
<button
onClick={handleRefresh}
className="mt-4 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
重试
</button>
</div>
</div>
)
}
if (!dashboardData) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center">
<p className="text-slate-600 dark:text-slate-300">暂无数据</p>
</div>
)
}
const { overview, trends, distribution, systemInfo, recentActivity, status: systemStatus } = dashboardData
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950">
{/* 头部 */}
<div className="bg-white/90 dark:bg-slate-900/90 backdrop-blur-sm shadow-sm border-b border-slate-200/70 dark:border-slate-800/70">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-6">
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white">📊 系统监控仪表板</h1>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-300">
{systemInfo.siteName} - 实时系统状态监控
</p>
</div>
<div className="flex items-center space-x-4">
<button
onClick={handleRefresh}
disabled={loading}
className={cn(
buttonStyles.primary,
'py-2 px-4 rounded-xl disabled:opacity-60 disabled:cursor-not-allowed'
)}
>
{loading ? '刷新中...' : '🔄 刷新'}
</button>
<div className="text-sm text-slate-500 dark:text-slate-400">
最后更新:{formatTimestamp(systemStatus.timestamp)}
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* 系统概览卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
<div className={cn(cardStyles.base, 'overflow-hidden')}>
<div className="p-5 space-y-2">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-500/90 dark:bg-blue-500 rounded-full flex items-center justify-center shadow-sm shadow-blue-500/40">
<span className="text-white font-medium">👥</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-slate-500 dark:text-slate-400 truncate">总用户数</dt>
<dd className="text-lg font-semibold text-slate-900 dark:text-white">{overview.totalUsers}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className={cn(cardStyles.base, 'overflow-hidden')}>
<div className="p-5 space-y-2">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-green-500/90 dark:bg-green-500 rounded-full flex items-center justify-center shadow-sm shadow-green-500/40">
<span className="text-white font-medium">⚡</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-slate-500 dark:text-slate-400 truncate">活跃用户</dt>
<dd className="text-lg font-semibold text-slate-900 dark:text-white">{overview.activeUsers}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className={cn(cardStyles.base, 'overflow-hidden')}>
<div className="p-5 space-y-2">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-purple-500/90 dark:bg-purple-500 rounded-full flex items-center justify-center shadow-sm shadow-purple-500/40">
<span className="text-white font-medium">📦</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-slate-500 dark:text-slate-400 truncate">总素材数</dt>
<dd className="text-lg font-semibold text-slate-900 dark:text-white">{overview.totalMaterials}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className={cn(cardStyles.base, 'overflow-hidden')}>
<div className="p-5 space-y-2">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-yellow-500/90 dark:bg-yellow-400 rounded-full flex items-center justify-center shadow-sm shadow-yellow-500/40">
<span className="text-white font-medium">🆕</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-slate-500 dark:text-slate-400 truncate">本月新用户</dt>
<dd className="text-lg font-semibold text-slate-900 dark:text-white">{overview.newUsersThisMonth}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className={cn(cardStyles.base, 'overflow-hidden')}>
<div className="p-5 space-y-2">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-indigo-500/90 dark:bg-indigo-500 rounded-full flex items-center justify-center shadow-sm shadow-indigo-500/40">
<span className="text-white font-medium">📈</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-slate-500 dark:text-slate-400 truncate">本月新素材</dt>
<dd className="text-lg font-semibold text-slate-900 dark:text-white">{overview.newMaterialsThisMonth}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* 用户增长趋势 */}
<div className={cn(cardStyles.base, 'p-6')}>
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-4">📈 用户增长趋势(最近30天)</h3>
<div className="h-64 overflow-y-auto">
<div className="space-y-2">
{trends.userGrowth.map((item, index) => (
<div key={index} className="flex justify-between items-center py-2 border-b border-slate-100 dark:border-slate-800/80">
<span className="text-sm text-slate-600 dark:text-slate-300">{formatDate(item.date)}</span>
<div className="flex items-center">
<div className="w-32 bg-slate-200 dark:bg-slate-700 rounded-full h-2 mr-3">
<div
className="bg-blue-600 dark:bg-blue-500 h-2 rounded-full"
style={{ width: `${Math.min(100, (item.count / Math.max(...trends.userGrowth.map(d => d.count))) * 100)}%` }}
></div>
</div>
<span className="text-sm font-medium text-slate-900 dark:text-white">{item.count}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* 素材上传趋势 */}
<div className={cn(cardStyles.base, 'p-6')}>
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-4">📊 素材上传趋势(最近30天)</h3>
<div className="h-64 overflow-y-auto">
<div className="space-y-2">
{trends.materialUploads.map((item, index) => (
<div key={index} className="flex justify-between items-center py-2 border-b border-slate-100 dark:border-slate-800/80">
<span className="text-sm text-slate-600 dark:text-slate-300">{formatDate(item.date)}</span>
<div className="flex items-center">
<div className="w-32 bg-slate-200 dark:bg-slate-700 rounded-full h-2 mr-3">
<div
className="bg-purple-600 dark:bg-purple-500 h-2 rounded-full"
style={{ width: `${Math.min(100, (item.count / Math.max(...trends.materialUploads.map(d => d.count))) * 100)}%` }}
></div>
</div>
<span className="text-sm font-medium text-slate-900 dark:text-white">{item.count}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
{/* 部门分布 */}
<div className={cn(cardStyles.base, 'p-6')}>
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-4">🏢 部门分布</h3>
<div className="space-y-3">
{distribution.departments.map((item, index) => (
<div key={index} className="flex justify-between items-center">
<span className="text-sm text-slate-600 dark:text-slate-300">{item.department || '未设置'}</span>
<span className="text-sm font-medium text-slate-900 dark:text-white">{item.count}</span>
</div>
))}
</div>
</div>
{/* 权限分布 */}
<div className={cn(cardStyles.base, 'p-6')}>
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-4">🛡️ 权限分布</h3>
<div className="space-y-3">
{distribution.roles.map((item, index) => (
<div key={index} className="flex justify-between items-center">
<span className="text-sm text-slate-600 dark:text-slate-300">
{item.role === 'ADMIN' && item.isSuperAdmin ? '超级管理员' :
item.role === 'ADMIN' ? '管理员' : '普通用户'}
</span>
<span className="text-sm font-medium text-slate-900 dark:text-white">{item.count}</span>
</div>
))}
</div>
</div>
{/* 系统信息 - 只读状态展示 */}
<div className={cn(cardStyles.base, 'p-6')}>
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-4">⚙️ 系统信息</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-slate-600 dark:text-slate-300">注册功能</span>
<span
className={cn(
'inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium',
systemInfo.registrationEnabled
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
)}
>
{systemInfo.registrationEnabled ? '开启' : '关闭'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-slate-600 dark:text-slate-300">用户激活</span>
<span
className={cn(
'inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium',
systemInfo.userActivationRequired
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
)}
>
{systemInfo.userActivationRequired ? '需要' : '关闭'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-slate-600 dark:text-slate-300">数据库状态</span>
<span
className={cn(
'inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium',
systemStatus.database === 'healthy'
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
)}
>
{systemStatus.database === 'healthy' ? '正常' : '异常'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-slate-600 dark:text-slate-300">运行时间</span>
<span className="text-sm font-medium text-slate-900 dark:text-white">
{Math.floor(systemStatus.uptime / 3600)}小时
</span>
</div>
</div>
</div>
</div>
{/* 最近活动 */}
<div className={cn(cardStyles.base, 'p-6')}>
<h3 className="text-lg font-medium text-slate-900 dark:text-white mb-4">🕒 最近活动</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200 dark:divide-slate-700/80">
<thead className="bg-slate-50 dark:bg-slate-900/60">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
素材标题
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
上传者
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
上传时间
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-slate-900/40 divide-y divide-slate-200 dark:divide-slate-800/80">
{recentActivity.map((activity) => (
<tr key={activity.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/60 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-white">
{activity.title}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-300">
{activity.uploadedBy.username}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{formatTimestamp(activity.createdAt)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}