Files
FunConnect/web/src/pages/Dashboard.tsx
FunMC b17679cec6 feat: FunConnect v1.0.0 - Minecraft联机平台完整版
- server: Node.js TCP中继服务器,支持多节点集群
- web: React管理面板(仪表盘、房间管理、节点管理)
- client: Electron桌面客户端(连接、创建/加入房间、本地代理)
- deploy: Ubuntu一键部署脚本
2026-02-22 23:33:00 +08:00

220 lines
7.4 KiB
TypeScript
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.
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}`;
}