From b359ce2dfe3349114b4eea3f6a0614426a9788fb Mon Sep 17 00:00:00 2001 From: FunMC Date: Mon, 23 Feb 2026 07:53:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Web=E9=9D=A2=E6=9D=BF=E8=BF=AD=E4=BB=A3?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rooms: 添加搜索/筛选(按名称、房间号、房主、版本类型) - Rooms: 10秒自动刷新房间列表 - Rooms: 删除房间使用确认弹窗替代confirm() - Rooms: 复制房间号显示已复制反馈 - Dashboard: 添加流量统计展示(入站/出站流量) - Dashboard: 15秒自动刷新数据 - API: 添加getTraffic、getRoom、kickPlayer方法 --- server/web/src/api.ts | 22 +++++++ server/web/src/pages/Dashboard.tsx | 42 ++++++++++++- server/web/src/pages/Rooms.tsx | 97 +++++++++++++++++++++++++----- 3 files changed, 143 insertions(+), 18 deletions(-) diff --git a/server/web/src/api.ts b/server/web/src/api.ts index 1afa9f8..7e8627d 100644 --- a/server/web/src/api.ts +++ b/server/web/src/api.ts @@ -120,6 +120,28 @@ export const apiService = { const res = await api.get('/cluster/rooms'); return res.data; }, + + async getTraffic(): Promise<{ + totalBytesIn: number; + totalBytesOut: number; + rooms: number; + players: number; + formattedIn: string; + formattedOut: string; + }> { + const res = await api.get('/traffic'); + return res.data; + }, + + async getRoom(roomId: string): Promise<{ room: RoomInfo; players: any[] }> { + const res = await api.get(`/rooms/${roomId}`); + return res.data; + }, + + async kickPlayer(roomId: string, playerId: string, reason?: string) { + const res = await api.post(`/rooms/${roomId}/kick/${playerId}`, { reason }); + return res.data; + }, }; export class WebSocketClient { diff --git a/server/web/src/pages/Dashboard.tsx b/server/web/src/pages/Dashboard.tsx index a7c937b..0ef80cb 100644 --- a/server/web/src/pages/Dashboard.tsx +++ b/server/web/src/pages/Dashboard.tsx @@ -1,10 +1,11 @@ import { useEffect, useState } from 'react'; -import { Server, Users, Gamepad2, Activity, Wifi, WifiOff } from 'lucide-react'; +import { Server, Users, Gamepad2, Activity, Wifi, WifiOff, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'; import { apiService, wsClient, type ServerStats } from '../api'; export default function Dashboard() { const [stats, setStats] = useState(null); const [health, setHealth] = useState(null); + const [traffic, setTraffic] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -17,18 +18,21 @@ export default function Dashboard() { cluster: data.nodes || prev.cluster, } : prev); }); - return () => off(); + const timer = setInterval(loadData, 15000); + return () => { off(); clearInterval(timer); }; }, []); async function loadData() { try { setLoading(true); - const [statsData, healthData] = await Promise.all([ + const [statsData, healthData, trafficData] = await Promise.all([ apiService.getStats(), apiService.getHealth(), + apiService.getTraffic().catch(() => null), ]); setStats(statsData); setHealth(healthData); + setTraffic(trafficData); setError(''); } catch (err: any) { setError('无法连接到服务器: ' + (err.message || '未知错误')); @@ -163,6 +167,38 @@ export default function Dashboard() { )} + {/* Traffic Stats */} + {traffic && ( +
+

+ + 流量统计 +

+
+
+ +

入站流量

+

{traffic.formattedIn}

+
+
+ +

出站流量

+

{traffic.formattedOut}

+
+
+ +

活跃房间

+

{traffic.rooms}

+
+
+ +

在线玩家

+

{traffic.players}

+
+
+
+ )} + {/* Quick Guide */}

快速入门

diff --git a/server/web/src/pages/Rooms.tsx b/server/web/src/pages/Rooms.tsx index 3eeb352..d1f0fd6 100644 --- a/server/web/src/pages/Rooms.tsx +++ b/server/web/src/pages/Rooms.tsx @@ -1,18 +1,18 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { Link } from 'react-router-dom'; -import { Gamepad2, Users, Clock, Lock, Trash2, Copy, Plus, RefreshCw } from 'lucide-react'; +import { Gamepad2, Users, Clock, Lock, Trash2, Copy, Plus, RefreshCw, Search, X } from 'lucide-react'; import { apiService, type RoomInfo } from '../api'; export default function Rooms() { const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const [search, setSearch] = useState(''); + const [editionFilter, setEditionFilter] = useState('all'); + const [deleteConfirm, setDeleteConfirm] = useState(null); + const [copied, setCopied] = useState(null); - useEffect(() => { - loadRooms(); - }, []); - - async function loadRooms() { + const loadRooms = useCallback(async () => { try { setLoading(true); const data = await apiService.getRooms(); @@ -23,13 +23,28 @@ export default function Rooms() { } finally { setLoading(false); } - } + }, []); + + useEffect(() => { + loadRooms(); + const timer = setInterval(loadRooms, 10000); + return () => clearInterval(timer); + }, [loadRooms]); + + const filteredRooms = rooms.filter(room => { + const matchSearch = !search || + room.name.toLowerCase().includes(search.toLowerCase()) || + room.id.toLowerCase().includes(search.toLowerCase()) || + room.hostName.toLowerCase().includes(search.toLowerCase()); + const matchEdition = editionFilter === 'all' || room.gameEdition === editionFilter; + return matchSearch && matchEdition; + }); async function handleDelete(roomId: string) { - if (!confirm('确定要删除这个房间吗?')) return; try { await apiService.deleteRoom(roomId); setRooms(rooms.filter(r => r.id !== roomId)); + setDeleteConfirm(null); } catch { alert('删除失败'); } @@ -37,14 +52,30 @@ export default function Rooms() { function copyRoomId(roomId: string) { navigator.clipboard.writeText(roomId); + setCopied(roomId); + setTimeout(() => setCopied(null), 2000); } return (
-
+ {/* Delete Confirmation Dialog */} + {deleteConfirm && ( +
+
+

确认删除

+

确定要删除房间 {deleteConfirm} 吗?此操作不可撤销。

+
+ + +
+
+
+ )} + +

房间列表

-

当前活跃的联机房间

+

当前活跃的联机房间 · 每10秒自动刷新

+ {/* Search & Filters */} +
+
+ + setSearch(e.target.value)} + placeholder="搜索房间名、房间号、房主..." + className="w-full bg-mc-dark border border-mc-accent/30 rounded-lg pl-10 pr-8 py-2 text-sm text-gray-200 focus:border-mc-green outline-none transition-colors" + /> + {search && ( + + )} +
+ +
+ {loading && (
@@ -70,7 +129,7 @@ export default function Rooms() {
)} - {!loading && rooms.length === 0 && ( + {!loading && rooms.length === 0 && !search && editionFilter === 'all' && (

暂无房间

@@ -82,8 +141,16 @@ export default function Rooms() {
)} + {!loading && rooms.length > 0 && filteredRooms.length === 0 && ( +
+ +

无匹配结果

+

试试其他搜索词或筛选条件

+
+ )} +
- {rooms.map(room => ( + {filteredRooms.map(room => (
@@ -102,13 +169,13 @@ export default function Rooms() { className="ml-2 text-mc-green hover:text-green-300 transition-colors" title="复制房间号" > - + {copied === room.id ? 已复制 : }