- server: Node.js TCP中继服务器,支持多节点集群 - web: React管理面板(仪表盘、房间管理、节点管理) - client: Electron桌面客户端(连接、创建/加入房间、本地代理) - deploy: Ubuntu一键部署脚本
220 lines
7.4 KiB
TypeScript
220 lines
7.4 KiB
TypeScript
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}秒`;
|
||
}
|