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

520 lines
16 KiB
TypeScript
Executable File
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 { 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>
);
}