mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 09:31:55 +08:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
dbe3725aaf | |||
4e3d24975c | |||
d9995fab5f | |||
63b5dd1893 | |||
3f7985ecf2 | |||
3958b3b35c | |||
9090b407cc | |||
372cd247ae | |||
49af2059b8 | |||
d5f3548af4 | |||
b800ce816a | |||
eeddef0efc | |||
20ca646e9a |
55
index.html
55
index.html
@ -3,29 +3,20 @@
|
|||||||
<head>
|
<head>
|
||||||
<script>
|
<script>
|
||||||
// 在页面渲染前就执行主题初始化
|
// 在页面渲染前就执行主题初始化
|
||||||
try {
|
document.documentElement.classList.add("dark")
|
||||||
const storageKey = "vite-ui-theme"
|
|
||||||
let theme = localStorage.getItem(storageKey)
|
|
||||||
if (theme === "system" || !theme) {
|
|
||||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
||||||
}
|
|
||||||
document.documentElement.classList.add(theme)
|
|
||||||
} catch (e) {
|
|
||||||
document.documentElement.classList.add("light")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局配置变量
|
// 全局配置变量
|
||||||
window.ShowServerDetails = true; // 是否显示服务器详细信息
|
window.ShowServerDetails = true; // 是否显示服务器详细信息
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
/* Prevent FOUC in Safari */
|
/* Prevent FOUC in Safari */
|
||||||
html:not(.dark):not(.light) * {
|
html:not(.dark) * {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: dark;
|
||||||
--bg: #ffffff;
|
--bg: #242424;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark {
|
html.dark {
|
||||||
@ -33,21 +24,16 @@
|
|||||||
--bg: #242424;
|
--bg: #242424;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light {
|
|
||||||
color-scheme: light;
|
|
||||||
--bg: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--bg) !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg) !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
background-color: var(--bg) !important;
|
background: transparent !important;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,31 +53,10 @@
|
|||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
;(function () {
|
;(function () {
|
||||||
const storageKey = "vite-ui-theme"
|
|
||||||
const theme = localStorage.getItem(storageKey) || "system"
|
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
|
root.classList.remove("light")
|
||||||
function updateThemeColor(isDark) {
|
root.classList.add("dark")
|
||||||
const themeColor = isDark ? "#242424" : "#fafafa"
|
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", "#242424")
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTheme(newTheme) {
|
|
||||||
root.classList.remove("light", "dark")
|
|
||||||
root.classList.add(newTheme)
|
|
||||||
updateThemeColor(newTheme === "dark")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (theme === "system") {
|
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
||||||
setTheme(systemTheme)
|
|
||||||
|
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
|
||||||
setTheme(e.matches ? "dark" : "light")
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setTheme(theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add loaded class after React has mounted
|
// Add loaded class after React has mounted
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
|
56
src/App.tsx
56
src/App.tsx
@ -1,17 +1,14 @@
|
|||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
|
import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
|
||||||
|
|
||||||
import { DashCommand } from "./components/DashCommand"
|
import { DashCommand } from "./components/DashCommand"
|
||||||
import ErrorBoundary from "./components/ErrorBoundary"
|
import ErrorBoundary from "./components/ErrorBoundary"
|
||||||
import Footer from "./components/Footer"
|
import Footer from "./components/Footer"
|
||||||
import Header, { RefreshToast } from "./components/Header"
|
import Header, { RefreshToast } from "./components/Header"
|
||||||
import { useBackground } from "./hooks/use-background"
|
|
||||||
import { useTheme } from "./hooks/use-theme"
|
import { useTheme } from "./hooks/use-theme"
|
||||||
import { InjectContext } from "./lib/inject"
|
import { InjectContext } from "./lib/inject"
|
||||||
import { fetchSetting } from "./lib/nezha-api"
|
import { fetchSetting } from "./lib/nezha-api"
|
||||||
import { cn } from "./lib/utils"
|
|
||||||
import ErrorPage from "./pages/ErrorPage"
|
import ErrorPage from "./pages/ErrorPage"
|
||||||
import NotFound from "./pages/NotFound"
|
import NotFound from "./pages/NotFound"
|
||||||
import Server from "./pages/Server"
|
import Server from "./pages/Server"
|
||||||
@ -24,10 +21,8 @@ const App: React.FC = () => {
|
|||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
})
|
})
|
||||||
const { i18n } = useTranslation()
|
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false)
|
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false)
|
||||||
const { backgroundImage: customBackgroundImage } = useBackground()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settingData?.data?.config?.custom_code) {
|
if (settingData?.data?.config?.custom_code) {
|
||||||
@ -59,45 +54,38 @@ const App: React.FC = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingData?.data?.config?.language && !localStorage.getItem("language")) {
|
|
||||||
i18n.changeLanguage(settingData?.data?.config?.language)
|
|
||||||
}
|
|
||||||
|
|
||||||
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router basename={import.meta.env.BASE_URL}>
|
<Router basename={import.meta.env.BASE_URL}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{/* 固定定位的背景层 */}
|
<div className="relative min-h-screen">
|
||||||
{customBackgroundImage && (
|
{/* 固定定位的背景层 */}
|
||||||
<div
|
<div
|
||||||
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75", {
|
className="fixed inset-0 bg-cover bg-no-repeat bg-center dark:brightness-75"
|
||||||
"hidden sm:block": customMobileBackgroundImage,
|
style={{
|
||||||
})}
|
backgroundImage: `url(https://random-api.czl.net/pic/normal)`,
|
||||||
style={{ backgroundImage: `url(${customBackgroundImage})` }}
|
zIndex: -1
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
{/* 毛玻璃蒙版层 */}
|
||||||
{customMobileBackgroundImage && (
|
|
||||||
<div
|
<div
|
||||||
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75")}
|
className="fixed inset-0 backdrop-blur-sm bg-black/80"
|
||||||
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
|
style={{
|
||||||
|
zIndex: -1
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<div
|
<main className="relative flex min-h-screen flex-col gap-2 p-2 md:p-6 md:pt-4 bg-transparent">
|
||||||
className={cn("flex min-h-screen w-full flex-col", {
|
|
||||||
"bg-background": !customBackgroundImage,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<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 />
|
<RefreshToast />
|
||||||
<Header />
|
<Header />
|
||||||
<DashCommand />
|
<DashCommand />
|
||||||
<Routes>
|
<div className="flex-1">
|
||||||
<Route path="/" element={<Server />} />
|
<Routes>
|
||||||
<Route path="/server/:id" element={<ServerDetail />} />
|
<Route path="/" element={<Server />} />
|
||||||
<Route path="/error" element={<ErrorPage />} />
|
<Route path="/server/:id" element={<ServerDetail />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="/error" element={<ErrorPage />} />
|
||||||
</Routes>
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
49
src/components/DirectCountrySelect.tsx
Normal file
49
src/components/DirectCountrySelect.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import ServerFlag from "@/components/ServerFlag";
|
||||||
|
|
||||||
|
type DirectCountrySelectProps = {
|
||||||
|
countries: string[];
|
||||||
|
currentCountry: string;
|
||||||
|
onChange: (country: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 这是一个简单的直接选择组件,避免可能的事件传播问题
|
||||||
|
export default function DirectCountrySelect({
|
||||||
|
countries,
|
||||||
|
currentCountry,
|
||||||
|
onChange
|
||||||
|
}: DirectCountrySelectProps) {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex flex-wrap gap-1.5 pb-1">
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 text-xs rounded-md transition-all border",
|
||||||
|
currentCountry === "All"
|
||||||
|
? "bg-blue-500 text-white border-blue-600 hover:bg-blue-600 shadow-sm"
|
||||||
|
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
)}
|
||||||
|
onClick={() => onChange("All")}
|
||||||
|
>
|
||||||
|
ALL
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{countries.map((country) => (
|
||||||
|
<button
|
||||||
|
key={country}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 text-xs rounded-md flex items-center gap-1.5 transition-all border",
|
||||||
|
currentCountry === country
|
||||||
|
? "bg-blue-500 text-white border-blue-600 hover:bg-blue-600 shadow-sm"
|
||||||
|
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
)}
|
||||||
|
onClick={() => onChange(country)}
|
||||||
|
>
|
||||||
|
<ServerFlag country_code={country.toLowerCase()} className="text-[12px]" />
|
||||||
|
{country}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,61 +1,77 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { m } from "framer-motion"
|
import { m } from "framer-motion"
|
||||||
import { createRef, useEffect, useRef } from "react"
|
import { createRef, useEffect, useRef, useState } from "react"
|
||||||
|
import ServerFlag from "@/components/ServerFlag"
|
||||||
|
|
||||||
export default function GroupSwitch({
|
export default function GroupSwitch({
|
||||||
tabs,
|
tabs,
|
||||||
currentTab,
|
currentTab,
|
||||||
setCurrentTab,
|
setCurrentTab,
|
||||||
|
isCountrySwitch = false
|
||||||
}: {
|
}: {
|
||||||
tabs: string[]
|
tabs: string[]
|
||||||
currentTab: string
|
currentTab: string
|
||||||
setCurrentTab: (tab: string) => void
|
setCurrentTab: (tab: string) => void
|
||||||
|
isCountrySwitch?: boolean
|
||||||
}) {
|
}) {
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false)
|
||||||
|
|
||||||
|
// 使用一个唯一的ID来确保各个组件的layoutId不会冲突
|
||||||
|
const layoutIdPrefix = isCountrySwitch ? "country-switch-" : "tab-switch-"
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()))
|
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = scrollRef.current
|
// 检测暗黑模式
|
||||||
if (!container) return
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
|
|
||||||
const isOverflowing = container.scrollWidth > container.clientWidth
|
// 监听主题变化
|
||||||
if (!isOverflowing) return
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
const onWheel = (e: WheelEvent) => {
|
if (mutation.attributeName === 'class') {
|
||||||
e.preventDefault()
|
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
||||||
container.scrollLeft += e.deltaY
|
}
|
||||||
}
|
})
|
||||||
|
})
|
||||||
container.addEventListener("wheel", onWheel, { passive: false })
|
|
||||||
|
observer.observe(document.documentElement, { attributes: true })
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener("wheel", onWheel)
|
observer.disconnect()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
// 处理标签点击
|
||||||
const savedGroup = sessionStorage.getItem("selectedGroup")
|
function handleClick(tab: string) {
|
||||||
if (savedGroup && tabs.includes(savedGroup)) {
|
// 避免重复点击当前选中的标签
|
||||||
setCurrentTab(savedGroup)
|
if (tab === currentTab) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 直接调用父组件传递的回调
|
||||||
|
setCurrentTab(tab);
|
||||||
|
console.log(`[${isCountrySwitch ? '国家' : '分组'}] 切换到: ${tab}`);
|
||||||
|
|
||||||
|
// 手动滚动到可见区域
|
||||||
|
const index = tabs.indexOf(tab);
|
||||||
|
if (index !== -1 && tagRefs.current[index]?.current) {
|
||||||
|
tagRefs.current[index].current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "center"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换标签出错:', error);
|
||||||
}
|
}
|
||||||
}, [tabs, setCurrentTab])
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)]
|
|
||||||
|
|
||||||
if (currentTagRef && currentTagRef.current) {
|
|
||||||
currentTagRef.current.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "nearest",
|
|
||||||
inline: "center",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [currentTab])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={scrollRef} className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
|
<div className={cn(
|
||||||
|
"scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]",
|
||||||
|
isCountrySwitch ? "border-l border-stone-200 dark:border-stone-700 ml-1 pl-1" : ""
|
||||||
|
)} ref={scrollRef}>
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
||||||
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
||||||
@ -63,9 +79,9 @@ export default function GroupSwitch({
|
|||||||
>
|
>
|
||||||
{tabs.map((tab: string, index: number) => (
|
{tabs.map((tab: string, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={tab}
|
key={isCountrySwitch ? `country-${tab}` : `group-${tab}`}
|
||||||
ref={tagRefs.current[index]}
|
ref={tagRefs.current[index]}
|
||||||
onClick={() => setCurrentTab(tab)}
|
onClick={() => handleClick(tab)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
||||||
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
||||||
@ -73,15 +89,22 @@ export default function GroupSwitch({
|
|||||||
>
|
>
|
||||||
{currentTab === tab && (
|
{currentTab === tab && (
|
||||||
<m.div
|
<m.div
|
||||||
layoutId="tab-switch"
|
layoutId={`${layoutIdPrefix}${isCountrySwitch ? 'country-' : 'group-'}${tab}`}
|
||||||
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
|
||||||
style={{
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: isDarkMode ? "rgb(68 64 60)" : "white", // bg-stone-700 : white
|
||||||
|
boxShadow: isDarkMode ? "0 1px 3px 0 rgba(255, 255, 255, 0.05)" : "0 1px 3px 0 rgba(0, 0, 0, 0.05)",
|
||||||
originY: "0px",
|
originY: "0px",
|
||||||
borderRadius: 46,
|
borderRadius: 46,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="relative z-20 flex items-center gap-1">
|
<div className="relative z-20 flex items-center gap-1">
|
||||||
|
{isCountrySwitch && tab !== "All" && <ServerFlag country_code={tab.toLowerCase()} className="text-[10px]" />}
|
||||||
<p className="whitespace-nowrap">{tab}</p>
|
<p className="whitespace-nowrap">{tab}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ModeToggle } from "@/components/ThemeSwitcher"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { useBackground } from "@/hooks/use-background"
|
import { useBackground } from "@/hooks/use-background"
|
||||||
@ -7,14 +6,13 @@ import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
|
import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { AnimatePresence, m } from "framer-motion"
|
import { AnimatePresence } from "framer-motion"
|
||||||
import { ImageMinus } from "lucide-react"
|
import { ImageMinus } from "lucide-react"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
import { LanguageSwitcher } from "./LanguageSwitcher"
|
|
||||||
import { Loader, LoadingSpinner } from "./loading/Loader"
|
import { Loader, LoadingSpinner } from "./loading/Loader"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
|
|
||||||
@ -76,7 +74,7 @@ function Header() {
|
|||||||
const customBackgroundImage = backgroundImage
|
const customBackgroundImage = backgroundImage
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl">
|
<div className="mx-auto w-full max-w-7xl">
|
||||||
<section className="flex items-center justify-between header-top">
|
<section className="flex items-center justify-between header-top">
|
||||||
<section
|
<section
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -103,8 +101,6 @@ function Header() {
|
|||||||
<Links />
|
<Links />
|
||||||
<DashboardLink />
|
<DashboardLink />
|
||||||
</div>
|
</div>
|
||||||
<LanguageSwitcher />
|
|
||||||
<ModeToggle />
|
|
||||||
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && (
|
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -195,18 +191,14 @@ export function RefreshToast() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<m.div
|
<div
|
||||||
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
|
||||||
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
|
||||||
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
|
||||||
transition={{ type: "spring", duration: 0.8 }}
|
|
||||||
className="fixed left-1/2 -translate-x-1/2 top-8 z-[999] flex items-center justify-between gap-4 rounded-[50px] border-[1px] border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
|
className="fixed left-1/2 -translate-x-1/2 top-8 z-[999] flex items-center justify-between gap-4 rounded-[50px] border-[1px] border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
|
||||||
>
|
>
|
||||||
<section className="flex items-center gap-1.5">
|
<section className="flex items-center gap-1.5">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
|
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
|
||||||
</section>
|
</section>
|
||||||
</m.div>
|
</div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -285,7 +277,7 @@ function Overview() {
|
|||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [])
|
}, [])
|
||||||
return (
|
return (
|
||||||
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
|
<section className={"mt-6 flex flex-col md:mt-8 header-timer"}>
|
||||||
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
|
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
export function LanguageSwitcher() {
|
|
||||||
const { t, i18n } = useTranslation()
|
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
|
||||||
|
|
||||||
const locale = i18n.languages[0]
|
|
||||||
|
|
||||||
const handleSelect = (e: Event, newLocale: string) => {
|
|
||||||
e.preventDefault() // 阻止默认的关闭行为
|
|
||||||
i18n.changeLanguage(newLocale)
|
|
||||||
}
|
|
||||||
|
|
||||||
const localeItems = [
|
|
||||||
{ name: t("language.zh-CN"), code: "zh-CN" },
|
|
||||||
{ name: t("language.zh-TW"), code: "zh-TW" },
|
|
||||||
{ name: t("language.en-US"), code: "en-US" },
|
|
||||||
{ name: t("language.ru-RU"), code: "ru-RU" },
|
|
||||||
{ name: t("language.es-ES"), code: "es-ES" },
|
|
||||||
{ name: t("language.de-DE"), code: "de-DE" },
|
|
||||||
{ name: t("language.ta-IN"), code: "ta-IN" },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
|
||||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<LanguageIcon className="size-4" />
|
|
||||||
<span className="sr-only">Change language</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
|
||||||
{localeItems.map((item) => (
|
|
||||||
<DropdownMenuItem key={item.code} onSelect={(e) => handleSelect(e, item.code)} className={locale === item.code ? "bg-muted gap-3" : ""}>
|
|
||||||
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
@ -383,9 +383,6 @@ export default function ServerCard({ now, serverInfo, cycleStats, groupName }: S
|
|||||||
<Cpu className="size-[14px] mr-1 text-blue-500" />
|
<Cpu className="size-[14px] mr-1 text-blue-500" />
|
||||||
<span className="text-xs">CPU</span>
|
<span className="text-xs">CPU</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={cn("text-xs font-bold", getColorClass(cpu))}>
|
|
||||||
{cpu.toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<ServerUsageBar value={cpu} />
|
<ServerUsageBar value={cpu} />
|
||||||
{/* CPU信息 */}
|
{/* CPU信息 */}
|
||||||
@ -428,9 +425,6 @@ export default function ServerCard({ now, serverInfo, cycleStats, groupName }: S
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-xs">{t("serverCard.mem")}</span>
|
<span className="text-xs">{t("serverCard.mem")}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={cn("text-xs font-bold", getColorClass(mem))}>
|
|
||||||
{mem.toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<ServerUsageBar value={mem} />
|
<ServerUsageBar value={mem} />
|
||||||
{/* 内存信息 */}
|
{/* 内存信息 */}
|
||||||
@ -477,9 +471,6 @@ export default function ServerCard({ now, serverInfo, cycleStats, groupName }: S
|
|||||||
<HardDrive className="size-[14px] mr-1 text-amber-500" />
|
<HardDrive className="size-[14px] mr-1 text-amber-500" />
|
||||||
<span className="text-xs">{t("serverCard.stg")}</span>
|
<span className="text-xs">{t("serverCard.stg")}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={cn("text-xs font-bold", getColorClass(stg))}>
|
|
||||||
{stg.toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<ServerUsageBar value={stg} />
|
<ServerUsageBar value={stg} />
|
||||||
{/* 存储信息 */}
|
{/* 存储信息 */}
|
||||||
@ -496,14 +487,14 @@ export default function ServerCard({ now, serverInfo, cycleStats, groupName }: S
|
|||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ArrowUp className="size-[14px] text-blue-500 mr-1" />
|
<ArrowUp className="size-[14px] text-blue-500 mr-1" />
|
||||||
<span className="text-xs">{t("serverCard.upload")}</span>
|
<span className="text-xs">Up</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium">{formatSpeed(up)}</span>
|
<span className="text-xs font-medium">{formatSpeed(up)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ArrowDown className="size-[14px] text-green-500 mr-1" />
|
<ArrowDown className="size-[14px] text-green-500 mr-1" />
|
||||||
<span className="text-xs">{t("serverCard.download")}</span>
|
<span className="text-xs">Down</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium">{formatSpeed(down)}</span>
|
<span className="text-xs font-medium">{formatSpeed(down)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useTheme } from "@/hooks/use-theme"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
|
|
||||||
export function ThemeColorManager() {
|
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateThemeColor = () => {
|
|
||||||
const currentTheme = theme
|
|
||||||
const meta = document.querySelector('meta[name="theme-color"]')
|
|
||||||
|
|
||||||
if (!meta) {
|
|
||||||
const newMeta = document.createElement("meta")
|
|
||||||
newMeta.name = "theme-color"
|
|
||||||
document.head.appendChild(newMeta)
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeColor =
|
|
||||||
currentTheme === "dark"
|
|
||||||
? "hsl(30 15% 8%)" // 深色模式背景色
|
|
||||||
: "hsl(0 0% 98%)" // 浅色模式背景色
|
|
||||||
|
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update on mount and theme change
|
|
||||||
updateThemeColor()
|
|
||||||
|
|
||||||
// Listen for system theme changes
|
|
||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
|
||||||
mediaQuery.addEventListener("change", updateThemeColor)
|
|
||||||
|
|
||||||
return () => mediaQuery.removeEventListener("change", updateThemeColor)
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
@ -1,11 +1,9 @@
|
|||||||
import { ReactNode, createContext, useEffect, useState } from "react"
|
import { ReactNode, createContext } from "react"
|
||||||
|
|
||||||
export type Theme = "dark" | "light" | "system"
|
export type Theme = "dark"
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
type ThemeProviderProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
defaultTheme?: Theme
|
|
||||||
storageKey?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeProviderState = {
|
type ThemeProviderState = {
|
||||||
@ -14,40 +12,22 @@ type ThemeProviderState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ThemeProviderState = {
|
const initialState: ThemeProviderState = {
|
||||||
theme: "system",
|
theme: "dark",
|
||||||
setTheme: () => null,
|
setTheme: () => null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||||
|
|
||||||
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
|
export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||||
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || "system")
|
const root = window.document.documentElement
|
||||||
|
root.classList.remove("light")
|
||||||
|
root.classList.add("dark")
|
||||||
|
const themeColor = "hsl(30 15% 8%)"
|
||||||
|
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||||
|
|
||||||
useEffect(() => {
|
const value: ThemeProviderState = {
|
||||||
const root = window.document.documentElement
|
theme: "dark",
|
||||||
|
setTheme: () => null,
|
||||||
root.classList.remove("light", "dark")
|
|
||||||
|
|
||||||
if (theme === "system") {
|
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
||||||
|
|
||||||
root.classList.add(systemTheme)
|
|
||||||
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.classList.add(theme)
|
|
||||||
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
theme,
|
|
||||||
setTheme: (theme: Theme) => {
|
|
||||||
localStorage.setItem(storageKey, theme)
|
|
||||||
setTheme(theme)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
|
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import { Theme } from "@/components/ThemeProvider"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
|
||||||
import { Moon, Sun } from "lucide-react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { useTheme } from "../hooks/use-theme"
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { setTheme, theme } = useTheme()
|
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
|
||||||
|
|
||||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setTheme(newTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
|
||||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
||||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
||||||
<span className="sr-only">Toggle theme</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
|
||||||
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "light" })} onSelect={(e) => handleSelect(e, "light")}>
|
|
||||||
{t("theme.light")}
|
|
||||||
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "dark" })} onSelect={(e) => handleSelect(e, "dark")}>
|
|
||||||
{t("theme.dark")}
|
|
||||||
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "system" })} onSelect={(e) => handleSelect(e, "system")}>
|
|
||||||
{t("theme.system")}
|
|
||||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
23
src/i18n.js
23
src/i18n.js
@ -2,37 +2,20 @@ import i18n from "i18next"
|
|||||||
import { initReactI18next } from "react-i18next"
|
import { initReactI18next } from "react-i18next"
|
||||||
|
|
||||||
import enTranslation from "./locales/en/translation.json"
|
import enTranslation from "./locales/en/translation.json"
|
||||||
import zhCNTranslation from "./locales/zh-CN/translation.json"
|
|
||||||
import zhTWTranslation from "./locales/zh-TW/translation.json"
|
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
"en-US": {
|
"en-US": {
|
||||||
translation: enTranslation,
|
translation: enTranslation,
|
||||||
},
|
},
|
||||||
"zh-CN": {
|
|
||||||
translation: zhCNTranslation,
|
|
||||||
},
|
|
||||||
"zh-TW": {
|
|
||||||
translation: zhTWTranslation,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStoredLanguage = () => {
|
|
||||||
return localStorage.getItem("language") || "en-US"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
resources,
|
resources,
|
||||||
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
|
lng: "en-US",
|
||||||
fallbackLng: "en-US", // 当前语言的翻译没有找到时,使用的备选语言
|
fallbackLng: "en-US",
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // react已经安全地转义
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加语言改变时的处理函数
|
|
||||||
i18n.on("languageChanged", (lng) => {
|
|
||||||
localStorage.setItem("language", lng)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Noto Sans SC', system-ui, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
@ -92,7 +92,8 @@
|
|||||||
@apply scroll-smooth;
|
@apply scroll-smooth;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply text-foreground;
|
||||||
|
background: transparent !important;
|
||||||
/* font-feature-settings: "rlig" 1, "calt" 1; */
|
/* font-feature-settings: "rlig" 1, "calt" 1; */
|
||||||
font-synthesis-weight: none;
|
font-synthesis-weight: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
{
|
|
||||||
"refreshing": "Aktualisieren",
|
|
||||||
"serviceTracker": {
|
|
||||||
"uptime": "Uptime",
|
|
||||||
"today": "Heute",
|
|
||||||
"noService": "Keine Servicedaten",
|
|
||||||
"daysAgo": "vor Tagen",
|
|
||||||
"loading": "Laden..."
|
|
||||||
},
|
|
||||||
"serverCard": {
|
|
||||||
"uptime": "Uptime",
|
|
||||||
"mem": "MEM",
|
|
||||||
"upload": "Upload",
|
|
||||||
"download": "Download",
|
|
||||||
"system": "System",
|
|
||||||
"stg": "STG",
|
|
||||||
"totalDownload": "Download",
|
|
||||||
"days": "Tage",
|
|
||||||
"hours": "Stunden",
|
|
||||||
"totalUpload": "Upload"
|
|
||||||
},
|
|
||||||
"serverDetail": {
|
|
||||||
"unknown": "Unbekannt",
|
|
||||||
"arch": "Arch",
|
|
||||||
"status": "Status",
|
|
||||||
"online": "Online",
|
|
||||||
"days": "Tage",
|
|
||||||
"upload": "Upload",
|
|
||||||
"download": "Download",
|
|
||||||
"offline": "Offline",
|
|
||||||
"uptime": "Uptime",
|
|
||||||
"version": "Version",
|
|
||||||
"mem": "Speicher",
|
|
||||||
"disk": "Festplatte",
|
|
||||||
"region": "Region",
|
|
||||||
"system": "System",
|
|
||||||
"lastActive": "Letzte Aktivität",
|
|
||||||
"temperature": "Temperatur"
|
|
||||||
},
|
|
||||||
"theme": {
|
|
||||||
"system": "Folgen Sie dem System",
|
|
||||||
"light": "Hell",
|
|
||||||
"dark": "Dunkel"
|
|
||||||
},
|
|
||||||
"monitor": {
|
|
||||||
"monitorCount": "Services",
|
|
||||||
"noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu",
|
|
||||||
"avgDelay": "Latenz"
|
|
||||||
},
|
|
||||||
"billingInfo": {
|
|
||||||
"error": "Fehler",
|
|
||||||
"remaining": "Verbleibend",
|
|
||||||
"indefinite": "Unbestimmt",
|
|
||||||
"expired": "Verfallen",
|
|
||||||
"days": "tage",
|
|
||||||
"price": "Preis",
|
|
||||||
"free": "Kostenlos",
|
|
||||||
"usage-baseed": "Verwendungsbasiert"
|
|
||||||
},
|
|
||||||
"overview": "Überblick",
|
|
||||||
"map": {
|
|
||||||
"Regions": "Regionen",
|
|
||||||
"Servers": "server",
|
|
||||||
"Distributions": "Server sind verteilt in"
|
|
||||||
},
|
|
||||||
"pwa": {
|
|
||||||
"reload": "Update",
|
|
||||||
"newContent": "Neue Inhalte verfügbar",
|
|
||||||
"offlineReady": "Anwendung bereit, offline zu verwenden"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"pageNotFound": "Seite nicht gefunden",
|
|
||||||
"backToHome": "Zurück zur Startseite"
|
|
||||||
},
|
|
||||||
"whereTheTimeIs": "Wo die Zeit ist",
|
|
||||||
"info": {
|
|
||||||
"websocketConnecting": "WebSocket verbindet",
|
|
||||||
"websocketConnected": "WebSocket verbunden",
|
|
||||||
"websocketDisconnected": "WebSocket getrennt",
|
|
||||||
"processing": "Verarbeiten..."
|
|
||||||
},
|
|
||||||
"tabSwitch": {
|
|
||||||
"Network": "Netzwerk",
|
|
||||||
"Detail": "Detail"
|
|
||||||
},
|
|
||||||
"nezha": "Nezha Monitoring",
|
|
||||||
"dashboard": "Dashboard",
|
|
||||||
"serverDetailChart": {
|
|
||||||
"upload": "Upload",
|
|
||||||
"download": "Download",
|
|
||||||
"process": "Prozess",
|
|
||||||
"disk": "Festplatte",
|
|
||||||
"mem": "Speicher",
|
|
||||||
"swap": "Swap"
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"zh-TW": "Traditionelles Chinesisch",
|
|
||||||
"en-US": "Englisch",
|
|
||||||
"zh-CN": "vereinfachtes Chinesisch"
|
|
||||||
},
|
|
||||||
"online": "Online",
|
|
||||||
"offline": "Offline",
|
|
||||||
"serverOverview": {
|
|
||||||
"totalServers": "Server insgesamt",
|
|
||||||
"onlineServers": "Online Server",
|
|
||||||
"offlineServers": "Offline Server",
|
|
||||||
"totalBandwidth": "Gesamte Bandbreite",
|
|
||||||
"speed": "Geschwindigkeit",
|
|
||||||
"network": "Netzwerk"
|
|
||||||
},
|
|
||||||
"cycleTransfer": {
|
|
||||||
"used": "benutzt",
|
|
||||||
"total": "gesamt",
|
|
||||||
"nextUpdate": "nächstes update"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"themeBy": "Design von "
|
|
||||||
},
|
|
||||||
"login": "Login"
|
|
||||||
}
|
|
@ -1,131 +0,0 @@
|
|||||||
{
|
|
||||||
"serviceTracker": {
|
|
||||||
"delay": "Latencia",
|
|
||||||
"noService": "No hay datos de servicio",
|
|
||||||
"uptime": "Tiempo de actividad",
|
|
||||||
"daysAgo": "días atrás",
|
|
||||||
"today": "Hoy",
|
|
||||||
"loading": "Cargando..."
|
|
||||||
},
|
|
||||||
"serverDetail": {
|
|
||||||
"disk": "Disco",
|
|
||||||
"region": "Región",
|
|
||||||
"system": "Sistema Operativo",
|
|
||||||
"lastActive": "Última vez activo",
|
|
||||||
"temperature": "Temperatura",
|
|
||||||
"bootTime": "Inicio del sistema",
|
|
||||||
"arch": "Arch",
|
|
||||||
"status": "Estado",
|
|
||||||
"online": "En línea",
|
|
||||||
"version": "Versión",
|
|
||||||
"offline": "Fuera de línea",
|
|
||||||
"unknown": "Desconocido",
|
|
||||||
"days": "Días",
|
|
||||||
"hours": "Horas",
|
|
||||||
"download": "Bajada",
|
|
||||||
"uptime": "Tiempo de actividad",
|
|
||||||
"mem": "Memoria",
|
|
||||||
"upload": "Subida"
|
|
||||||
},
|
|
||||||
"serverDetailChart": {
|
|
||||||
"process": "Procesos",
|
|
||||||
"disk": "Disco",
|
|
||||||
"mem": "Memoria",
|
|
||||||
"swap": "Swap",
|
|
||||||
"upload": "Subida",
|
|
||||||
"download": "Bajada"
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"en-US": "Inglés",
|
|
||||||
"zh-TW": "Chino Tradicional",
|
|
||||||
"zh-CN": "Chino simplificado"
|
|
||||||
},
|
|
||||||
"TypeCommand": "Escriba un comando o busca...",
|
|
||||||
"Shortcuts": "Atajos",
|
|
||||||
"Home": "Inicio",
|
|
||||||
"login": "Iniciar sesión",
|
|
||||||
"online": "En línea",
|
|
||||||
"offline": "Fuerda de línea",
|
|
||||||
"whereTheTimeIs": "Hora actual",
|
|
||||||
"serverOverview": {
|
|
||||||
"totalBandwidth": "Ancho de banda total",
|
|
||||||
"speed": "Velocidad",
|
|
||||||
"network": "Red",
|
|
||||||
"onlineServers": "Servidores en línea",
|
|
||||||
"totalServers": "Total de Servidores",
|
|
||||||
"offlineServers": "Servidores fuera de línea"
|
|
||||||
},
|
|
||||||
"map": {
|
|
||||||
"Regions": "Regiones",
|
|
||||||
"Servers": "Servidores",
|
|
||||||
"Distributions": "Servidores distribuidos en"
|
|
||||||
},
|
|
||||||
"overview": "Descripción general",
|
|
||||||
"dashboard": "Panel",
|
|
||||||
"nezha": "Monitoreo Nezha",
|
|
||||||
"serverCard": {
|
|
||||||
"mem": "Ram",
|
|
||||||
"days": "Días",
|
|
||||||
"hours": "Horas",
|
|
||||||
"upload": "Subida",
|
|
||||||
"download": "Bajada",
|
|
||||||
"system": "Sistema operativo",
|
|
||||||
"uptime": "Tiempo de actividad",
|
|
||||||
"totalUpload": "Subida",
|
|
||||||
"totalDownload": "Bajada",
|
|
||||||
"stg": "Almacenamiento"
|
|
||||||
},
|
|
||||||
"cycleTransfer": {
|
|
||||||
"used": "Usado",
|
|
||||||
"total": "total",
|
|
||||||
"nextUpdate": "próxima actualización"
|
|
||||||
},
|
|
||||||
"tabSwitch": {
|
|
||||||
"Detail": "Detalle",
|
|
||||||
"Network": "Red"
|
|
||||||
},
|
|
||||||
"monitor": {
|
|
||||||
"avgDelay": "Latencia",
|
|
||||||
"noData": "No hay datos de servidores, primero agregue un monitor de servicio",
|
|
||||||
"monitorCount": "Servicios"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"pageNotFound": "Página no encontrada",
|
|
||||||
"backToHome": "Volver al Inicio"
|
|
||||||
},
|
|
||||||
"theme": {
|
|
||||||
"system": "Sistema",
|
|
||||||
"dark": "Oscuro",
|
|
||||||
"light": "Claro"
|
|
||||||
},
|
|
||||||
"billingInfo": {
|
|
||||||
"remaining": "Restante",
|
|
||||||
"error": "error",
|
|
||||||
"days": "días",
|
|
||||||
"price": "Precio",
|
|
||||||
"free": "Gratis",
|
|
||||||
"indefinite": "Indedinido",
|
|
||||||
"expired": "Expirado",
|
|
||||||
"usage-baseed": "Basado en el uso"
|
|
||||||
},
|
|
||||||
"pwa": {
|
|
||||||
"offlineReady": "Aplicacion lista para trabajar fuera de línea",
|
|
||||||
"newContent": "Nuevo contenido disponible",
|
|
||||||
"reload": "Actualizar"
|
|
||||||
},
|
|
||||||
"info": {
|
|
||||||
"websocketConnecting": "Conexión WebSocket",
|
|
||||||
"websocketDisconnected": "WebSocket desconectado",
|
|
||||||
"websocketConnected": "WebSocket conectado",
|
|
||||||
"processing": "Procesando..."
|
|
||||||
},
|
|
||||||
"NoResults": "No se encontraron resultados.",
|
|
||||||
"refreshing": "Actualizando",
|
|
||||||
"Servers": "Servidores",
|
|
||||||
"ToggleLightMode": "Activar el modo claro",
|
|
||||||
"ToggleDarkMode": "Activar el modo oscuro",
|
|
||||||
"ToggleSystemMode": "Activar modo del sistema",
|
|
||||||
"footer": {
|
|
||||||
"themeBy": "Tema por. "
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
{
|
|
||||||
"map": {
|
|
||||||
"Servers": "сервера",
|
|
||||||
"Distributions": "Серверы распределены в",
|
|
||||||
"Regions": "Регионы"
|
|
||||||
},
|
|
||||||
"serverDetailChart": {
|
|
||||||
"disk": "Диск",
|
|
||||||
"download": "Скачивание",
|
|
||||||
"swap": "Swap",
|
|
||||||
"upload": "Загрузка",
|
|
||||||
"mem": "Mem",
|
|
||||||
"process": "Процесс"
|
|
||||||
},
|
|
||||||
"serverCard": {
|
|
||||||
"system": "Система",
|
|
||||||
"hours": "Часов",
|
|
||||||
"uptime": "Аптайм",
|
|
||||||
"download": "Скачивание",
|
|
||||||
"mem": "MEM",
|
|
||||||
"stg": "STG",
|
|
||||||
"upload": "Загрузка",
|
|
||||||
"totalUpload": "Загружено",
|
|
||||||
"totalDownload": "Скачано",
|
|
||||||
"days": "Дней"
|
|
||||||
},
|
|
||||||
"tabSwitch": {
|
|
||||||
"Detail": "Детали",
|
|
||||||
"Network": "Сеть"
|
|
||||||
},
|
|
||||||
"whereTheTimeIs": "Где время",
|
|
||||||
"theme": {
|
|
||||||
"dark": "Темная тема",
|
|
||||||
"light": "Светлая тема",
|
|
||||||
"system": "Как в Системе"
|
|
||||||
},
|
|
||||||
"login": "Логин",
|
|
||||||
"language": {
|
|
||||||
"zh-TW": "Традиционный китайский",
|
|
||||||
"zh-CN": "Упрощенный китайский",
|
|
||||||
"en-US": "Английский",
|
|
||||||
"de-DE": "Немецкий",
|
|
||||||
"ta-IN": "Тамильский",
|
|
||||||
"ru-RU": "Русский",
|
|
||||||
"es-ES": "Испанский"
|
|
||||||
},
|
|
||||||
"overview": "Обзор",
|
|
||||||
"info": {
|
|
||||||
"websocketConnecting": "WebSocket подключение",
|
|
||||||
"websocketConnected": "WebSocket подключен",
|
|
||||||
"websocketDisconnected": "WebSocket отключен",
|
|
||||||
"processing": "Обработка..."
|
|
||||||
},
|
|
||||||
"cycleTransfer": {
|
|
||||||
"nextUpdate": "следующее обновление",
|
|
||||||
"used": "использовано",
|
|
||||||
"total": "всего"
|
|
||||||
},
|
|
||||||
"dashboard": "Панель",
|
|
||||||
"online": "Онлайн",
|
|
||||||
"refreshing": "Обновление",
|
|
||||||
"serverOverview": {
|
|
||||||
"totalServers": "Всего Серверов",
|
|
||||||
"totalBandwidth": "Общая пропускная способность",
|
|
||||||
"network": "Сеть",
|
|
||||||
"speed": "Скорость",
|
|
||||||
"onlineServers": "Серверы в сети",
|
|
||||||
"offlineServers": "Серверы не в сети"
|
|
||||||
},
|
|
||||||
"serviceTracker": {
|
|
||||||
"noService": "Нет данных о сервисе",
|
|
||||||
"delay": "Задержка",
|
|
||||||
"daysAgo": "Дней назад",
|
|
||||||
"today": "Сегодня",
|
|
||||||
"uptime": "Аптайм",
|
|
||||||
"loading": "Загрузка..."
|
|
||||||
},
|
|
||||||
"serverDetail": {
|
|
||||||
"status": "Статус",
|
|
||||||
"days": "Дней",
|
|
||||||
"hours": "Часов",
|
|
||||||
"offline": "Оффлайн",
|
|
||||||
"uptime": "Аптайм",
|
|
||||||
"arch": "Arch",
|
|
||||||
"mem": "Mem",
|
|
||||||
"disk": "Диск",
|
|
||||||
"system": "Система",
|
|
||||||
"lastActive": "Время последней активности",
|
|
||||||
"download": "Скачивание",
|
|
||||||
"unknown": "Неизвестно",
|
|
||||||
"version": "Версия",
|
|
||||||
"online": "В сети",
|
|
||||||
"region": "Регион",
|
|
||||||
"bootTime": "Время загрузки",
|
|
||||||
"upload": "Загрузка",
|
|
||||||
"temperature": "Температура"
|
|
||||||
},
|
|
||||||
"monitor": {
|
|
||||||
"noData": "Нет данных мониторинга сервера, пожалуйста, сначала добавьте монитор службы",
|
|
||||||
"avgDelay": "Задержка",
|
|
||||||
"monitorCount": "Сервисы"
|
|
||||||
},
|
|
||||||
"pwa": {
|
|
||||||
"newContent": "Доступен новый контент",
|
|
||||||
"reload": "Обновить",
|
|
||||||
"offlineReady": "Приложение готово работать в офлайн-режиме"
|
|
||||||
},
|
|
||||||
"billingInfo": {
|
|
||||||
"remaining": "Осталось",
|
|
||||||
"error": "ошибка",
|
|
||||||
"indefinite": "Неопределено",
|
|
||||||
"expired": "Истекло",
|
|
||||||
"price": "Цена",
|
|
||||||
"free": "Бесплатно",
|
|
||||||
"days": "дней",
|
|
||||||
"usage-baseed": "Оплата по использованию"
|
|
||||||
},
|
|
||||||
"TypeCommand": "Введите команду или выполните поиск...",
|
|
||||||
"Servers": "Серверы",
|
|
||||||
"ToggleLightMode": "Переключить на светлую тему",
|
|
||||||
"Home": "Главная",
|
|
||||||
"offline": "Оффлайн",
|
|
||||||
"error": {
|
|
||||||
"pageNotFound": "Страница не найдена",
|
|
||||||
"backToHome": "Вернуться на главную"
|
|
||||||
},
|
|
||||||
"ToggleDarkMode": "Переключить на темную тему",
|
|
||||||
"Shortcuts": "Горячие клавиши",
|
|
||||||
"ToggleSystemMode": "Использовать системную тему",
|
|
||||||
"footer": {
|
|
||||||
"themeBy": "Тема от "
|
|
||||||
},
|
|
||||||
"NoResults": "Ничего не найдено.",
|
|
||||||
"nezha": "Nezha Monitoring"
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
{
|
|
||||||
"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": "ஏற்றுகிறது ..."
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"nezha": "服务器监控",
|
|
||||||
"overview": "概览",
|
|
||||||
"dashboard": "管理后台",
|
|
||||||
"login": "登录",
|
|
||||||
"online": "在线",
|
|
||||||
"offline": "离线",
|
|
||||||
"whereTheTimeIs": "当前时间",
|
|
||||||
"refreshing": "刷新中",
|
|
||||||
"info": {
|
|
||||||
"websocketConnecting": "WebSocket 连接中",
|
|
||||||
"websocketConnected": "WebSocket 连接成功",
|
|
||||||
"websocketDisconnected": "WebSocket 连接断开",
|
|
||||||
"processing": "处理中..."
|
|
||||||
},
|
|
||||||
"serverOverview": {
|
|
||||||
"totalServers": "服务器总数",
|
|
||||||
"onlineServers": "在线服务器",
|
|
||||||
"offlineServers": "离线服务器",
|
|
||||||
"totalBandwidth": "总流量",
|
|
||||||
"speed": "速率",
|
|
||||||
"network": "网络"
|
|
||||||
},
|
|
||||||
"map": {
|
|
||||||
"Distributions": "服务器分布在",
|
|
||||||
"Regions": "个区域",
|
|
||||||
"Servers": "个服务器"
|
|
||||||
},
|
|
||||||
"serverCard": {
|
|
||||||
"mem": "内存",
|
|
||||||
"stg": "存储",
|
|
||||||
"days": "天",
|
|
||||||
"hours": "小时",
|
|
||||||
"upload": "上传",
|
|
||||||
"download": "下载",
|
|
||||||
"system": "系统",
|
|
||||||
"uptime": "运行时间",
|
|
||||||
"totalUpload": "总上传",
|
|
||||||
"totalDownload": "总下载",
|
|
||||||
"cpu": "处理器",
|
|
||||||
"vcpu": "虚拟核心",
|
|
||||||
"tcp": "TCP连接",
|
|
||||||
"udp": "UDP连接",
|
|
||||||
"process": "进程数"
|
|
||||||
},
|
|
||||||
"cycleTransfer": {
|
|
||||||
"used": "已使用",
|
|
||||||
"total": "总计",
|
|
||||||
"nextUpdate": "下次更新"
|
|
||||||
},
|
|
||||||
"serviceTracker": {
|
|
||||||
"noService": "无服务数据",
|
|
||||||
"uptime": "在线率",
|
|
||||||
"delay": "延迟",
|
|
||||||
"daysAgo": "天前",
|
|
||||||
"today": "今天",
|
|
||||||
"loading": "加载中..."
|
|
||||||
},
|
|
||||||
"serverDetail": {
|
|
||||||
"status": "状态",
|
|
||||||
"online": "在线",
|
|
||||||
"days": "天",
|
|
||||||
"hours": "小时",
|
|
||||||
"offline": "离线",
|
|
||||||
"unknown": "未知",
|
|
||||||
"uptime": "运行时间",
|
|
||||||
"version": "版本",
|
|
||||||
"arch": "架构",
|
|
||||||
"mem": "内存",
|
|
||||||
"disk": "磁盘",
|
|
||||||
"region": "区域",
|
|
||||||
"system": "系统",
|
|
||||||
"upload": "上传",
|
|
||||||
"download": "下载",
|
|
||||||
"lastActive": "最后上报时间",
|
|
||||||
"temperature": "温度",
|
|
||||||
"bootTime": "启动时间"
|
|
||||||
},
|
|
||||||
"serverDetailChart": {
|
|
||||||
"process": "进程数",
|
|
||||||
"disk": "磁盘",
|
|
||||||
"mem": "内存",
|
|
||||||
"swap": "虚拟内存",
|
|
||||||
"upload": "上传",
|
|
||||||
"download": "下载"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"themeBy": "主题-"
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"zh-CN": "简体中文",
|
|
||||||
"zh-TW": "繁體中文",
|
|
||||||
"en": "English"
|
|
||||||
},
|
|
||||||
"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": "按量计费"
|
|
||||||
},
|
|
||||||
"TypeCommand": "输入命令或搜索",
|
|
||||||
"NoResults": "结果为空",
|
|
||||||
"Servers": "服务器",
|
|
||||||
"Shortcuts": "快捷键",
|
|
||||||
"ToggleLightMode": "切换亮色模式",
|
|
||||||
"ToggleDarkMode": "切换暗色模式",
|
|
||||||
"ToggleSystemMode": "切换系统模式",
|
|
||||||
"Home": "首页"
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
{
|
|
||||||
"nezha": "服務器監控",
|
|
||||||
"overview": "概覽",
|
|
||||||
"dashboard": "管理後台",
|
|
||||||
"login": "登錄",
|
|
||||||
"online": "在線",
|
|
||||||
"offline": "離線",
|
|
||||||
"whereTheTimeIs": "目前時間",
|
|
||||||
"refreshing": "刷新中",
|
|
||||||
"info": {
|
|
||||||
"websocketConnecting": "WebSocket 連接中",
|
|
||||||
"websocketConnected": "WebSocket 連接成功",
|
|
||||||
"websocketDisconnected": "WebSocket 連接斷開",
|
|
||||||
"processing": "處理中..."
|
|
||||||
},
|
|
||||||
"serverOverview": {
|
|
||||||
"totalServers": "總服務器",
|
|
||||||
"onlineServers": "線上服務器",
|
|
||||||
"offlineServers": "離線服務器",
|
|
||||||
"totalBandwidth": "總帶寬",
|
|
||||||
"speed": "速率",
|
|
||||||
"network": "網路"
|
|
||||||
},
|
|
||||||
"map": {
|
|
||||||
"Distributions": "服務器分布在",
|
|
||||||
"Regions": "個區域",
|
|
||||||
"Servers": "個服務器"
|
|
||||||
},
|
|
||||||
"serverCard": {
|
|
||||||
"mem": "內存",
|
|
||||||
"stg": "存儲",
|
|
||||||
"days": "天",
|
|
||||||
"hours": "小時",
|
|
||||||
"upload": "上傳",
|
|
||||||
"download": "下載",
|
|
||||||
"system": "系統",
|
|
||||||
"uptime": "運行時間",
|
|
||||||
"totalUpload": "總上傳",
|
|
||||||
"totalDownload": "總下載"
|
|
||||||
},
|
|
||||||
"cycleTransfer": {
|
|
||||||
"used": "已使用",
|
|
||||||
"total": "總量",
|
|
||||||
"nextUpdate": "下次更新"
|
|
||||||
},
|
|
||||||
"serviceTracker": {
|
|
||||||
"noService": "無服務數據",
|
|
||||||
"uptime": "在線率",
|
|
||||||
"delay": "延遲",
|
|
||||||
"daysAgo": "天前",
|
|
||||||
"today": "今天",
|
|
||||||
"loading": "載入中..."
|
|
||||||
},
|
|
||||||
"serverDetail": {
|
|
||||||
"status": "狀態",
|
|
||||||
"online": "線上",
|
|
||||||
"days": "天",
|
|
||||||
"hours": "小時",
|
|
||||||
"offline": "離線",
|
|
||||||
"unknown": "未知",
|
|
||||||
"uptime": "運行時間",
|
|
||||||
"version": "版本",
|
|
||||||
"arch": "架構",
|
|
||||||
"mem": "內存",
|
|
||||||
"disk": "磁盤",
|
|
||||||
"region": "地區",
|
|
||||||
"system": "系統",
|
|
||||||
"upload": "上傳",
|
|
||||||
"download": "下載",
|
|
||||||
"lastActive": "最後上報時間",
|
|
||||||
"temperature": "溫度",
|
|
||||||
"bootTime": "啟動時間"
|
|
||||||
},
|
|
||||||
"serverDetailChart": {
|
|
||||||
"process": "進程數",
|
|
||||||
"disk": "磁盤",
|
|
||||||
"mem": "內存",
|
|
||||||
"swap": "虛擬記憶體",
|
|
||||||
"upload": "上傳",
|
|
||||||
"download": "下載"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"themeBy": "主題-"
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"zh-CN": "简体中文",
|
|
||||||
"zh-TW": "繁體中文",
|
|
||||||
"en-US": "English"
|
|
||||||
},
|
|
||||||
"theme": {
|
|
||||||
"light": "亮色",
|
|
||||||
"dark": "暗色",
|
|
||||||
"system": "跟隨系統"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"pageNotFound": "頁面不存在",
|
|
||||||
"backToHome": "回到主頁"
|
|
||||||
},
|
|
||||||
"tabSwitch": {
|
|
||||||
"detail": "詳細資訊",
|
|
||||||
"network": "網路"
|
|
||||||
},
|
|
||||||
"monitor": {
|
|
||||||
"noData": "沒有服務監控數據,請在管理後台服務新增監控任務",
|
|
||||||
"status": "狀態",
|
|
||||||
"avgDelay": "延遲",
|
|
||||||
"monitorCount": "個監控"
|
|
||||||
},
|
|
||||||
"billingInfo": {
|
|
||||||
"remaining": "剩餘天數",
|
|
||||||
"error": "獲取失敗",
|
|
||||||
"indefinite": "無限期",
|
|
||||||
"expired": "已過期",
|
|
||||||
"days": "天",
|
|
||||||
"price": "價格",
|
|
||||||
"free": "免費",
|
|
||||||
"usage-baseed": "按量計費"
|
|
||||||
},
|
|
||||||
"TypeCommand": "輸入命令或搜尋",
|
|
||||||
"NoResults": "沒有結果",
|
|
||||||
"Servers": "伺服器",
|
|
||||||
"Shortcuts": "快捷鍵",
|
|
||||||
"ToggleLightMode": "切換亮色模式",
|
|
||||||
"ToggleDarkMode": "切換暗色模式",
|
|
||||||
"ToggleSystemMode": "切換系統模式",
|
|
||||||
"Home": "首頁"
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ import ReactDOM from "react-dom/client"
|
|||||||
import { Toaster } from "sonner"
|
import { Toaster } from "sonner"
|
||||||
|
|
||||||
import App from "./App"
|
import App from "./App"
|
||||||
import { ThemeColorManager } from "./components/ThemeColorManager"
|
|
||||||
import { ThemeProvider } from "./components/ThemeProvider"
|
import { ThemeProvider } from "./components/ThemeProvider"
|
||||||
import { MotionProvider } from "./components/motion/motion-provider"
|
import { MotionProvider } from "./components/motion/motion-provider"
|
||||||
import { SortProvider } from "./context/sort-provider"
|
import { SortProvider } from "./context/sort-provider"
|
||||||
@ -18,8 +17,7 @@ const queryClient = new QueryClient()
|
|||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<MotionProvider>
|
<MotionProvider>
|
||||||
<ThemeProvider storageKey="vite-ui-theme">
|
<ThemeProvider>
|
||||||
<ThemeColorManager />
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<WebSocketProvider url="/api/v1/ws/server">
|
<WebSocketProvider url="/api/v1/ws/server">
|
||||||
<StatusProvider>
|
<StatusProvider>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import GlobalMap from "@/components/GlobalMap"
|
import GlobalMap from "@/components/GlobalMap"
|
||||||
import GroupSwitch from "@/components/GroupSwitch"
|
|
||||||
import ServerCard from "@/components/ServerCard"
|
import ServerCard from "@/components/ServerCard"
|
||||||
import ServerOverview from "@/components/ServerOverview"
|
import ServerOverview from "@/components/ServerOverview"
|
||||||
import { ServiceTracker } from "@/components/ServiceTracker"
|
import { ServiceTracker } from "@/components/ServiceTracker"
|
||||||
@ -17,8 +16,9 @@ import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
|||||||
import { ServerGroup } from "@/types/nezha-api"
|
import { ServerGroup } from "@/types/nezha-api"
|
||||||
import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon } from "@heroicons/react/20/solid"
|
import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon } from "@heroicons/react/20/solid"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import DirectCountrySelect from "@/components/DirectCountrySelect"
|
||||||
|
|
||||||
export default function Servers() {
|
export default function Servers() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -33,7 +33,15 @@ export default function Servers() {
|
|||||||
const [showMap, setShowMap] = useState<string>("0")
|
const [showMap, setShowMap] = useState<string>("0")
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [settingsOpen, setSettingsOpen] = useState<boolean>(false)
|
const [settingsOpen, setSettingsOpen] = useState<boolean>(false)
|
||||||
|
|
||||||
|
// 使用ref存储筛选状态,防止WebSocket消息刷新时重置
|
||||||
|
const groupRef = useRef<string>("All")
|
||||||
|
const countryRef = useRef<string>("All")
|
||||||
const [currentGroup, setCurrentGroup] = useState<string>("All")
|
const [currentGroup, setCurrentGroup] = useState<string>("All")
|
||||||
|
const [currentCountry, setCurrentCountry] = useState<string>("All")
|
||||||
|
|
||||||
|
// 保存是否已经初始化了筛选状态
|
||||||
|
const initializedRef = useRef<boolean>(false)
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
@ -44,11 +52,16 @@ export default function Servers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTagChange = (newGroup: string) => {
|
const handleCountryChange = useCallback((newCountry: string) => {
|
||||||
setCurrentGroup(newGroup)
|
countryRef.current = newCountry;
|
||||||
sessionStorage.setItem("selectedGroup", newGroup)
|
|
||||||
sessionStorage.setItem("scrollPosition", String(containerRef.current?.scrollTop || 0))
|
// 强制立即更新状态
|
||||||
}
|
setCurrentCountry(newCountry);
|
||||||
|
|
||||||
|
// 保存到会话存储
|
||||||
|
sessionStorage.setItem("selectedCountry", newCountry);
|
||||||
|
sessionStorage.setItem("scrollPosition", String(containerRef.current?.scrollTop || 0));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const showServicesState = localStorage.getItem("showServices")
|
const showServicesState = localStorage.getItem("showServices")
|
||||||
@ -71,22 +84,54 @@ export default function Servers() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 仅在组件挂载时初始化一次状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (initializedRef.current) return;
|
||||||
|
|
||||||
const savedGroup = sessionStorage.getItem("selectedGroup") || "All"
|
const savedGroup = sessionStorage.getItem("selectedGroup") || "All"
|
||||||
|
const savedCountry = sessionStorage.getItem("selectedCountry") || "All"
|
||||||
|
|
||||||
|
groupRef.current = savedGroup
|
||||||
|
countryRef.current = savedCountry
|
||||||
|
|
||||||
setCurrentGroup(savedGroup)
|
setCurrentGroup(savedGroup)
|
||||||
|
setCurrentCountry(savedCountry)
|
||||||
|
|
||||||
restoreScrollPosition()
|
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 nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||||
|
|
||||||
const groupTabs = [
|
// 获取所有可用的国家代码
|
||||||
|
const availableCountries = nezhaWsData?.servers
|
||||||
|
? [...new Set(nezhaWsData.servers.map(server => server.country_code?.toLowerCase()))]
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort()
|
||||||
|
: []
|
||||||
|
|
||||||
|
const countryTabs = [
|
||||||
"All",
|
"All",
|
||||||
...(groupData?.data
|
...availableCountries.map(code => code.toUpperCase())
|
||||||
?.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) || []),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 获取cycle_transfer_stats数据
|
// 获取cycle_transfer_stats数据
|
||||||
@ -122,11 +167,25 @@ export default function Servers() {
|
|||||||
|
|
||||||
let filteredServers =
|
let filteredServers =
|
||||||
nezhaWsData?.servers?.filter((server) => {
|
nezhaWsData?.servers?.filter((server) => {
|
||||||
if (currentGroup === "All") return true
|
// 组筛选
|
||||||
const group = groupData?.data?.find(
|
if (currentGroup !== "All") {
|
||||||
(g: ServerGroup) => g.group.name === currentGroup && Array.isArray(g.servers) && g.servers.includes(server.id),
|
const group = groupData?.data?.find(
|
||||||
)
|
(g: ServerGroup) => g.group.name === currentGroup && Array.isArray(g.servers) && g.servers.includes(server.id),
|
||||||
return !!group
|
)
|
||||||
|
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 totalServers = filteredServers.length || 0
|
||||||
@ -215,7 +274,7 @@ export default function Servers() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl px-0">
|
<div className="mx-auto w-full max-w-7xl px-0">
|
||||||
<ServerOverview
|
<ServerOverview
|
||||||
total={totalServers}
|
total={totalServers}
|
||||||
online={onlineServers}
|
online={onlineServers}
|
||||||
@ -225,7 +284,7 @@ export default function Servers() {
|
|||||||
upSpeed={upSpeed}
|
upSpeed={upSpeed}
|
||||||
downSpeed={downSpeed}
|
downSpeed={downSpeed}
|
||||||
/>
|
/>
|
||||||
<div className="flex mt-6 items-center justify-between gap-2 server-overview-controls">
|
<div className="flex mt-4 items-center justify-between gap-2 server-overview-controls">
|
||||||
<section className="flex items-center gap-2 w-full overflow-hidden">
|
<section className="flex items-center gap-2 w-full overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -271,7 +330,6 @@ export default function Servers() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={handleTagChange} />
|
|
||||||
</section>
|
</section>
|
||||||
<Popover onOpenChange={setSettingsOpen}>
|
<Popover onOpenChange={setSettingsOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@ -334,7 +392,17 @@ export default function Servers() {
|
|||||||
</div>
|
</div>
|
||||||
{showMap === "1" && <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />}
|
{showMap === "1" && <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />}
|
||||||
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
|
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
|
||||||
<section ref={containerRef} className="grid grid-cols-1 gap-4 md:grid-cols-3 mt-6 server-card-list">
|
|
||||||
|
{/* 优化直接国家选择器 */}
|
||||||
|
<div className="mt-3">
|
||||||
|
<DirectCountrySelect
|
||||||
|
countries={countryTabs.filter(tab => tab !== "All")}
|
||||||
|
currentCountry={currentCountry}
|
||||||
|
onChange={handleCountryChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section ref={containerRef} className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-4 server-card-list">
|
||||||
{filteredServers.map((serverInfo) => {
|
{filteredServers.map((serverInfo) => {
|
||||||
// 查找服务器所属的分组
|
// 查找服务器所属的分组
|
||||||
const serverGroup = groupData?.data?.find(
|
const serverGroup = groupData?.data?.find(
|
||||||
|
@ -24,7 +24,7 @@ export default function ServerDetail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl px-0 flex flex-col gap-4 server-info">
|
<div className="mx-auto w-full max-w-7xl px-0 flex flex-col gap-4 server-info">
|
||||||
<ServerDetailOverview server_id={server_id} />
|
<ServerDetailOverview server_id={server_id} />
|
||||||
<section className="flex items-center my-2 w-full">
|
<section className="flex items-center my-2 w-full">
|
||||||
<Separator className="flex-1" />
|
<Separator className="flex-1" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user