Merge branch 'main' of https://github.com/hamster1963/nezha-dash-v1 into hamster1963-main

This commit is contained in:
wood chen 2025-01-24 17:53:42 +08:00
commit 6aec9ed2dc
26 changed files with 526 additions and 131 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -16,55 +16,56 @@
"@heroicons/react": "^2.2.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-query-devtools": "^5.63.0",
"@tanstack/react-query": "^5.64.2",
"@tanstack/react-query-devtools": "^5.64.2",
"@tanstack/react-table": "^8.20.6",
"@trivago/prettier-plugin-sort-imports": "^5.2.1",
"@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"country-flag-icons": "^1.5.13",
"cmdk": "1.0.0",
"country-flag-icons": "^1.5.14",
"d3-geo": "^3.1.1",
"dayjs": "^1.11.13",
"framer-motion": "^12.0.0-alpha.2",
"framer-motion": "^12.0.3",
"i18n-iso-countries": "^7.13.0",
"i18next": "^24.2.1",
"lucide-react": "^0.460.0",
"luxon": "^3.5.0",
"prettier-plugin-tailwindcss": "^0.6.9",
"prettier-plugin-tailwindcss": "^0.6.10",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^15.4.0",
"react-router-dom": "^7.1.1",
"react-router-dom": "^7.1.3",
"recharts": "^2.15.0",
"sonner": "^1.7.1",
"sonner": "^1.7.2",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/node": "^22.10.5",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@eslint/js": "^9.18.0",
"@types/node": "^22.10.9",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react-swc": "^3.7.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint": "^9.18.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"postcss": "^8.4.49",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.3",
"typescript-eslint": "^8.19.1",
"vite": "^6.0.7"
"typescript-eslint": "^8.21.0",
"vite": "^6.0.11"
}
}

View File

@ -6,6 +6,7 @@ import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
import ErrorBoundary from "./components/ErrorBoundary"
import Footer from "./components/Footer"
import Header, { RefreshToast } from "./components/Header"
import { useBackground } from "./hooks/use-background"
import { useTheme } from "./hooks/use-theme"
import { InjectContext } from "./lib/inject"
import { fetchSetting } from "./lib/nezha-api"
@ -14,6 +15,7 @@ import ErrorPage from "./pages/ErrorPage"
import NotFound from "./pages/NotFound"
import Server from "./pages/Server"
import ServerDetail from "./pages/ServerDetail"
import { DashCommand } from "./components/DashCommand"
const App: React.FC = () => {
const { data: settingData, error } = useQuery({
@ -25,11 +27,7 @@ const App: React.FC = () => {
const { i18n } = useTranslation()
const { setTheme } = useTheme()
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false)
// 检测是否强制指定了主题颜色
const forceTheme =
// @ts-expect-error ForceTheme is a global variable
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined
const { backgroundImage: customBackgroundImage } = useBackground()
useEffect(() => {
if (settingData?.data?.config?.custom_code) {
@ -38,6 +36,11 @@ const App: React.FC = () => {
}
}, [settingData?.data?.config?.custom_code])
// 检测是否强制指定了主题颜色
const forceTheme =
// @ts-expect-error ForceTheme is a global variable
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined
useEffect(() => {
if (forceTheme === "dark" || forceTheme === "light") {
setTheme(forceTheme)
@ -60,13 +63,7 @@ const App: React.FC = () => {
i18n.changeLanguage(settingData?.data?.config?.language)
}
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customMobileBackgroundImage =
// @ts-expect-error CustomMobileBackgroundImage is a global variable
(window.CustomMobileBackgroundImage as string) !== "" ? window.CustomMobileBackgroundImage : undefined
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
return (
<Router basename={import.meta.env.BASE_URL}>
@ -94,6 +91,7 @@ const App: React.FC = () => {
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
<RefreshToast />
<Header />
<DashCommand />
<Routes>
<Route path="/" element={<Server />} />
<Route path="/server/:id" element={<ServerDetail />} />

View File

@ -22,9 +22,7 @@ interface CycleTransferStatsClientProps {
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => {
const { t } = useTranslation()
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
return (
<div
className={cn(

View File

@ -0,0 +1,114 @@
"use client"
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"
import { useTheme } from "@/hooks/use-theme"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatNezhaInfo } from "@/lib/utils"
import { NezhaWebsocketResponse } from "@/types/nezha-api"
import { Home, Moon, Sun, SunMoon } from "lucide-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
export function DashCommand() {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState("")
const navigate = useNavigate()
const { t } = useTranslation()
const { setTheme } = useTheme()
const { lastMessage, connected } = useWebSocketContext()
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
if (!connected || !nezhaWsData) return null
const shortcuts = [
{
keywords: ["home", "homepage"],
icon: <Home />,
label: t("Home"),
action: () => navigate("/"),
},
{
keywords: ["light", "theme", "lightmode"],
icon: <Sun />,
label: t("ToggleLightMode"),
action: () => setTheme("light"),
},
{
keywords: ["dark", "theme", "darkmode"],
icon: <Moon />,
label: t("ToggleDarkMode"),
action: () => setTheme("dark"),
},
{
keywords: ["system", "theme", "systemmode"],
icon: <SunMoon />,
label: t("ToggleSystemMode"),
action: () => setTheme("system"),
},
].map((item) => ({
...item,
value: `${item.keywords.join(" ")} ${item.label}`,
}))
return (
<>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} />
<CommandList>
<CommandEmpty>{t("NoResults")}</CommandEmpty>
<CommandGroup heading={t("Servers")}>
{nezhaWsData.servers.map((server) => (
<CommandItem
key={server.id}
value={server.name}
onSelect={() => {
navigate(`/server/${server.id}`)
setOpen(false)
}}
>
{formatNezhaInfo(nezhaWsData.now, server).online ? (
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
) : (
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
)}
<span>{server.name}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading={t("Shortcuts")}>
{shortcuts.map((item) => (
<CommandItem
key={item.label}
value={item.value}
onSelect={() => {
item.action()
setOpen(false)
}}
>
{item.icon}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
</>
)
}

View File

@ -13,9 +13,7 @@ export default function GlobalMap({ serverList, now }: { serverList: NezhaServer
const countryList: string[] = []
const serverCounts: { [key: string]: number } = {}
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
serverList.forEach((server) => {
if (server.country_code) {

View File

@ -11,9 +11,7 @@ export default function GroupSwitch({
currentTab: string
setCurrentTab: (tab: string) => void
}) {
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const scrollRef = useRef<HTMLDivElement>(null)
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()))

View File

@ -1,11 +1,13 @@
import { ModeToggle } from "@/components/ThemeSwitcher"
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { useBackground } from "@/hooks/use-background"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
import { cn } from "@/lib/utils"
import { useQuery } from "@tanstack/react-query"
import { AnimatePresence, m } from "framer-motion"
import { ImageMinus } from "lucide-react"
import { DateTime } from "luxon"
import { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
@ -18,6 +20,7 @@ import { Button } from "./ui/button"
function Header() {
const { t } = useTranslation()
const navigate = useNavigate()
const { backgroundImage, updateBackground } = useBackground()
const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"],
@ -38,9 +41,7 @@ function Header() {
// @ts-expect-error CustomDesc is a global variable
const customDesc = window.CustomDesc || t("nezha")
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
useEffect(() => {
const link = document.querySelector("link[rel*='icon']") || document.createElement("link")
@ -57,6 +58,22 @@ function Header() {
document.title = siteName || "CZL SVR"
}, [siteName])
const handleBackgroundToggle = () => {
if (window.CustomBackgroundImage) {
// Store the current background image before removing it
sessionStorage.setItem("savedBackgroundImage", window.CustomBackgroundImage)
updateBackground(undefined)
} else {
// Restore the saved background image
const savedImage = sessionStorage.getItem("savedBackgroundImage")
if (savedImage) {
updateBackground(savedImage)
}
}
}
const customBackgroundImage = backgroundImage
return (
<div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between header-top">
@ -87,6 +104,19 @@ function Header() {
</div>
<LanguageSwitcher />
<ModeToggle />
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && (
<Button
variant="outline"
size="sm"
onClick={handleBackgroundToggle}
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
"bg-white/70 dark:bg-black/70": customBackgroundImage,
"hidden sm:block": customMobileBackgroundImage,
})}
>
<ImageMinus className="w-4 h-4" />
</Button>
)}
<Button
variant="outline"
size="sm"
@ -234,32 +264,23 @@ function DashboardLink() {
)
}
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef<() => void>(() => {})
useEffect(() => {
savedCallback.current = callback
})
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0)
return () => clearInterval(interval)
}
return undefined
}, [delay])
}
function Overview() {
const { t } = useTranslation()
const [mouted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const timeOption = DateTime.TIME_SIMPLE
const timeOption = DateTime.TIME_WITH_SECONDS
timeOption.hour12 = true
const [timeString, setTimeString] = useState(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
useInterval(() => {
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
}, 1000)
useEffect(() => {
const updateTime = () => {
const now = DateTime.now().setLocale("en-US").toLocaleString(timeOption)
setTimeString(now)
requestAnimationFrame(updateTime)
}
requestAnimationFrame(updateTime)
}, [])
return (
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
<p className="text-base font-semibold">👋 {t("overview")}</p>

View File

@ -9,9 +9,7 @@ import { useTranslation } from "react-i18next"
export function LanguageSwitcher() {
const { t, i18n } = useTranslation()
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const locale = i18n.languages[0]

View File

@ -93,9 +93,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
const defaultChart = "All"
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const [activeChart, setActiveChart] = React.useState(defaultChart)
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false)

View File

@ -27,9 +27,7 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
const showFlag = true
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
// @ts-expect-error ShowNetTransfer is a global variable
const showNetTransfer = window.ShowNetTransfer as boolean

View File

@ -27,9 +27,7 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
const showFlag = true
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const parsedData = parsePublicNote(public_note)

View File

@ -128,9 +128,7 @@ function GpuChart({
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
// 初始化历史数据
useEffect(() => {
@ -237,9 +235,7 @@ function CpuChart({ now, data, messageHistory }: { now: number; data: NezhaServe
const { cpu } = formatNezhaInfo(now, data)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
// 初始化历史数据
useEffect(() => {
@ -343,9 +339,7 @@ function ProcessChart({ now, data, messageHistory }: { now: number; data: NezhaS
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const { process } = formatNezhaInfo(now, data)
@ -457,9 +451,7 @@ function MemChart({ now, data, messageHistory }: { now: number; data: NezhaServe
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const { mem, swap } = formatNezhaInfo(now, data)
@ -602,9 +594,7 @@ function DiskChart({ now, data, messageHistory }: { now: number; data: NezhaServ
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const { disk } = formatNezhaInfo(now, data)
@ -715,9 +705,7 @@ function NetworkChart({ now, data, messageHistory }: { now: number; data: NezhaS
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const { up, down } = formatNezhaInfo(now, data)
@ -858,9 +846,7 @@ function ConnectChart({ now, data, messageHistory }: { now: number; data: NezhaS
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const { tcp, udp } = formatNezhaInfo(now, data)

View File

@ -75,9 +75,7 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
last_active_time_string,
} = formatNezhaInfo(nezhaWsData.now, server)
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
countries.registerLocale(enLocale)

View File

@ -25,9 +25,7 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
// @ts-expect-error CustomIllustration is a global variable
const customIllustration = window.CustomIllustration || "/animated-man.webp"
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
return (
<>

View File

@ -17,9 +17,7 @@ interface ServiceTrackerProps {
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
const { t } = useTranslation()
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
return (
<div
className={cn(

View File

@ -4,9 +4,7 @@ import { useTranslation } from "react-i18next"
export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: string[]; currentTab: string; setCurrentTab: (tab: string) => void }) {
const { t } = useTranslation()
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
return (
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
<div

View File

@ -12,9 +12,7 @@ export function ModeToggle() {
const { t } = useTranslation()
const { setTheme, theme } = useTheme()
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const handleSelect = (e: Event, newTheme: Theme) => {
e.preventDefault()

View File

@ -0,0 +1,107 @@
"use client"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import * as React from "react"
const Command = React.forwardRef<React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>>(
({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn("flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)}
{...props}
/>
),
)
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogTitle />
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>>(
({ className, ...props }, ref) => (
<div className="flex items-center border-b bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
),
)
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>>(
({ className, ...props }, ref) => (
<CommandPrimitive.List ref={ref} className={cn("max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden", className)} {...props} />
),
)
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>>(
(props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />,
)
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>(
({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
),
)
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />)
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>>(
({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}
/>
),
)
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
}
CommandShortcut.displayName = "CommandShortcut"
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator }

View File

@ -106,28 +106,15 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
useEffect(() => {
connect()
// 添加页面可见性变化监听
const handleVisibilityChange = () => {
if (document.hidden) {
// 页面隐藏时断开连接
cleanup()
} else {
// 页面可见时重新连接
connect()
}
}
// 添加页面卸载事件监听
const handleBeforeUnload = () => {
cleanup()
}
document.addEventListener("visibilitychange", handleVisibilityChange)
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
cleanup()
document.removeEventListener("visibilitychange", handleVisibilityChange)
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [url])

View File

@ -0,0 +1,56 @@
import { useEffect, useState } from "react"
declare global {
interface Window {
CustomBackgroundImage: string
CustomMobileBackgroundImage: string
}
}
const BACKGROUND_CHANGE_EVENT = "backgroundChange"
export function useBackground() {
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(undefined)
useEffect(() => {
// 监听背景变化
const handleBackgroundChange = () => {
setBackgroundImage(window.CustomBackgroundImage || undefined)
}
// 初始化检查
const checkInitialBackground = () => {
if (window.CustomBackgroundImage) {
setBackgroundImage(window.CustomBackgroundImage)
} else {
const savedImage = sessionStorage.getItem("savedBackgroundImage")
if (savedImage) {
window.CustomBackgroundImage = savedImage
setBackgroundImage(savedImage)
}
}
}
// 设置一个轮询来检查初始背景
const intervalId = setInterval(() => {
if (window.CustomBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) {
checkInitialBackground()
clearInterval(intervalId)
}
}, 100)
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
return () => {
window.removeEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
clearInterval(intervalId)
}
}, [])
const updateBackground = (newBackground: string | undefined) => {
window.CustomBackgroundImage = newBackground || ""
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT))
}
return { backgroundImage, updateBackground }
}

View File

@ -116,5 +116,13 @@
"price": "Price",
"free": "Free",
"usage-baseed": "Usage-based"
}
},
"TypeCommand": "Type a command or search...",
"NoResults": "No results found.",
"Servers": "Servers",
"Shortcuts": "Shortcuts",
"ToggleLightMode": "Toggle Light Mode",
"ToggleDarkMode": "Toggle Dark Mode",
"ToggleSystemMode": "Toggle System Mode",
"Home": "Home"
}

View File

@ -0,0 +1,120 @@
{
"nezha": "கண்காணிப்பு",
"overview": "கண்ணோட்டம்",
"dashboard": "முகப்புப்பெட்டி",
"login": "புகுபதிவு",
"serverCard": {
"mem": "மெம்",
"stg": "Stg",
"days": "நாட்கள்",
"hours": "மணி",
"upload": "பதிவேற்றும்",
"download": "பதிவிறக்கம்",
"system": "மண்டலம்",
"uptime": "நேரம்",
"totalUpload": "பதிவேற்றும்",
"totalDownload": "பதிவிறக்கம்"
},
"online": "ஆன்லைனில்",
"offline": "இணையமில்லாமல்",
"whereTheTimeIs": "நேரம் இருக்கும் இடம்",
"refreshing": "புத்துணர்ச்சி",
"info": {
"websocketConnecting": "வெப்சாக்கெட் இணைத்தல்",
"websocketConnected": "வெப்சாக்கெட் இணைக்கப்பட்டுள்ளது",
"websocketDisconnected": "வெப்சாக்கெட் துண்டிக்கப்பட்டது",
"processing": "செயலாக்கம் ..."
},
"serverOverview": {
"totalServers": "மொத்த சேவையகங்கள்",
"onlineServers": "நிகழ்நிலை சேவையகங்கள்",
"offlineServers": "இணைப்பில்லாத சேவையகங்கள்",
"totalBandwidth": "மொத்த அலைவரிசை",
"speed": "வேகம்",
"network": "பிணையம்"
},
"map": {
"Distributions": "சேவையகங்கள் விநியோகிக்கப்படுகின்றன",
"Regions": "பகுதிகள்",
"Servers": "சேவையகங்கள்"
},
"cycleTransfer": {
"used": "பயன்படுத்தப்பட்டது",
"total": "மொத்தம்",
"nextUpdate": "அடுத்த புதுப்பிப்பு"
},
"serverDetail": {
"offline": "இணையமில்லாமல்",
"unknown": "தெரியவில்லை",
"uptime": "நேரம்",
"version": "பதிப்பு",
"arch": "மான்",
"mem": "மெம்",
"disk": "வட்டு",
"region": "பகுதி",
"system": "மண்டலம்",
"status": "நிலை",
"online": "ஆன்லைனில்",
"days": "நாட்கள்",
"upload": "பதிவேற்றும்",
"download": "பதிவிறக்கம்",
"lastActive": "கடைசி செயலில் நேரம்",
"temperature": "வெப்பநிலை"
},
"serverDetailChart": {
"swap": "இடமாற்றம்",
"upload": "பதிவேற்றும்",
"download": "பதிவிறக்கம்",
"process": "செயல்முறை",
"disk": "வட்டு",
"mem": "மெம்"
},
"footer": {
"themeBy": "மூலம் கருப்பொருள் "
},
"language": {
"zh-CN": "எளிமைப்படுத்தப்பட்ட சீன",
"zh-TW": "பாரம்பரிய சீன",
"en-US": "ஆங்கிலம்"
},
"theme": {
"light": "ஒளி",
"dark": "இருண்ட",
"system": "மண்டலம்"
},
"error": {
"pageNotFound": "பக்கம் கிடைக்கவில்லை",
"backToHome": "வீட்டிற்கு திரும்பவும்"
},
"tabSwitch": {
"Detail": "விவரம்",
"Network": "பிணையம்"
},
"monitor": {
"noData": "சேவையக மானிட்டர் தரவு இல்லை, முதலில் ஒரு பணி மானிட்டரைச் சேர்க்கவும்",
"avgDelay": "சுணக்கம்",
"monitorCount": "சேவைகள்"
},
"pwa": {
"offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது",
"newContent": "புதிய உள்ளடக்கம் கிடைக்கிறது",
"reload": "புதுப்பிப்பு"
},
"billingInfo": {
"remaining": "மீதமுள்ள",
"error": "பிழை",
"indefinite": "காலவரையற்றது",
"expired": "காலாவதியான",
"days": "நாட்கள்",
"price": "விலை",
"free": "இலவசம்",
"usage-baseed": "பயன்பாடு அடிப்படையிலானது"
},
"serviceTracker": {
"noService": "பணி தரவு இல்லை",
"uptime": "நேரம்",
"daysAgo": "சில நாட்களுக்கு முன்பு",
"today": "இன்று",
"loading": "ஏற்றுகிறது ..."
}
}

View File

@ -116,5 +116,13 @@
"price": "价格",
"free": "免费",
"usage-baseed": "按量计费"
}
},
"TypeCommand": "输入命令或搜索",
"NoResults": "结果为空",
"Servers": "服务器",
"Shortcuts": "快捷键",
"ToggleLightMode": "切换亮色模式",
"ToggleDarkMode": "切换暗色模式",
"ToggleSystemMode": "切换系统模式",
"Home": "首页"
}

View File

@ -112,5 +112,13 @@
"price": "價格",
"free": "免費",
"usage-baseed": "按量計費"
}
},
"TypeCommand": "輸入命令或搜尋",
"NoResults": "沒有結果",
"Servers": "伺服器",
"Shortcuts": "快捷鍵",
"ToggleLightMode": "切換亮色模式",
"ToggleDarkMode": "切換暗色模式",
"ToggleSystemMode": "切換系統模式",
"Home": "首頁"
}

View File

@ -36,9 +36,7 @@ export default function Servers() {
const [settingsOpen, setSettingsOpen] = useState<boolean>(false)
const [currentGroup, setCurrentGroup] = useState<string>("All")
const customBackgroundImage =
// @ts-expect-error CustomBackgroundImage is a global variable
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const restoreScrollPosition = () => {
const savedPosition = sessionStorage.getItem("scrollPosition")
@ -74,7 +72,16 @@ export default function Servers() {
restoreScrollPosition()
}, [])
const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])]
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
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 && !lastMessage) {
return (
@ -87,8 +94,6 @@ export default function Servers() {
)
}
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
if (!nezhaWsData) {
return (
<div className="flex flex-col items-center justify-center ">