import { ArrowDownIcon, ArrowsUpDownIcon, ArrowUpIcon, ChartBarSquareIcon, MapIcon, ViewColumnsIcon, } from "@heroicons/react/20/solid"; import { useQuery } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import GlobalMap from "@/components/GlobalMap"; import GroupSwitch from "@/components/GroupSwitch"; import { Loader } from "@/components/loading/Loader"; import ServerCard from "@/components/ServerCard"; import ServerCardInline from "@/components/ServerCardInline"; import ServerOverview from "@/components/ServerOverview"; import { ServiceTracker } from "@/components/ServiceTracker"; import { 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 type { ServerGroup } from "@/types/nezha-api"; export default function Servers() { const { t } = useTranslation(); const { sortType, sortOrder, setSortOrder, setSortType } = useSort(); const { data: groupData } = useQuery({ queryKey: ["server-group"], queryFn: () => fetchServerGroup(), }); const { data: serviceData } = useQuery({ queryKey: ["service"], queryFn: () => fetchService(), refetchOnMount: true, refetchOnWindowFocus: true, refetchInterval: 10000, }); const hasServices = !!serviceData?.data?.services && Object.keys(serviceData.data.services).length > 0; const { lastData, connected } = useWebSocketContext(); const { status } = useStatus(); const [showServices, setShowServices] = useState("0"); const [showMap, setShowMap] = useState("0"); const [inline, setInline] = useState("0"); const hasRestoredScroll = useRef(false); const [currentGroup, setCurrentGroup] = useState("All"); const nezhaWsData = lastData; const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined; const restoreScrollPosition = useCallback(() => { const isFromMainPage = sessionStorage.getItem("fromMainPage") === "true"; const savedPosition = sessionStorage.getItem("scrollPosition"); const scrollTop = savedPosition ? Number(savedPosition) : Number.NaN; if ( hasRestoredScroll.current || !isFromMainPage || !Number.isFinite(scrollTop) ) { return; } hasRestoredScroll.current = true; requestAnimationFrame(() => { requestAnimationFrame(() => { window.scrollTo({ top: scrollTop, left: 0, behavior: "auto" }); }); }); }, []); const handleTagChange = (newGroup: string) => { setCurrentGroup(newGroup); sessionStorage.setItem("selectedGroup", newGroup); sessionStorage.setItem("scrollPosition", String(window.scrollY || 0)); }; useEffect(() => { const showServicesState = localStorage.getItem("showServices"); if (window.ForceShowServices) { setShowServices("1"); } else if (showServicesState !== null) { setShowServices(showServicesState); } }, []); useEffect(() => { if (!hasServices) { setShowServices("0"); } }, [hasServices]); useEffect(() => { const checkInlineSettings = () => { const isMobile = window.innerWidth < 768; if (!isMobile) { const inlineState = localStorage.getItem("inline"); if (window.ForceCardInline) { setInline("1"); } else if (inlineState !== null) { setInline(inlineState); } } }; checkInlineSettings(); window.addEventListener("resize", checkInlineSettings); return () => { window.removeEventListener("resize", checkInlineSettings); }; }, []); useEffect(() => { const showMapState = localStorage.getItem("showMap"); if (window.ForceShowMap) { setShowMap("1"); } else if (showMapState !== null) { setShowMap(showMapState); } }, []); useEffect(() => { const savedGroup = sessionStorage.getItem("selectedGroup") || "All"; setCurrentGroup(savedGroup); }, []); useEffect(() => { if (nezhaWsData) { restoreScrollPosition(); } }, [nezhaWsData, restoreScrollPosition]); const groupTabs = [ "All", ...(groupData?.data ?.filter((item: ServerGroup) => { return ( Array.isArray(item.servers) && item.servers.some((serverId) => nezhaWsData?.servers?.some((server) => server.id === serverId), ) ); }) ?.map((item: ServerGroup) => item.group.name) || []), ]; if (!connected && !lastData) { return (
{t("info.websocketConnecting")}
); } if (!nezhaWsData) { return (

{t("info.processing")}

); } let filteredServers = nezhaWsData?.servers?.filter((server) => { if (currentGroup === "All") return true; const group = groupData?.data?.find( (g: ServerGroup) => g.group.name === currentGroup && Array.isArray(g.servers) && g.servers.includes(server.id), ); return !!group; }) || []; 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 (
{hasServices && ( )}
| {t(`sort.types.${sortType.replace(/ /g, "_")}`)}
{showMap === "1" && ( )} {showServices === "1" && } {inline === "1" && (
{filteredServers.map((serverInfo) => ( ))}
)} {inline === "0" && (
{filteredServers.map((serverInfo) => ( ))}
)}
); }