From 9d1378793e854522cd0c5674c0d0051b062632a1 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sat, 27 Jun 2026 20:48:39 +0800 Subject: [PATCH] refactor(panel): dashboard --- panel/src/components/VoxelFleet.tsx | 9 +- panel/src/i18n/resources/en-US/dashboard.json | 33 +- panel/src/i18n/resources/zh-CN/dashboard.json | 33 +- panel/src/pages/Dashboard.tsx | 470 ++++++++++++++++-- 4 files changed, 498 insertions(+), 47 deletions(-) diff --git a/panel/src/components/VoxelFleet.tsx b/panel/src/components/VoxelFleet.tsx index beec70f..b1b6eed 100644 --- a/panel/src/components/VoxelFleet.tsx +++ b/panel/src/components/VoxelFleet.tsx @@ -34,8 +34,13 @@ export function VoxelFleet({ servers }: Props) { const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - mount.appendChild(renderer.domElement); + renderer.domElement.style.position = "absolute"; + renderer.domElement.style.top = "0"; + renderer.domElement.style.left = "0"; + renderer.domElement.style.width = "100%"; + renderer.domElement.style.height = "100%"; renderer.domElement.style.display = "block"; + mount.appendChild(renderer.domElement); scene.add(new THREE.AmbientLight(0xffffff, 0.55)); const key = new THREE.DirectionalLight(0xffffff, 1.1); @@ -157,7 +162,7 @@ export function VoxelFleet({ servers }: Props) { }; }, []); - return
; + return
; } /** sig is a cheap fingerprint of the fleet's renderable shape (order-independent diff --git a/panel/src/i18n/resources/en-US/dashboard.json b/panel/src/i18n/resources/en-US/dashboard.json index d7dd7fd..08db249 100644 --- a/panel/src/i18n/resources/en-US/dashboard.json +++ b/panel/src/i18n/resources/en-US/dashboard.json @@ -10,5 +10,36 @@ "fleet": "Fleet", "manage": "Manage", "loading_scene": "Loading scene…", - "preparing_scene": "Preparing scene…" + "preparing_scene": "Preparing scene…", + "fleet_load": "Fleet Load & Health", + "active_load": "Player Load Rate", + "status_distribution": "Node Status Distribution", + "account_status": "Account Status", + "account_linked_title": "Account Linked", + "account_linked_desc": "Your identity is bound to a Minecraft UUID. You have full server ownership and wake permissions.", + "account_unlinked_title": "Account Unlinked", + "account_unlinked_desc": "You must link a Minecraft character to claim or operate servers. Go to Account to bind now.", + "go_link": "Bind Now", + "system_specs": "Specs & Metrics", + "spec_images_total": "Whitelisted Images", + "spec_images_built": "Built Locally", + "spec_images_external": "External Whitelisted", + "spec_servers_total": "Total Servers", + "spec_servers_owned": "Owned Servers", + "spec_servers_claimable": "Claimable Servers", + "spec_running_ratio": "Running Rate", + "spec_stopped_ratio": "Sleeping Rate", + "spec_failed_ratio": "Failed Rate", + "no_servers_monitor": "No active instances in the cluster", + "images_count": "Whitelisted backend images available", + "legend_title": "3D Node Monitor Legend", + "legend_running": "Running", + "legend_starting": "Starting", + "legend_stopping": "Stopping", + "legend_stopped": "Sleeping", + "legend_failed": "Failed", + "legend_unknown": "Unknown", + "welcome_title": "Welcome to Felis Lolicon", + "welcome_desc": "You have logged into the User Dashboard. Go to 'My Servers' to view and claim nodes allocated to you, or manage their sleep, wake, and console logs.", + "go_to_servers_btn": "Manage My Servers" } diff --git a/panel/src/i18n/resources/zh-CN/dashboard.json b/panel/src/i18n/resources/zh-CN/dashboard.json index b119375..37e7c93 100644 --- a/panel/src/i18n/resources/zh-CN/dashboard.json +++ b/panel/src/i18n/resources/zh-CN/dashboard.json @@ -10,5 +10,36 @@ "fleet": "服务器概览", "manage": "管理", "loading_scene": "正在加载 3D 场景…", - "preparing_scene": "正在准备场景…" + "preparing_scene": "正在准备场景…", + "fleet_load": "负载与健康度", + "active_load": "玩家在线负载", + "status_distribution": "节点状态分布", + "account_status": "账号绑定状态", + "account_linked_title": "已关联游戏身份", + "account_linked_desc": "您的 Web 身份已成功绑定至 Minecraft 角色。拥有完整的所有权认领及启动权限。", + "account_unlinked_title": "未关联游戏角色", + "account_unlinked_desc": "您需要关联 Minecraft 角色才能认领或操作服务器。请立即前往个人中心绑定。", + "go_link": "前往绑定", + "system_specs": "系统实例与镜像指标", + "spec_images_total": "支持镜像总数", + "spec_images_built": "集群自建镜像", + "spec_images_external": "外部登记镜像", + "spec_servers_total": "可见服务器总数", + "spec_servers_owned": "我拥有的服务器", + "spec_servers_claimable": "公网可认领服", + "spec_running_ratio": "节点运行率", + "spec_stopped_ratio": "节点休眠率", + "spec_failed_ratio": "节点故障率", + "no_servers_monitor": "当前集群暂无服务器实例", + "images_count": "个支持的白名单镜像环境", + "legend_title": "3D 节点监控图例", + "legend_running": "运行中", + "legend_starting": "启动中", + "legend_stopping": "停止中", + "legend_stopped": "已休眠", + "legend_failed": "异常失败", + "legend_unknown": "未知状态", + "welcome_title": "欢迎使用 Felis 编排平台", + "welcome_desc": "您已成功登录用户控制台。您可以在「我的服务器」页面查看并认领分配给您的专属节点,或者对其进行休眠、唤醒和控制台日志监控。", + "go_to_servers_btn": "操作我的服务器" } diff --git a/panel/src/pages/Dashboard.tsx b/panel/src/pages/Dashboard.tsx index f3c1fa7..9708616 100644 --- a/panel/src/pages/Dashboard.tsx +++ b/panel/src/pages/Dashboard.tsx @@ -1,13 +1,25 @@ import { lazy, Suspense, useMemo } from "react"; import { Link } from "react-router-dom"; -import { Activity, Server, Play, Hand } from "lucide-react"; +import { + Activity, + Server, + Play, + CheckCircle, + AlertTriangle, + ArrowRight, + Cpu, + ShieldCheck, + Database, + Box, + Hand, +} from "lucide-react"; import { useTranslation } from "react-i18next"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Loading, ErrorState, EmptyState } from "@/components/States"; +import { Loading, ErrorState } from "@/components/States"; import { api } from "@/lib/api"; import { useAsync, useConfig } from "@/lib/hooks"; -import type { Phase, ServerInfo } from "@/lib/types"; +import type { Phase, ServerInfo, WhitelistImage } from "@/lib/types"; // three.js is heavy and only the Dashboard renders it — split it into its own // async chunk so the rest of the panel doesn't pay for it on first load. @@ -41,18 +53,32 @@ function Stat({ export function Dashboard() { const cfg = useConfig(); - const { data, error, loading, reload } = useAsync(() => api.myServers(), []); + const { data: servers, error: serversErr, loading: serversLoading, reload: reloadServers } = useAsync(() => api.myServers(), []); + const { data: linkData, error: linkErr, loading: linkLoading } = useAsync(() => api.linkStatus(), []); + const { data: identity, error: meErr, loading: meLoading } = useAsync(() => api.me(), []); + + // 仅对管理员加载镜像白名单列表,防止普通用户请求 403 报错 + const { data: imagesData } = useAsync(() => { + if (identity?.is_admin) { + return api.listImages(); + } + return Promise.resolve([]); + }, [identity]); + const { t } = useTranslation("dashboard"); const counts = useMemo(() => { - const servers = data ?? []; - const by = (p: Phase) => servers.filter((s) => s.phase === p).length; + const list = servers ?? []; + const by = (p: Phase) => list.filter((s) => s.phase === p).length; return { - total: servers.length, + total: list.length, running: by("Running"), - players: servers.reduce((n, s) => n + (s.players ?? 0), 0), + players: list.reduce((n, s) => n + (s.players ?? 0), 0), }; - }, [data]); + }, [servers]); + + const loading = serversLoading || linkLoading || meLoading; + const error = serversErr || linkErr || meErr; return (
@@ -61,12 +87,20 @@ export function Dashboard() {

{t("subtitle")}

- {loading && !data ? ( + {loading && !servers ? ( ) : error ? ( - + + ) : !cfg ? ( + ) : ( - + )}
); @@ -75,55 +109,405 @@ export function Dashboard() { function FleetView({ servers, counts, - hasConfig, + linkStatus, + images, + isAdmin, }: { servers: ServerInfo[]; counts: { total: number; running: number; players: number }; - hasConfig: boolean; + linkStatus?: { linked: boolean }; + images: WhitelistImage[]; + isAdmin: boolean; }) { const { t } = useTranslation("dashboard"); - if (servers.length === 0) { + + // 1. 统计各状态 of 服务器数量 + const statusStats = useMemo(() => { + let running = 0; + let starting = 0; + let stopping = 0; + let stopped = 0; + let failed = 0; + let unknown = 0; + let maxPlayers = 0; + + servers.forEach((s) => { + if (s.phase === "Running") running++; + else if (s.phase === "Starting") starting++; + else if (s.phase === "Stopping") stopping++; + else if (s.phase === "Stopped") stopped++; + else if (s.phase === "Failed") failed++; + else unknown++; + + maxPlayers += s.maxPlayers ?? 0; + }); + + return { + running, + starting, + stopping, + stopped, + failed, + unknown, + maxPlayers, + }; + }, [servers]); + + const loadPercentage = statusStats.maxPlayers > 0 + ? Math.round((counts.players / statusStats.maxPlayers) * 100) + : 0; + + // 2. 自建与外部镜像真实统计 + const builtImagesCount = useMemo(() => + images.filter((img) => img.source === "built" || !img.source).length, + [images] + ); + const externalImagesCount = useMemo(() => + images.filter((img) => img.source === "external").length, + [images] + ); + + // 3. 我拥有及可认领服务器真实统计 + const ownedServersCount = useMemo(() => + servers.filter((s) => s.owned).length, + [servers] + ); + const claimableServersCount = useMemo(() => + servers.filter((s) => s.claimable).length, + [servers] + ); + + // 4. 各种真实状态比率统计 + const runningRatio = useMemo(() => + servers.length > 0 ? Math.round((statusStats.running / servers.length) * 100) : 0, + [servers, statusStats.running] + ); + const stoppedRatio = useMemo(() => + servers.length > 0 ? Math.round((statusStats.stopped / servers.length) * 100) : 0, + [servers, statusStats.stopped] + ); + const failedRatio = useMemo(() => + servers.length > 0 ? Math.round((statusStats.failed / servers.length) * 100) : 0, + [servers, statusStats.failed] + ); + + // 辅助渲染百分比进度条段 + const renderBarSegment = (count: number, colorClass: string) => { + if (servers.length === 0 || count === 0) return null; + const widthPct = (count / servers.length) * 100; return ( - - - - - +
); - } + }; return ( - <> +
+ {/* 顶部 Stat */}
- - - {t("fleet")} - - - - - - {hasConfig ? ( -
+
+ {/* 左侧:账号状态、健康度及负载监控 (lg:col-span-8 h-full 捕获拉伸高度) */} +
+ {/* 1. 游戏角色绑定 Banner */} + + + {linkStatus?.linked ? ( + <> +
+
+ + {t("account_linked_title")} +
+

+ {t("account_linked_desc")} +

+
+ + + + + ) : ( + <> +
+
+ + {t("account_unlinked_title")} +
+

+ {t("account_unlinked_desc")} +

+
+ + + + + )} +
+
+ + {/* 2. 管理员展示集群负载健康与镜像参数看板,普通用户展示极简引导 */} + {isAdmin ? ( + <> + {/* 集群负载与健康卡片 */} + + + {t("fleet_load")} + + + {/* 玩家负载进度条 */} +
+
+ + {t("active_load")} + + + {counts.players} / {statusStats.maxPlayers} {t("stat_players_online")} ({loadPercentage}%) + +
+
+
+
+
+ + {/* 服务器状态分布健康条 */} +
+
+ {t("status_distribution")} + {servers.length} 实例 +
+ {servers.length > 0 ? ( +
+ {renderBarSegment(statusStats.running, "bg-[#22c55e]")} + {renderBarSegment(statusStats.starting, "bg-[#eab308]")} + {renderBarSegment(statusStats.stopping, "bg-[#f97316]")} + {renderBarSegment(statusStats.failed, "bg-[#ef4444]")} + {renderBarSegment(statusStats.stopped, "bg-[#64748b]")} + {renderBarSegment(statusStats.unknown, "bg-[#94a3b8]")} +
+ ) : ( +
+ {t("no_servers_monitor")} +
+ )} +
+ + {/* 镜像及规格说明 */} +
+ +
+ {images.length} + {t("images_count")}。所有后端镜像运行前均通过 Trivy CVE 漏洞扫描,强制在隔离的安全容器中托管(spec §16)。 +
+
+ + + + {/* 平台环境与配置参数卡片 (flex-1 自动拉伸) */} + + + + {t("system_specs")} + + + +
+ {/* 镜像总数 */} +
+
+ + {t("spec_images_total")} +
+
{images.length}
+
+ + {/* 集群内自建镜像 */} +
+
+ + {t("spec_images_built")} +
+
{builtImagesCount}
+
+ + {/* 外部登记导入镜像 */} +
+
+ + {t("spec_images_external")} +
+
{externalImagesCount}
+
+ + {/* 可见服务器总数 */} +
+
+ + {t("spec_servers_total")} +
+
{counts.total}
+
+ + {/* 我拥有管理的服务器 */} +
+
+ + {t("spec_servers_owned")} +
+
{ownedServersCount}
+
+ + {/* 公网可认领服务器 */} +
+
+ + {t("spec_servers_claimable")} +
+
{claimableServersCount}
+
+ + {/* 运行中实例占比 */} +
+
+ + {t("spec_running_ratio")} +
+
{runningRatio}%
+
+ + {/* 节点休眠率 */} +
+
+ + {t("spec_stopped_ratio")} +
+
{stoppedRatio}%
+
+ + {/* 运行故障率 */} +
+
+ + {t("spec_failed_ratio")} +
+
{failedRatio}%
+
+
+
+
+ + ) : ( + + + + {t("welcome_title")} + + + +

+ {t("welcome_desc")} +

+ + {/* 极简平台新手引导指引(真实且有用) */} +
+
快速入门指引:
+
    +
  • 认领无主服后,系统会自动将您的 Minecraft UUID 添加至准入白名单。
  • +
  • 服务器如果长时间处于无人在线空闲状态,平台会自动执行休眠存档以节约资源。
  • +
  • 游戏入口统一由 Velocity 网关承载,只需连接您的专用子域名即可。
  • +
+
+ +
+ + + +
+
+
+ )} +
+ + {/* 右侧:3D星图拓扑监控 (lg:col-span-4 h-full) */} + + + {t("fleet")} +

三维体素网络拓扑图

+
+ +
}>
- ) : ( - - )} -
-
- + {/* 状态图例说明 + 指标统计 */} +
+
+ {t("legend_title")} +
+
+
+
+ + {t("legend_running")} +
+ {statusStats.running} +
+
+
+ + {t("legend_starting")} +
+ {statusStats.starting} +
+
+
+ + {t("legend_stopping")} +
+ {statusStats.stopping} +
+
+
+ + {t("legend_stopped")} +
+ {statusStats.stopped} +
+
+
+ + {t("legend_failed")} +
+ {statusStats.failed} +
+
+
+ + {t("legend_unknown")} +
+ {statusStats.unknown} +
+
+
+
+
+
+
); }