Files
FunConnect/web/src/pages/Dashboard.tsx

220 lines
7.4 KiB
TypeScript
Raw Normal View History

import { useEffect, useState } from 'react';
import { Server, Users, Gamepad2, Activity, Wifi, WifiOff } from 'lucide-react';
import { apiService, wsClient, type ServerStats } from '../api';
export default function Dashboard() {
const [stats, setStats] = useState<ServerStats | null>(null);
const [health, setHealth] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
loadData();
const off = wsClient.on('status', (data: any) => {
setStats(prev => prev ? {
...prev,
node: { ...prev.node, rooms: data.rooms, players: data.players },
cluster: data.nodes || prev.cluster,
} : prev);
});
return () => off();
}, []);
async function loadData() {
try {
setLoading(true);
const [statsData, healthData] = await Promise.all([
apiService.getStats(),
apiService.getHealth(),
]);
setStats(statsData);
setHealth(healthData);
setError('');
} catch (err: any) {
setError('无法连接到服务器: ' + (err.message || '未知错误'));
} finally {
setLoading(false);
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-mc-green border-t-transparent rounded-full" />
</div>
);
}
if (error) {
return (
<div className="card border-red-500/30">
<div className="flex items-center gap-3 text-red-400">
<WifiOff className="w-6 h-6" />
<div>
<h3 className="font-bold"></h3>
<p className="text-sm mt-1">{error}</p>
</div>
</div>
<button onClick={loadData} className="btn-primary mt-4"></button>
</div>
);
}
const statCards = [
{
label: '在线房间',
value: stats?.node.rooms || 0,
max: stats?.node.maxRooms || 100,
icon: Gamepad2,
color: 'text-green-400',
bg: 'bg-green-400/10',
},
{
label: '在线玩家',
value: stats?.node.players || 0,
icon: Users,
color: 'text-blue-400',
bg: 'bg-blue-400/10',
},
{
label: '集群节点',
value: stats?.cluster?.onlineNodes || 1,
max: stats?.cluster?.totalNodes || 1,
icon: Server,
color: 'text-purple-400',
bg: 'bg-purple-400/10',
},
{
label: '服务状态',
value: health?.status === 'ok' ? '正常' : '异常',
icon: Activity,
color: health?.status === 'ok' ? 'text-green-400' : 'text-red-400',
bg: health?.status === 'ok' ? 'bg-green-400/10' : 'bg-red-400/10',
},
];
return (
<div>
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="text-2xl font-bold"></h2>
<p className="text-gray-400 text-sm mt-1">FunMC </p>
</div>
<div className="flex items-center gap-2 text-sm">
<Wifi className="w-4 h-4 text-green-400" />
<span className="text-gray-400">: {stats?.node.name || 'N/A'}</span>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{statCards.map((card, i) => {
const Icon = card.icon;
return (
<div key={i} className="card hover:border-mc-accent/60 transition-colors">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm">{card.label}</p>
<p className={`text-3xl font-bold mt-2 ${card.color}`}>
{card.value}
{card.max !== undefined && (
<span className="text-sm text-gray-500 font-normal">/{card.max}</span>
)}
</p>
</div>
<div className={`w-12 h-12 rounded-xl ${card.bg} flex items-center justify-center`}>
<Icon className={`w-6 h-6 ${card.color}`} />
</div>
</div>
</div>
);
})}
</div>
{/* Server Info */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="card">
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
<Server className="w-5 h-5 text-mc-green" />
</h3>
<div className="space-y-3">
<InfoRow label="节点ID" value={health?.nodeId || 'N/A'} />
<InfoRow label="节点名称" value={health?.nodeName || 'N/A'} />
<InfoRow label="运行模式" value={health?.isMaster ? '主节点' : '工作节点'} />
<InfoRow label="运行时间" value={formatUptime(health?.uptime || 0)} />
<InfoRow label="最大房间数" value={String(stats?.node.maxRooms || 0)} />
</div>
</div>
{stats?.cluster && (
<div className="card">
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
<Activity className="w-5 h-5 text-purple-400" />
</h3>
<div className="space-y-3">
<InfoRow label="总节点数" value={String(stats.cluster.totalNodes)} />
<InfoRow label="在线节点" value={String(stats.cluster.onlineNodes)} />
<InfoRow label="总房间数" value={String(stats.cluster.totalRooms)} />
<InfoRow label="总玩家数" value={String(stats.cluster.totalPlayers)} />
</div>
</div>
)}
</div>
{/* Quick Guide */}
<div className="card mt-6">
<h3 className="text-lg font-bold mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StepCard
step={1}
title="创建房间"
desc="在「创建房间」页面填写房间信息启动你的Minecraft服务器"
/>
<StepCard
step={2}
title="分享房间号"
desc="将生成的房间号发给你的好友,他们可以通过房间号连接"
/>
<StepCard
step={3}
title="开始游戏"
desc="好友使用FunMC客户端连接到中继服务器即可一起畅玩"
/>
</div>
</div>
</div>
);
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-center justify-between py-2 border-b border-mc-accent/10">
<span className="text-gray-400 text-sm">{label}</span>
<span className="text-white font-medium text-sm">{value}</span>
</div>
);
}
function StepCard({ step, title, desc }: { step: number; title: string; desc: string }) {
return (
<div className="bg-mc-dark/50 rounded-lg p-4 border border-mc-accent/10">
<div className="w-8 h-8 rounded-full bg-mc-green/20 text-mc-green font-bold flex items-center justify-center text-sm mb-3">
{step}
</div>
<h4 className="font-bold text-sm mb-1">{title}</h4>
<p className="text-gray-400 text-xs">{desc}</p>
</div>
);
}
function formatUptime(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}${m}${s}`;
if (m > 0) return `${m}${s}`;
return `${s}`;
}