mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 09:31:55 +08:00
Merge branch 'main' of https://github.com/hamster1963/nezha-dash-v1 into hamster1963-main
This commit is contained in:
commit
6aec9ed2dc
39
package.json
39
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
22
src/App.tsx
22
src/App.tsx
@ -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 />} />
|
||||
|
@ -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(
|
||||
|
114
src/components/DashCommand.tsx
Normal file
114
src/components/DashCommand.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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) {
|
||||
|
@ -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>()))
|
||||
|
@ -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>
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
107
src/components/ui/command.tsx
Normal file
107
src/components/ui/command.tsx
Normal 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 }
|
@ -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])
|
||||
|
56
src/hooks/use-background.ts
Normal file
56
src/hooks/use-background.ts
Normal 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 }
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
120
src/locales/ta/translation.json
Normal file
120
src/locales/ta/translation.json
Normal 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": "ஏற்றுகிறது ..."
|
||||
}
|
||||
}
|
@ -116,5 +116,13 @@
|
||||
"price": "价格",
|
||||
"free": "免费",
|
||||
"usage-baseed": "按量计费"
|
||||
}
|
||||
},
|
||||
"TypeCommand": "输入命令或搜索",
|
||||
"NoResults": "结果为空",
|
||||
"Servers": "服务器",
|
||||
"Shortcuts": "快捷键",
|
||||
"ToggleLightMode": "切换亮色模式",
|
||||
"ToggleDarkMode": "切换暗色模式",
|
||||
"ToggleSystemMode": "切换系统模式",
|
||||
"Home": "首页"
|
||||
}
|
||||
|
@ -112,5 +112,13 @@
|
||||
"price": "價格",
|
||||
"free": "免費",
|
||||
"usage-baseed": "按量計費"
|
||||
}
|
||||
},
|
||||
"TypeCommand": "輸入命令或搜尋",
|
||||
"NoResults": "沒有結果",
|
||||
"Servers": "伺服器",
|
||||
"Shortcuts": "快捷鍵",
|
||||
"ToggleLightMode": "切換亮色模式",
|
||||
"ToggleDarkMode": "切換暗色模式",
|
||||
"ToggleSystemMode": "切換系統模式",
|
||||
"Home": "首頁"
|
||||
}
|
||||
|
@ -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 ">
|
||||
|
Loading…
x
Reference in New Issue
Block a user