WuYongJie f646b71813
Some checks failed
CI Check / check (push) Has been cancelled
chore: 初始化本地仓库并同步最近改动
2025-11-18 17:50:16 +08:00

440 lines
17 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'
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-gray-50 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-gray-600">加载系统监控数据...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="min-h-screen bg-gray-50 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-gray-50 flex items-center justify-center">
<p className="text-gray-600">暂无数据</p>
</div>
)
}
const { overview, trends, distribution, systemInfo, recentActivity, status: systemStatus } = dashboardData
return (
<div className="min-h-screen bg-gray-50">
{/* 头部 */}
<div className="bg-white shadow-sm border-b">
<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-gray-900">📊 系统监控仪表板</h1>
<p className="mt-1 text-sm text-gray-500">
{systemInfo.siteName} - 实时系统状态监控
</p>
</div>
<div className="flex items-center space-x-4">
<button
onClick={handleRefresh}
disabled={loading}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300 text-white font-medium py-2 px-4 rounded-md"
>
{loading ? '刷新中...' : '🔄 刷新'}
</button>
<div className="text-sm text-gray-500">
最后更新:{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">
{/* 系统概览卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<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-gray-500 truncate">总用户数</dt>
<dd className="text-lg font-medium text-gray-900">{overview.totalUsers}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<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-gray-500 truncate">活跃用户</dt>
<dd className="text-lg font-medium text-gray-900">{overview.activeUsers}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
<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-gray-500 truncate">总素材数</dt>
<dd className="text-lg font-medium text-gray-900">{overview.totalMaterials}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center">
<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-gray-500 truncate">本月新用户</dt>
<dd className="text-lg font-medium text-gray-900">{overview.newUsersThisMonth}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-indigo-500 rounded-full flex items-center justify-center">
<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-gray-500 truncate">本月新素材</dt>
<dd className="text-lg font-medium text-gray-900">{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="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 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-gray-100">
<span className="text-sm text-gray-600">{formatDate(item.date)}</span>
<div className="flex items-center">
<div className="w-32 bg-gray-200 rounded-full h-2 mr-3">
<div
className="bg-blue-600 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-gray-900">{item.count}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* 素材上传趋势 */}
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 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-gray-100">
<span className="text-sm text-gray-600">{formatDate(item.date)}</span>
<div className="flex items-center">
<div className="w-32 bg-gray-200 rounded-full h-2 mr-3">
<div
className="bg-purple-600 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-gray-900">{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="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 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-gray-600">{item.department || '未设置'}</span>
<span className="text-sm font-medium text-gray-900">{item.count}</span>
</div>
))}
</div>
</div>
{/* 权限分布 */}
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 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-gray-600">
{item.role === 'ADMIN' && item.isSuperAdmin ? '超级管理员' :
item.role === 'ADMIN' ? '管理员' : '普通用户'}
</span>
<span className="text-sm font-medium text-gray-900">{item.count}</span>
</div>
))}
</div>
</div>
{/* 系统信息 */}
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">⚙️ 系统信息</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">注册功能</span>
<span className={`text-sm font-medium ${systemInfo.registrationEnabled ? 'text-green-600' : 'text-red-600'}`}>
{systemInfo.registrationEnabled ? '开启' : '关闭'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">用户激活</span>
<span className={`text-sm font-medium ${systemInfo.userActivationRequired ? 'text-yellow-600' : 'text-green-600'}`}>
{systemInfo.userActivationRequired ? '需要' : '关闭'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">数据库状态</span>
<span className={`text-sm font-medium ${systemStatus.database === 'healthy' ? 'text-green-600' : 'text-red-600'}`}>
{systemStatus.database === 'healthy' ? '正常' : '异常'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">运行时间</span>
<span className="text-sm font-medium text-gray-900">
{Math.floor(systemStatus.uptime / 3600)}小时
</span>
</div>
</div>
</div>
</div>
{/* 最近活动 */}
<div className="bg-white shadow rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">🕒 最近活动</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
素材标题
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
上传者
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
上传时间
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{recentActivity.map((activity) => (
<tr key={activity.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{activity.title}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{activity.uploadedBy.username}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatTimestamp(activity.createdAt)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}