import GlobalMap from "@/components/GlobalMap" import ServerCard from "@/components/ServerCard" import ServerOverview from "@/components/ServerOverview" import { ServiceTracker } from "@/components/ServiceTracker" import { Loader } from "@/components/loading/Loader" import { Label } from "@/components/ui/label" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { SORT_ORDERS, SORT_TYPES } from "@/context/sort-context" import { useSort } from "@/hooks/use-sort" import { useStatus } from "@/hooks/use-status" import { useWebSocketContext } from "@/hooks/use-websocket-context" import { fetchServerGroup, fetchService } from "@/lib/nezha-api" import { cn, formatNezhaInfo } from "@/lib/utils" import { NezhaWebsocketResponse } from "@/types/nezha-api" import { ServerGroup } from "@/types/nezha-api" import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon } from "@heroicons/react/20/solid" import { useQuery } from "@tanstack/react-query" import { useCallback, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import DirectCountrySelect from "@/components/DirectCountrySelect" export default function Servers() { const { t } = useTranslation() const { sortType, sortOrder, setSortOrder, setSortType } = useSort() const { data: groupData } = useQuery({ queryKey: ["server-group"], queryFn: () => fetchServerGroup(), }) const { lastMessage, connected } = useWebSocketContext() const { status } = useStatus() const [showServices, setShowServices] = useState("0") const [showMap, setShowMap] = useState("0") const containerRef = useRef(null) const [settingsOpen, setSettingsOpen] = useState(false) // 使用ref存储筛选状态,防止WebSocket消息刷新时重置 const groupRef = useRef("All") const countryRef = useRef("All") const [currentGroup, setCurrentGroup] = useState("All") const [currentCountry, setCurrentCountry] = useState("All") // 保存是否已经初始化了筛选状态 const initializedRef = useRef(false) const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const restoreScrollPosition = () => { const savedPosition = sessionStorage.getItem("scrollPosition") if (savedPosition && containerRef.current) { containerRef.current.scrollTop = Number(savedPosition) } } const handleCountryChange = useCallback((newCountry: string) => { countryRef.current = newCountry; // 强制立即更新状态 setCurrentCountry(newCountry); // 保存到会话存储 sessionStorage.setItem("selectedCountry", newCountry); sessionStorage.setItem("scrollPosition", String(containerRef.current?.scrollTop || 0)); }, []); useEffect(() => { const showServicesState = localStorage.getItem("showServices") if (window.ForceShowServices) { setShowServices("1") } else if (showServicesState !== null) { setShowServices(showServicesState) } else { localStorage.setItem("showServices", "0") setShowServices("0") } }, []) useEffect(() => { const showMapState = localStorage.getItem("showMap") if (window.ForceShowMap) { setShowMap("1") } else if (showMapState !== null) { setShowMap(showMapState) } }, []) // 仅在组件挂载时初始化一次状态 useEffect(() => { if (initializedRef.current) return; const savedGroup = sessionStorage.getItem("selectedGroup") || "All" const savedCountry = sessionStorage.getItem("selectedCountry") || "All" groupRef.current = savedGroup countryRef.current = savedCountry setCurrentGroup(savedGroup) setCurrentCountry(savedCountry) restoreScrollPosition() // 如果没有保存值,初始化存储 if (!sessionStorage.getItem("selectedGroup")) { sessionStorage.setItem("selectedGroup", "All") } if (!sessionStorage.getItem("selectedCountry")) { sessionStorage.setItem("selectedCountry", "All") } initializedRef.current = true }, []) // 当WebSocket消息更新时,确保UI状态与ref同步 useEffect(() => { if (!lastMessage || !initializedRef.current) return; // 保持用户选择的筛选状态 setCurrentGroup(groupRef.current) setCurrentCountry(countryRef.current) }, [lastMessage]) const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null // 获取所有可用的国家代码 const availableCountries = nezhaWsData?.servers ? [...new Set(nezhaWsData.servers.map(server => server.country_code?.toLowerCase()))] .filter(Boolean) .sort() : [] const countryTabs = [ "All", ...availableCountries.map(code => code.toUpperCase()) ] // 获取cycle_transfer_stats数据 const { data: serviceData } = useQuery({ queryKey: ["service"], queryFn: () => fetchService(), refetchOnMount: true, refetchOnWindowFocus: true, refetchInterval: 10000, }) const cycleTransferStats = serviceData?.data?.cycle_transfer_stats if (!connected && !lastMessage) { return (
{t("info.websocketConnecting")}
) } if (!nezhaWsData) { return (

{t("info.processing")}

) } let filteredServers = nezhaWsData?.servers?.filter((server) => { // 组筛选 if (currentGroup !== "All") { const group = groupData?.data?.find( (g: ServerGroup) => g.group.name === currentGroup && Array.isArray(g.servers) && g.servers.includes(server.id), ) if (!group) { return false } } // 国家筛选 if (currentCountry !== "All") { const serverCountry = server.country_code?.toUpperCase() if (serverCountry !== currentCountry) { return false } } return true }) || [] const totalServers = filteredServers.length || 0 const onlineServers = filteredServers.filter((server) => formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0 const offlineServers = filteredServers.filter((server) => !formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0 const up = filteredServers.reduce( (total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_transfer ?? 0) : total), 0, ) || 0 const down = filteredServers.reduce( (total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_transfer ?? 0) : total), 0, ) || 0 const upSpeed = filteredServers.reduce( (total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_speed ?? 0) : total), 0, ) || 0 const downSpeed = filteredServers.reduce( (total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_speed ?? 0) : total), 0, ) || 0 filteredServers = status === "all" ? filteredServers : filteredServers.filter((server) => [status].includes(formatNezhaInfo(nezhaWsData.now, server).online ? "online" : "offline")) filteredServers = filteredServers.sort((a, b) => { const serverAInfo = formatNezhaInfo(nezhaWsData.now, a) const serverBInfo = formatNezhaInfo(nezhaWsData.now, b) if (sortType !== "name") { // 仅在非 "name" 排序时,先按在线状态排序 if (!serverAInfo.online && serverBInfo.online) return 1 if (serverAInfo.online && !serverBInfo.online) return -1 if (!serverAInfo.online && !serverBInfo.online) { // 如果两者都离线,可以继续按照其他条件排序,或者保持原序 // 这里选择保持原序 return 0 } } let comparison = 0 switch (sortType) { case "name": comparison = a.name.localeCompare(b.name) break case "uptime": comparison = (a.state?.uptime ?? 0) - (b.state?.uptime ?? 0) break case "system": comparison = a.host.platform.localeCompare(b.host.platform) break case "cpu": comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0) break case "mem": comparison = (formatNezhaInfo(nezhaWsData.now, a).mem ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).mem ?? 0) break case "disk": comparison = (formatNezhaInfo(nezhaWsData.now, a).disk ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).disk ?? 0) break case "up": comparison = (a.state?.net_out_speed ?? 0) - (b.state?.net_out_speed ?? 0) break case "down": comparison = (a.state?.net_in_speed ?? 0) - (b.state?.net_in_speed ?? 0) break case "up total": comparison = (a.state?.net_out_transfer ?? 0) - (b.state?.net_out_transfer ?? 0) break case "down total": comparison = (a.state?.net_in_transfer ?? 0) - (b.state?.net_in_transfer ?? 0) break default: comparison = 0 } return sortOrder === "asc" ? comparison : -comparison }) return (
{showMap === "1" && } {showServices === "1" && } {/* 优化直接国家选择器 */}
tab !== "All")} currentCountry={currentCountry} onChange={handleCountryChange} />
{filteredServers.map((serverInfo) => { // 查找服务器所属的分组 const serverGroup = groupData?.data?.find( (g: ServerGroup) => Array.isArray(g.servers) && g.servers.includes(serverInfo.id) ); return ( ); })}
) }