casualroom/apps/fenghuo/web/app/[locale]/dashboard/monitoring/page.tsx

520 lines
16 KiB
TypeScript
Raw Normal View History

2025-07-28 07:50:50 +08:00
'use client';
import { SiteHeader, PageInfo } from '@/components/site-header';
import { SidebarInset, SidebarProvider } from '@nice/ui/components/sidebar';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nice/ui/components/card';
import { Badge } from '@nice/ui/components/badge';
import { Button } from '@nice/ui/components/button';
import { Progress } from '@nice/ui/components/progress';
import {
IconServer,
IconUsers,
IconFiles,
IconCloudUpload,
IconTrendingUp,
IconTrendingDown,
IconActivity,
IconDatabase,
IconCpu,
IconShield,
IconBell,
IconCalendar,
IconClock,
IconAlertTriangle,
IconCircleCheck,
IconCircleX,
IconRefresh,
IconNetwork,
IconBrain,
IconFolder,
} from '@tabler/icons-react';
import { useTRPC } from '@fenghuo/client';
import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { SystemStatus } from '@fenghuo/common';
import { useSetPageInfo } from '@/components/providers/dashboard-provider';
// 用户和文件数据的模拟数据保留因为这部分数据不在systemStatus中
const additionalStats = {
users: {
total: 1248,
online: 89,
newToday: 12,
activeThisWeek: 324,
growth: '+5.2%',
peakOnline: 156,
},
files: {
total: 45280,
size: '2.4TB',
uploaded: 156,
shared: 89,
downloadedToday: 1289,
uploadedToday: 234,
growth: '+8.1%',
},
database: {
connections: 45,
maxConnections: 200,
queryTime: 2.3,
status: 'healthy',
size: '890MB',
backupStatus: 'completed',
lastBackup: '4小时前',
},
};
const recentActivities = [
{
id: 1,
user: '张三',
action: '上传了文件',
target: 'project.pdf',
time: '5分钟前',
type: 'upload',
ip: '192.168.1.100',
},
{
id: 2,
user: '李四',
action: '创建了文章',
target: 'API设计指南',
time: '10分钟前',
type: 'create',
ip: '192.168.1.101',
},
];
const systemAlerts = [
{ id: 1, type: 'warning', level: 'medium', message: '磁盘空间使用率超过80%', time: '1小时前', resolved: false },
{ id: 2, type: 'info', level: 'low', message: '系统备份已完成', time: '2小时前', resolved: true },
{ id: 3, type: 'success', level: 'low', message: '安全扫描未发现威胁', time: '3小时前', resolved: true },
{ id: 4, type: 'error', level: 'high', message: 'Email服务响应异常', time: '4小时前', resolved: false },
];
function StatsCard({
title,
value,
description,
icon: Icon,
trend,
trendValue,
className = '',
status,
}: {
title: string;
value: string | number;
description: string;
icon: any;
trend?: 'up' | 'down';
trendValue?: string;
className?: string;
status?: 'good' | 'warning' | 'error';
}) {
const getStatusColor = () => {
switch (status) {
case 'good':
return 'text-green-500';
case 'warning':
return 'text-yellow-500';
case 'error':
return 'text-red-500';
default:
return 'text-muted-foreground';
}
};
return (
<Card className={className}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className={`h-4 w-4 ${getStatusColor()}`} />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
<span>{description}</span>
{trend && trendValue && (
<div className="flex items-center">
{trend === 'up' ? (
<IconTrendingUp className="h-3 w-3 text-green-500" />
) : (
<IconTrendingDown className="h-3 w-3 text-red-500" />
)}
<span className={trend === 'up' ? 'text-green-500' : 'text-red-500'}>{trendValue}</span>
</div>
)}
</div>
</CardContent>
</Card>
);
}
function SystemStatusCard({ systemStatus, systemStatusLoading, refetchSystemStatus }: { systemStatus?: SystemStatus, systemStatusLoading: boolean, refetchSystemStatus: () => void }) {
// 如果没有数据,显示加载状态
if (systemStatusLoading || !systemStatus) {
return (
<Card className="col-span-2">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconServer className="h-5 w-5" />
<span></span>
</div>
<Badge variant="secondary">...</Badge>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="text-center text-muted-foreground py-8">
...
</div>
</CardContent>
</Card>
);
}
return (
<Card className="col-span-2">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconServer className="h-5 w-5" />
<span></span>
</div>
<div className="flex items-center space-x-2">
<Badge variant={systemStatus?.isRunning ? 'default' : 'destructive'}>
{systemStatus?.isRunning ? '运行中' : '离线'}
</Badge>
<Button variant="outline" size="sm" onClick={refetchSystemStatus}>
<IconRefresh className="h-4 w-4" />
</Button>
</div>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="flex justify-between items-center">
<span className="text-sm font-medium"></span>
<span className="text-sm text-muted-foreground">{systemStatus?.uptimeFormatted}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm font-medium"></span>
<span className="text-sm text-muted-foreground">{systemStatus?.processCount}</span>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconCpu className="h-4 w-4" />
<span className="text-sm">CPU使用率</span>
</div>
<span className="text-sm font-medium">{systemStatus?.cpuUsage.toFixed(1)}%</span>
</div>
<Progress value={systemStatus?.cpuUsage} className="h-2" />
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconBrain className="h-4 w-4" />
<span className="text-sm">使</span>
</div>
<span className="text-sm font-medium">{systemStatus?.memory.usagePercent.toFixed(1)}%</span>
</div>
<Progress value={systemStatus?.memory.usagePercent} className="h-2" />
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconFolder className="h-4 w-4" />
<span className="text-sm">使</span>
</div>
<span className="text-sm font-medium">{systemStatus?.disk.usagePercent.toFixed(1)}%</span>
</div>
<Progress value={systemStatus?.disk.usagePercent} className="h-2" />
</div>
<div className="grid grid-cols-3 gap-4 pt-2">
<div className="text-center">
<div className="text-lg font-bold text-primary">{systemStatus?.loadAverage.oneMinute}</div>
<div className="text-xs text-muted-foreground">1</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-primary">{systemStatus?.loadAverage.fiveMinutes}</div>
<div className="text-xs text-muted-foreground">5</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-primary">{systemStatus?.loadAverage.fifteenMinutes}</div>
<div className="text-xs text-muted-foreground">15</div>
</div>
</div>
</CardContent>
</Card>
);
}
function UserActivityCard() {
const getActivityIcon = (type: string) => {
switch (type) {
case 'upload':
return <IconCloudUpload className="h-4 w-4 text-blue-500" />;
case 'create':
return <IconFiles className="h-4 w-4 text-green-500" />;
case 'share':
return <IconUsers className="h-4 w-4 text-purple-500" />;
case 'permission':
return <IconShield className="h-4 w-4 text-orange-500" />;
case 'login':
return <IconUsers className="h-4 w-4 text-teal-500" />;
case 'download':
return <IconDatabase className="h-4 w-4 text-indigo-500" />;
default:
return <IconActivity className="h-4 w-4" />;
}
};
return (
<Card className="col-span-2">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<IconActivity className="h-5 w-5" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-center space-x-3">
<div className="flex-shrink-0">{getActivityIcon(activity.type)}</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">
<span className="text-blue-600">{activity.user}</span> {activity.action}{' '}
<span className="font-semibold">{activity.target}</span>
</p>
<div className="flex items-center space-x-4 text-xs text-muted-foreground">
<div className="flex items-center space-x-1">
<IconClock className="h-3 w-3" />
<span>{activity.time}</span>
</div>
<div className="flex items-center space-x-1">
<IconNetwork className="h-3 w-3" />
<span>{activity.ip}</span>
</div>
</div>
</div>
</div>
))}
</div>
<div className="mt-4 pt-4 border-t">
<Button variant="outline" size="sm" className="w-full">
</Button>
</div>
</CardContent>
</Card>
);
}
function SystemAlertsCard() {
const getAlertIcon = (type: string, level: string) => {
switch (type) {
case 'warning':
return <IconAlertTriangle className="h-4 w-4 text-yellow-500" />;
case 'error':
return <IconCircleX className="h-4 w-4 text-red-500" />;
case 'success':
return <IconCircleCheck className="h-4 w-4 text-green-500" />;
case 'info':
return <IconBell className="h-4 w-4 text-blue-500" />;
default:
return <IconBell className="h-4 w-4 text-muted-foreground" />;
}
};
const getLevelBadge = (level: string) => {
switch (level) {
case 'high':
return <Badge variant="destructive"></Badge>;
case 'medium':
return <Badge variant="secondary"></Badge>;
case 'low':
return <Badge variant="outline"></Badge>;
default:
return <Badge variant="outline"></Badge>;
}
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconBell className="h-5 w-5" />
<span></span>
</div>
<Badge variant="destructive">{systemAlerts.filter((alert) => !alert.resolved).length}</Badge>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{systemAlerts.map((alert) => (
<div key={alert.id} className="flex items-start space-x-3">
<div className="flex-shrink-0 mt-1">{getAlertIcon(alert.type, alert.level)}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<p className={`text-sm ${!alert.resolved ? 'font-medium' : ''}`}>{alert.message}</p>
{getLevelBadge(alert.level)}
</div>
<div className="flex items-center space-x-2 mt-1">
<p className="text-xs text-muted-foreground">{alert.time}</p>
{alert.resolved && (
<Badge variant="outline" className="text-xs">
</Badge>
)}
</div>
</div>
</div>
))}
</div>
<div className="mt-4 pt-4 border-t">
<Button variant="outline" size="sm" className="w-full">
</Button>
</div>
</CardContent>
</Card>
);
}
function DatabaseStatusCard() {
const connectionPercent = Math.round((additionalStats.database.connections / additionalStats.database.maxConnections) * 100);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<IconDatabase className="h-5 w-5" />
<span></span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm font-medium"></span>
<Badge variant={additionalStats.database.status === 'healthy' ? 'default' : 'destructive'}>
{additionalStats.database.status === 'healthy' ? '健康' : '异常'}
</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<span className="text-sm font-medium">
{additionalStats.database.connections} / {additionalStats.database.maxConnections}
</span>
</div>
<Progress value={connectionPercent} className="h-2" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="text-center">
<div className="text-lg font-bold text-primary">{additionalStats.database.queryTime}ms</div>
<div className="text-xs text-muted-foreground"></div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-primary">{additionalStats.database.size}</div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<Badge variant={additionalStats.database.backupStatus === 'completed' ? 'default' : 'secondary'}>
{additionalStats.database.backupStatus === 'completed' ? '已完成' : '进行中'}
</Badge>
</div>
<div className="text-xs text-muted-foreground">: {additionalStats.database.lastBackup}</div>
</div>
</CardContent>
</Card>
);
}
export default function MonitoringPage() {
const trpc = useTRPC();
useSetPageInfo({
title: '系统监控',
subtitle: '实时监控系统运行状态和用户活动',
})
const { data: systemStatus, isLoading: systemStatusLoading, refetch: refetchSystemStatus } = useQuery(
trpc.system_monitor.getSystemStatus.queryOptions(),
);
return (
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-6 p-6">
{/* 统计卡片 */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<StatsCard
title="在线用户"
value={additionalStats.users.online}
description={`总用户 ${additionalStats.users.total.toLocaleString()}`}
icon={IconUsers}
trend="up"
trendValue={additionalStats.users.growth}
status="good"
/>
<StatsCard
title="文件总数"
value={additionalStats.files.total.toLocaleString()}
description={`总存储 ${additionalStats.files.size}`}
icon={IconFiles}
trend="up"
trendValue={additionalStats.files.growth}
status="good"
/>
<StatsCard
title="服务器负载"
value={systemStatus ? `${systemStatus.loadAverage.currentLoadPercent.toFixed(1)}%` : '--'}
description={systemStatus ? `内存 ${systemStatus.memory.usagePercent.toFixed(1)}%` : '加载中...'}
icon={IconServer}
status={
!systemStatus
? 'warning'
: systemStatus.cpuUsage > 80
? 'error'
: systemStatus.cpuUsage > 60
? 'warning'
: 'good'
}
/>
</div>
{/* 详细信息第一行 */}
<div className="grid gap-6 md:grid-cols-3">
<SystemStatusCard
systemStatus={systemStatus}
systemStatusLoading={systemStatusLoading}
refetchSystemStatus={refetchSystemStatus}
/>
<SystemAlertsCard />
</div>
{/* 详细信息第二行 */}
<div className="grid gap-6 md:grid-cols-2">
<UserActivityCard />
<DatabaseStatusCard />
</div>
</div>
</div>
);
}