feat: FunConnect v1.0.0 - Minecraft联机平台完整版
- server: Node.js TCP中继服务器,支持多节点集群 - web: React管理面板(仪表盘、房间管理、节点管理) - client: Electron桌面客户端(连接、创建/加入房间、本地代理) - deploy: Ubuntu一键部署脚本
This commit is contained in:
219
web/src/pages/Dashboard.tsx
Normal file
219
web/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
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}秒`;
|
||||
}
|
||||
Reference in New Issue
Block a user