diff --git a/panel/src/i18n/resources/en-US/servers.json b/panel/src/i18n/resources/en-US/servers.json index 8b7ecf4..b4e6a21 100644 --- a/panel/src/i18n/resources/en-US/servers.json +++ b/panel/src/i18n/resources/en-US/servers.json @@ -1,6 +1,10 @@ { "my_servers_title": "My servers", "my_servers_subtitle": "Wake, stop, or claim the servers you can manage.", + "search_placeholder": "Search server name or address…", + "filter_status_all": "All statuses", + "search_no_match": "No matching servers found.", + "search_clear_btn": "Clear filters", "no_servers_linked": "No servers linked to you", "no_servers_hint": "Claim an unowned server you have access to, or ask an admin to provision one.", "my_servers_footer": "Servers and game types are provisioned and managed by the platform. You claim a node to operate it; raw cluster config is never exposed here.", diff --git a/panel/src/i18n/resources/zh-CN/servers.json b/panel/src/i18n/resources/zh-CN/servers.json index da2132d..ef82a6b 100644 --- a/panel/src/i18n/resources/zh-CN/servers.json +++ b/panel/src/i18n/resources/zh-CN/servers.json @@ -1,6 +1,10 @@ { "my_servers_title": "我的服务器", "my_servers_subtitle": "管理你拥有或可用的服务器。", + "search_placeholder": "搜索服务器名称或地址…", + "filter_status_all": "全部状态", + "search_no_match": "没有匹配的服务器。", + "search_clear_btn": "清除筛选", "no_servers_linked": "暂无关联的服务器", "no_servers_hint": "认领一台你拥有访问权限的服务器,或联系管理员为你分配。", "my_servers_footer": "服务器及游戏类型由平台统一管理。认领后即可操作节点,原始集群配置不会暴露在此。", diff --git a/panel/src/pages/MyServers.tsx b/panel/src/pages/MyServers.tsx index 5f9ce54..40272ca 100644 --- a/panel/src/pages/MyServers.tsx +++ b/panel/src/pages/MyServers.tsx @@ -1,12 +1,16 @@ import { useState } from "react"; -import { Server } from "lucide-react"; +import { Server, Search, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Card, CardContent } from "@/components/ui/card"; import { ServerCard } from "@/components/ServerCard"; import { Pagination } from "@/components/Pagination"; import { Loading, ErrorState, EmptyState } from "@/components/States"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; import { api } from "@/lib/api"; import { useAsync, useConfig } from "@/lib/hooks"; +import { hostFor } from "@/lib/config"; const PAGE_SIZE = 12; @@ -16,8 +20,29 @@ export function MyServers() { const { t } = useTranslation("servers"); const servers = data ?? []; const [page, setPage] = useState(1); - const totalPages = Math.max(1, Math.ceil(servers.length / PAGE_SIZE)); - const paged = servers.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + + const filtered = servers.filter((s) => { + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase().trim(); + const host = cfg ? hostFor(s.subdomain, cfg).toLowerCase() : ""; + const name = s.name.toLowerCase(); + const displayName = (s.displayName || "").toLowerCase(); + if (!name.includes(q) && !displayName.includes(q) && !host.includes(q)) { + return false; + } + } + if (statusFilter !== "all") { + if (s.phase !== statusFilter) { + return false; + } + } + return true; + }); + + const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); + const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); return ( <> @@ -31,6 +56,56 @@ export function MyServers() { + {servers.length > 0 && cfg && ( +
{t("search_no_match")}
+ +