Merge upstream v2 with Domain tracking

This commit is contained in:
2026-04-16 11:57:15 +08:00
124 changed files with 13179 additions and 10039 deletions
+129 -105
View File
@@ -1,109 +1,133 @@
import { useQuery } from "@tanstack/react-query"
import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
import { useQuery } from "@tanstack/react-query";
import type React from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import { DashCommand } from "./components/DashCommand"
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"
import { cn } from "./lib/utils"
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";
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";
import { cn } from "./lib/utils";
import ErrorPage from "./pages/ErrorPage";
import NotFound from "./pages/NotFound";
import Server from "./pages/Server";
import ServerDetail from "./pages/ServerDetail";
// Route checker component
const RouteChecker: React.FC = () => {
return <MainApp />;
};
const MainApp: React.FC = () => {
const { data: settingData, error } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
});
const { i18n } = useTranslation();
const { setTheme } = useTheme();
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false);
const { backgroundImage: customBackgroundImage } = useBackground();
useEffect(() => {
if (settingData?.data?.config?.custom_code) {
InjectContext(settingData?.data?.config?.custom_code);
setIsCustomCodeInjected(true);
}
}, [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);
}
}, [forceTheme, setTheme]);
if (error) {
return <ErrorPage code={500} message={error.message} />;
}
if (!settingData) {
return null;
}
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
return null;
}
if (
settingData?.data?.config?.language &&
!localStorage.getItem("language")
) {
i18n.changeLanguage(settingData?.data?.config?.language);
}
const customMobileBackgroundImage =
window.CustomMobileBackgroundImage !== ""
? window.CustomMobileBackgroundImage
: undefined;
return (
<ErrorBoundary>
{/* 固定定位的背景层 */}
{customBackgroundImage && (
<div
className={cn(
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75",
{
"hidden sm:block": customMobileBackgroundImage,
},
)}
style={{ backgroundImage: `url(${customBackgroundImage})` }}
/>
)}
{customMobileBackgroundImage && (
<div
className={cn(
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75",
)}
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
/>
)}
<div
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 />
<Header />
<DashCommand />
<Routes>
<Route path="/" element={<Server />} />
<Route path="/server/:id" element={<ServerDetail />} />
<Route path="/error" element={<ErrorPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Footer />
</main>
</div>
</ErrorBoundary>
);
};
// Main App wrapper with router
const App: React.FC = () => {
const { data: settingData, error } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
})
const { i18n } = useTranslation()
const { setTheme } = useTheme()
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false)
const { backgroundImage: customBackgroundImage } = useBackground()
return (
<Router basename={import.meta.env.BASE_URL}>
<RouteChecker />
</Router>
);
};
useEffect(() => {
if (settingData?.data?.config?.custom_code) {
InjectContext(settingData?.data?.config?.custom_code)
setIsCustomCodeInjected(true)
}
}, [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)
}
}, [forceTheme])
if (error) {
return <ErrorPage code={500} message={error.message} />
}
if (!settingData) {
return null
}
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
return null
}
if (settingData?.data?.config?.language && !localStorage.getItem("language")) {
i18n.changeLanguage(settingData?.data?.config?.language)
}
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
return (
<Router basename={import.meta.env.BASE_URL}>
<ErrorBoundary>
{/* 固定定位的背景层 */}
{customBackgroundImage && (
<div
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75", {
"hidden sm:block": customMobileBackgroundImage,
})}
style={{ backgroundImage: `url(${customBackgroundImage})` }}
/>
)}
{customMobileBackgroundImage && (
<div
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75")}
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
/>
)}
<div
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 />
<Header />
<DashCommand />
<Routes>
<Route path="/" element={<Server />} />
<Route path="/server/:id" element={<ServerDetail />} />
<Route path="/error" element={<ErrorPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Footer />
</main>
</div>
</ErrorBoundary>
</Router>
)
}
export default App
export default App;
+109
View File
@@ -0,0 +1,109 @@
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
export function AnimateCountClient({
count,
className,
minDigits,
}: {
count: number;
className?: string;
minDigits?: number;
}) {
const [previousCount, setPreviousCount] = useState(count);
useEffect(() => {
if (count !== previousCount) {
setTimeout(() => {
setPreviousCount(count);
}, 300);
}
}, [count, previousCount]);
return (
<AnimateCount
key={count}
preCount={previousCount}
className={cn("inline-flex items-center leading-none", className)}
minDigits={minDigits}
data-issues-count-animation
>
{count}
</AnimateCount>
);
}
export default AnimateCountClient;
export function AnimateCount({
children: count,
className,
preCount,
minDigits = 1,
...props
}: {
children: number;
className?: string;
preCount?: number;
minDigits?: number;
}) {
const currentDigits = count.toString().split("");
const previousDigits = (
preCount !== undefined
? preCount.toString()
: count - 1 >= 0
? (count - 1).toString()
: "0"
).split("");
// Ensure both numbers meet the minimum length requirement and maintain the same length for animation
const maxLength = Math.max(
previousDigits.length,
currentDigits.length,
minDigits,
);
while (previousDigits.length < maxLength) {
previousDigits.unshift("0");
}
while (currentDigits.length < maxLength) {
currentDigits.unshift("0");
}
return (
<div {...props} className={cn("flex h-[1em] items-center", className)}>
{currentDigits.map((digit, index) => {
const hasChanged = digit !== previousDigits[index];
return (
<div
key={`${index}-${digit}`}
className={cn(
"relative flex h-full min-w-[0.6em] items-center text-center",
{
"min-w-[0.2em]": digit === ".",
},
)}
>
<div
aria-hidden
data-issues-count-exit
className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged ? "animate" : "opacity-0",
)}
>
{previousDigits[index]}
</div>
<div
data-issues-count-enter
className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged && "animate",
)}
>
{digit}
</div>
</div>
);
})}
</div>
);
}
+56 -50
View File
@@ -1,62 +1,68 @@
import { CycleTransferStats, NezhaServer } from "@/types/nezha-api"
import React from "react"
import type React from "react";
import type { CycleTransferStats, NezhaServer } from "@/types/nezha-api";
import { CycleTransferStatsClient } from "./CycleTransferStatsClient"
import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
interface CycleTransferStatsProps {
serverList: NezhaServer[]
cycleStats: CycleTransferStats
className?: string
serverList: NezhaServer[];
cycleStats: CycleTransferStats;
className?: string;
}
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serverList, cycleStats, className }) => {
if (serverList.length === 0) {
return null
}
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
serverList,
cycleStats,
className,
}) => {
if (serverList.length === 0) {
return null;
}
const serverIdList = serverList.map((server) => server.id.toString())
const serverIdList = serverList.map((server) => server.id.toString());
return (
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
if (!cycleData.server_name) {
return null
}
return (
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
if (!cycleData.server_name) {
return null;
}
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => {
const transfer = cycleData.transfer?.[serverId] || 0
const nextUpdate = cycleData.next_update?.[serverId]
return Object.entries(cycleData.server_name).map(
([serverId, serverName]) => {
const transfer = cycleData.transfer?.[serverId] || 0;
const nextUpdate = cycleData.next_update?.[serverId];
if (!serverIdList.includes(serverId)) {
return null
}
if (!serverIdList.includes(serverId)) {
return null;
}
if (!transfer && !nextUpdate) {
return null
}
if (!transfer && !nextUpdate) {
return null;
}
return (
<CycleTransferStatsClient
key={`${cycleId}-${serverId}`}
name={cycleData.name}
from={cycleData.from}
to={cycleData.to}
max={cycleData.max}
serverStats={[
{
serverId,
serverName,
transfer,
nextUpdate: nextUpdate || "",
},
]}
className={className}
/>
)
})
})}
</section>
)
}
return (
<CycleTransferStatsClient
key={`${cycleId}-${serverId}`}
name={cycleData.name}
from={cycleData.from}
to={cycleData.to}
max={cycleData.max}
serverStats={[
{
serverId,
serverName,
transfer,
nextUpdate: nextUpdate || "",
},
]}
className={className}
/>
);
},
);
})}
</section>
);
};
export default CycleTransferStatsCard
export default CycleTransferStatsCard;
+87 -70
View File
@@ -1,79 +1,96 @@
import { formatBytes } from "@/lib/format"
import { cn } from "@/lib/utils"
import React from "react"
import { useTranslation } from "react-i18next"
import type React from "react";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@/lib/format";
import { cn } from "@/lib/utils";
interface CycleTransferStatsClientProps {
name: string
from: string
to: string
max: number
serverStats: Array<{
serverId: string
serverName: string
transfer: number
nextUpdate: string
}>
className?: string
name: string;
from: string;
to: string;
max: number;
serverStats: Array<{
serverId: string;
serverName: string;
transfer: number;
nextUpdate: string;
}>;
className?: string;
}
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => {
const { t } = useTranslation()
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
return (
<div
className={cn(
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-sm transition-all duration-200 dark:shadow-none",
className,
{
"bg-card/70": customBackgroundImage,
},
)}
>
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
const progress = (transfer / max) * 100
export const CycleTransferStatsClient: React.FC<
CycleTransferStatsClientProps
> = ({ name, from, to, max, serverStats, className }) => {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return (
<div
className={cn(
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-xs transition-all duration-200 dark:shadow-none",
className,
{
"bg-card/70": customBackgroundImage,
},
)}
>
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
const progress = (transfer / max) * 100;
return (
<div key={serverId} className="space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</span>
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">{name}</div>
</div>
return (
<div key={serverId} className="space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
{serverName}
</span>
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">
{name}
</div>
</div>
{/* Progress Section */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<div className="flex items-baseline gap-1">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{formatBytes(transfer)}</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span>
</div>
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span>
</div>
{/* Progress Section */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<div className="flex items-baseline gap-1">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
{formatBytes(transfer)}
</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400">
/ {formatBytes(max)}
</span>
</div>
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
{progress.toFixed(1)}%
</span>
</div>
<div className="relative h-1.5">
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
<div
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
style={{ width: `${Math.min(progress, 100)}%` }}
/>
</div>
</div>
<div className="relative h-1.5">
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
<div
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
style={{ width: `${Math.min(progress, 100)}%` }}
/>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
<span>
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
</span>
<span>
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
</span>
</div>
</div>
)
})}
</div>
)
}
{/* Footer */}
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
<span>
{new Date(from).toLocaleDateString()} -{" "}
{new Date(to).toLocaleDateString()}
</span>
<span>
{t("cycleTransfer.nextUpdate")}:{" "}
{new Date(nextUpdate).toLocaleString()}
</span>
</div>
</div>
);
})}
</div>
);
};
export default CycleTransferStatsClient
export default CycleTransferStatsClient;
+117 -106
View File
@@ -1,118 +1,129 @@
"use client"
"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"
import { Home, Moon, Sun, SunMoon } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { useCommand } from "@/hooks/use-command";
import { useTheme } from "@/hooks/use-theme";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export function DashCommand() {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState("")
const navigate = useNavigate()
const { t } = useTranslation()
const { setTheme } = useTheme()
const { isOpen, closeCommand, toggleCommand } = useCommand();
const [search, setSearch] = useState("");
const navigate = useNavigate();
const { t } = useTranslation();
const { setTheme } = useTheme();
const { lastMessage, connected } = useWebSocketContext()
const { lastMessage, connected } = useWebSocketContext();
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
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)
}
}
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
toggleCommand();
}
};
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, [toggleCommand]);
if (!connected || !nezhaWsData) return null
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}`,
}))
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 className="border-t">
<CommandEmpty>{t("NoResults")}</CommandEmpty>
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
<>
<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 />
return (
<CommandDialog open={isOpen} onOpenChange={closeCommand}>
<CommandInput
placeholder={t("TypeCommand")}
value={search}
onValueChange={setSearch}
/>
<CommandList className="border-t">
<CommandEmpty>{t("NoResults")}</CommandEmpty>
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
<CommandGroup heading={t("Servers")}>
{nezhaWsData.servers.map((server) => (
<CommandItem
key={server.id}
value={server.name}
onSelect={() => {
navigate(`/server/${server.id}`);
closeCommand();
}}
>
{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>
</>
)
<CommandGroup heading={t("Shortcuts")}>
{shortcuts.map((item) => (
<CommandItem
key={item.label}
value={item.value}
onSelect={() => {
item.action();
closeCommand();
}}
>
{item.icon}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
+27 -22
View File
@@ -1,36 +1,41 @@
import React from "react"
import React from "react";
import ErrorPage from "../pages/ErrorPage"
import ErrorPage from "../pages/ErrorPage";
interface Props {
children: React.ReactNode
children: React.ReactNode;
}
interface State {
hasError: boolean
error?: Error
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return {
hasError: true,
error,
}
}
static getDerivedStateFromError(error: Error): State {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return <ErrorPage code={500} message={this.state.error?.message || "应用程序发生错误"} />
}
render() {
if (this.state.hasError) {
return (
<ErrorPage
code={500}
message={this.state.error?.message || "应用程序发生错误"}
/>
);
}
return this.props.children
}
return this.props.children;
}
}
export default ErrorBoundary
export default ErrorBoundary;
+69 -55
View File
@@ -1,60 +1,74 @@
// src/components/Footer.tsx (已添加您的署名)
import { fetchSetting } from "@/lib/nezha-api"
import { useQuery } from "@tanstack/react-query"
import React from "react"
import { useTranslation } from "react-i18next"
import { useQuery } from "@tanstack/react-query";
import type React from "react";
import { useTranslation } from "react-i18next";
import { fetchSetting } from "@/lib/nezha-api";
const Footer: React.FC = () => {
const { t } = useTranslation()
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
const { t } = useTranslation();
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
const { data: settingData } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
})
const { data: settingData } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
});
return (
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
<section className="flex flex-col">
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
<div className="flex items-center gap-1">
&copy;2020-{new Date().getFullYear()}{" "}
<a href={"https://github.com/naiba/nezha"} target="_blank" className="hover:underline">
Nezha
</a>
<p>{settingData?.data?.version || ""}</p>
</div>
<div className="server-footer-theme flex flex-col items-center sm:items-end">
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
{isMac ? <span className="text-xs"></span> : "Ctrl "}K
</kbd>
</p>
<section>
{t("footer.themeBy")}{" "}
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank" className="hover:underline">
nezha-dash
</a>
{import.meta.env.VITE_GIT_HASH && (
<a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1 hover:underline">
({import.meta.env.VITE_GIT_HASH})
</a>
)}
</section>
<section className="mt-1">
{"Modified by "}
<a href={"https://github.com/buriburizaem0n"} target="_blank" className="hover:underline font-medium">
buriburizaem0n
</a>
</section>
</div>
</section>
</section>
</footer>
)
}
return (
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
<section className="flex flex-col">
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
<div className="flex items-center gap-1">
&copy;2020-{new Date().getFullYear()}{" "}
<a
href={"https://github.com/naiba/nezha"}
target="_blank"
rel="noopener"
>
Nezha
</a>
<p>{settingData?.data?.version || ""}</p>
</div>
<div className="server-footer-theme flex flex-col items-center sm:items-end">
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
{isMac ? <span className="text-xs"></span> : "Ctrl "}K
</kbd>
</p>
<section>
{t("footer.themeBy")}
<a
href={"https://github.com/hamster1963/nezha-dash"}
target="_blank"
rel="noopener"
>
nezha-dash
</a>
{import.meta.env.VITE_GIT_HASH && (
<a
href={`https://github.com/hamster1963/nezha-dash-v1/commit/${import.meta.env.VITE_GIT_HASH}`}
className="ml-1"
>
({import.meta.env.VITE_GIT_HASH})
</a>
)}
</section>
<section className="mt-1">
{"Modified by "}
<a
href={"https://github.com/buriburizaem0n"}
target="_blank"
rel="noopener"
className="hover:underline font-medium"
>
buriburizaem0n
</a>
</section>
</div>
</section>
</section>
</footer>
);
};
export default Footer
export default Footer;
+210 -161
View File
@@ -1,184 +1,233 @@
import useTooltip from "@/hooks/use-tooltip"
import { geoJsonString } from "@/lib/geo-json-string"
import { countryCoordinates } from "@/lib/geo-limit"
import { cn, formatNezhaInfo } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { geoEquirectangular, geoPath } from "d3-geo"
import { useTranslation } from "react-i18next"
import { geoEquirectangular, geoPath } from "d3-geo";
import { useTranslation } from "react-i18next";
import useTooltip from "@/hooks/use-tooltip";
import { geoJsonString } from "@/lib/geo-json-string";
import { countryCoordinates } from "@/lib/geo-limit";
import { cn, formatNezhaInfo } from "@/lib/utils";
import type { NezhaServer } from "@/types/nezha-api";
import MapTooltip from "./MapTooltip"
import MapTooltip from "./MapTooltip";
export default function GlobalMap({ serverList, now }: { serverList: NezhaServer[]; now: number }) {
const { t } = useTranslation()
const countryList: string[] = []
const serverCounts: { [key: string]: number } = {}
export default function GlobalMap({
serverList,
now,
}: {
serverList: NezhaServer[];
now: number;
}) {
const { t } = useTranslation();
const countryList: string[] = [];
const serverCounts: { [key: string]: number } = {};
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
serverList.forEach((server) => {
if (server.country_code) {
const countryCode = server.country_code.toUpperCase()
if (!countryList.includes(countryCode)) {
countryList.push(countryCode)
}
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
}
})
serverList.forEach((server) => {
if (server.country_code) {
const countryCode = server.country_code.toUpperCase();
if (!countryList.includes(countryCode)) {
countryList.push(countryCode);
}
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
}
});
const width = 900
const height = 500
const width = 900;
const height = 500;
const geoJson = JSON.parse(geoJsonString)
const filteredFeatures = geoJson.features.filter((feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "")
const geoJson = JSON.parse(geoJsonString);
const filteredFeatures = geoJson.features.filter(
(feature: { properties: { iso_a3_eh: string } }) =>
feature.properties.iso_a3_eh !== "",
);
return (
<section
className={cn("flex flex-col gap-4 mt-8", {
"bg-card/70 rounded-lg p-4": customBackgroundImage,
})}
>
<p className="text-sm font-medium opacity-40">
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
</p>
<div className="w-full overflow-x-auto">
<InteractiveMap
countries={countryList}
serverCounts={serverCounts}
width={width}
height={height}
filteredFeatures={filteredFeatures}
nezhaServerList={serverList}
now={now}
/>
</div>
</section>
)
return (
<section
className={cn("flex flex-col gap-4 mt-8", {
"bg-card/70 rounded-lg p-4": customBackgroundImage,
})}
>
<p className="text-sm font-medium opacity-40">
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
</p>
<div className="w-full overflow-x-auto">
<InteractiveMap
countries={countryList}
serverCounts={serverCounts}
width={width}
height={height}
filteredFeatures={filteredFeatures}
nezhaServerList={serverList}
now={now}
/>
</div>
</section>
);
}
interface InteractiveMapProps {
countries: string[]
serverCounts: { [key: string]: number }
width: number
height: number
filteredFeatures: {
type: "Feature"
properties: {
iso_a2_eh: string
[key: string]: string
}
geometry: never
}[]
nezhaServerList: NezhaServer[]
now: number
countries: string[];
serverCounts: { [key: string]: number };
width: number;
height: number;
filteredFeatures: {
type: "Feature";
properties: {
iso_a2_eh: string;
[key: string]: string;
};
geometry: never;
}[];
nezhaServerList: NezhaServer[];
now: number;
}
export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, nezhaServerList, now }: InteractiveMapProps) {
const { setTooltipData } = useTooltip()
export function InteractiveMap({
countries,
serverCounts,
width,
height,
filteredFeatures,
nezhaServerList,
now,
}: InteractiveMapProps) {
const { setTooltipData } = useTooltip();
const projection = geoEquirectangular()
.scale(140)
.translate([width / 2, height / 2])
.rotate([-12, 0, 0])
const projection = geoEquirectangular()
.scale(140)
.translate([width / 2, height / 2])
.rotate([-12, 0, 0]);
const path = geoPath().projection(projection)
const path = geoPath().projection(projection);
return (
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" className="w-full h-auto">
<defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
</pattern>
</defs>
<g>
{/* Background rect to handle mouse events in empty areas */}
<rect x="0" y="0" width={width} height={height} fill="transparent" onMouseEnter={() => setTooltipData(null)} />
{filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
return (
<div
className="relative w-full aspect-2/1"
onMouseLeave={() => setTooltipData(null)}
>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
>
<defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
</pattern>
</defs>
<g>
{/* Background rect to handle mouse events in empty areas */}
<rect
x="0"
y="0"
width={width}
height={height}
fill="transparent"
onMouseEnter={() => setTooltipData(null)}
/>
{filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(
feature.properties.iso_a2_eh,
);
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
return (
<path
key={index}
d={path(feature) || ""}
className={
isHighlighted
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
}
onMouseEnter={() => {
if (!isHighlighted) {
setTooltipData(null)
return
}
if (path.centroid(feature)) {
const countryCode = feature.properties.iso_a2_eh
const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode)
.map((server: NezhaServer) => ({
name: server.name,
status: formatNezhaInfo(now, server).online,
}))
setTooltipData({
centroid: path.centroid(feature),
country: feature.properties.name,
count: serverCount,
servers: countryServers,
})
}
}}
/>
)
})}
return (
<path
key={index}
d={path(feature) || ""}
className={
isHighlighted
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
}
onMouseEnter={() => {
if (!isHighlighted) {
setTooltipData(null);
return;
}
if (path.centroid(feature)) {
const countryCode = feature.properties.iso_a2_eh;
const countryServers = nezhaServerList
.filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() === countryCode,
)
.map((server: NezhaServer) => ({
id: server.id,
name: server.name,
status: formatNezhaInfo(now, server).online,
}));
setTooltipData({
centroid: path.centroid(feature),
country: feature.properties.name,
count: serverCount,
servers: countryServers,
});
}
}}
/>
);
})}
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
{countries.map((countryCode) => {
// 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some((feature) => feature.properties.iso_a2_eh === countryCode)
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
{countries.map((countryCode) => {
// 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode,
);
// 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null
// 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null;
// 获取国家的经纬度
const coords = countryCoordinates[countryCode]
if (!coords) return null
// 获取国家的经纬度
const coords = countryCoordinates[countryCode];
if (!coords) return null;
// 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]
const serverCount = serverCounts[countryCode] || 0
// 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
const serverCount = serverCounts[countryCode] || 0;
return (
<g
key={countryCode}
onMouseEnter={() => {
const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode.toUpperCase())
.map((server: NezhaServer) => ({
name: server.name,
status: formatNezhaInfo(now, server).online,
}))
setTooltipData({
centroid: [x, y],
country: coords.name,
count: serverCount,
servers: countryServers,
})
}}
className="cursor-pointer"
>
<circle
cx={x}
cy={y}
r={4}
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/>
</g>
)
})}
</g>
</svg>
<MapTooltip />
</div>
)
return (
<g
key={countryCode}
onMouseEnter={() => {
const countryServers = nezhaServerList
.filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() ===
countryCode.toUpperCase(),
)
.map((server: NezhaServer) => ({
id: server.id,
name: server.name,
status: formatNezhaInfo(now, server).online,
}));
setTooltipData({
centroid: [x, y],
country: coords.name,
count: serverCount,
servers: countryServers,
});
}}
className="cursor-pointer"
>
<circle
cx={x}
cy={y}
r={4}
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/>
</g>
);
})}
</g>
</svg>
<MapTooltip />
</div>
);
}
+96 -77
View File
@@ -1,92 +1,111 @@
import { cn } from "@/lib/utils"
import { m } from "framer-motion"
import { createRef, useEffect, useRef } from "react"
import { m } from "framer-motion";
import { createRef, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
export default function GroupSwitch({
tabs,
currentTab,
setCurrentTab,
tabs,
currentTab,
setCurrentTab,
}: {
tabs: string[]
currentTab: string
setCurrentTab: (tab: string) => void
tabs: string[];
currentTab: string;
setCurrentTab: (tab: string) => void;
}) {
const customBackgroundImage = (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>()))
const scrollRef = useRef<HTMLDivElement>(null);
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()));
useEffect(() => {
const container = scrollRef.current
if (!container) return
useEffect(() => {
const container = scrollRef.current;
if (!container) return;
const isOverflowing = container.scrollWidth > container.clientWidth
if (!isOverflowing) return
const isOverflowing = container.scrollWidth > container.clientWidth;
if (!isOverflowing) return;
const onWheel = (e: WheelEvent) => {
e.preventDefault()
container.scrollLeft += e.deltaY
}
const onWheel = (e: WheelEvent) => {
e.preventDefault();
container.scrollLeft += e.deltaY;
};
container.addEventListener("wheel", onWheel, { passive: false })
container.addEventListener("wheel", onWheel, { passive: false });
return () => {
container.removeEventListener("wheel", onWheel)
}
}, [])
return () => {
container.removeEventListener("wheel", onWheel);
};
}, []);
useEffect(() => {
const savedGroup = sessionStorage.getItem("selectedGroup")
if (savedGroup && tabs.includes(savedGroup)) {
setCurrentTab(savedGroup)
}
}, [tabs, setCurrentTab])
useEffect(() => {
if (tabs.length === 1 && tabs[0] === "All") {
setCurrentTab("All");
return;
}
const savedGroup = sessionStorage.getItem("selectedGroup");
if (savedGroup && tabs.includes(savedGroup)) {
setCurrentTab(savedGroup);
}
}, [tabs, setCurrentTab]);
useEffect(() => {
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)]
useEffect(() => {
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)];
if (currentTagRef && currentTagRef.current) {
currentTagRef.current.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
})
}
}, [currentTab])
if (currentTagRef?.current) {
currentTagRef.current.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
}, [currentTab, tabs.indexOf]);
return (
<div ref={scrollRef} className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
<div
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,
})}
>
{tabs.map((tab: string, index: number) => (
<div
key={tab}
ref={tagRefs.current[index]}
onClick={() => setCurrentTab(tab)}
className={cn(
"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 && (
<m.div
layoutId="tab-switch"
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={{
originY: "0px",
borderRadius: 46,
}}
/>
)}
<div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{tab}</p>
</div>
</div>
))}
</div>
</div>
)
if (tabs.length === 1 && tabs[0] === "All") {
return null;
}
return (
<div
ref={scrollRef}
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
>
<div
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,
},
)}
>
{tabs.map((tab: string, index: number) => (
<div
key={tab}
ref={tagRefs.current[index]}
onClick={() => setCurrentTab(tab)}
className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
currentTab === tab
? "text-black dark:text-white"
: "text-stone-400 dark:text-stone-500",
)}
>
{currentTab === tab && (
<m.div
layoutId="tab-switch"
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={{
originY: "0px",
borderRadius: 46,
}}
/>
)}
<div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{tab}</p>
</div>
</div>
))}
</div>
</div>
);
}
+320 -259
View File
@@ -1,303 +1,364 @@
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 NumberFlow, { NumberFlowGroup } from "@number-flow/react"
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"
import { useNavigate } from "react-router-dom"
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";
import { useNavigate } from "react-router-dom";
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 { LanguageSwitcher } from "./LanguageSwitcher"
import { Loader, LoadingSpinner } from "./loading/Loader"
import { Button } from "./ui/button"
import AnimateCountClient from "./AnimatedCount";
import { LanguageSwitcher } from "./LanguageSwitcher";
import { Loader, LoadingSpinner } from "./loading/Loader";
import { SearchButton } from "./SearchButton";
import { Button } from "./ui/button";
interface TimeState {
hh: number;
mm: number;
ss: number;
}
const useCurrentTime = () => {
const [time, setTime] = useState<TimeState>({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
});
useEffect(() => {
const intervalId = setInterval(() => {
const now = DateTime.now().setLocale("en-US");
setTime({
hh: now.hour,
mm: now.minute,
ss: now.second,
});
}, 1000);
return () => clearInterval(intervalId);
}, []);
return time;
};
function Header() {
const { t } = useTranslation()
const navigate = useNavigate()
const { backgroundImage, updateBackground } = useBackground()
const { t } = useTranslation();
const navigate = useNavigate();
const { backgroundImage, updateBackground } = useBackground();
const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
})
const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
});
const { lastMessage, connected } = useWebSocketContext()
const { lastMessage, connected } = useWebSocketContext();
const onlineCount = connected ? (lastMessage ? JSON.parse(lastMessage.data).online || 0 : 0) : "..."
const onlineCount = connected
? lastMessage
? JSON.parse(lastMessage.data).online || 0
: 0
: "...";
const siteName = settingData?.data?.config?.site_name
const siteName = settingData?.data?.config?.site_name;
// @ts-expect-error CustomLogo is a global variable
const customLogo = window.CustomLogo || "/apple-touch-icon.png"
// @ts-expect-error CustomLogo is a global variable
const customLogo = window.CustomLogo || "/apple-touch-icon.png";
// @ts-expect-error CustomDesc is a global variable
const customDesc = window.CustomDesc || t("nezha")
// @ts-expect-error CustomDesc is a global variable
const customDesc = window.CustomDesc || t("nezha");
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
const customMobileBackgroundImage =
window.CustomMobileBackgroundImage !== ""
? window.CustomMobileBackgroundImage
: undefined;
useEffect(() => {
const link = document.querySelector("link[rel*='icon']") || document.createElement("link")
// @ts-expect-error set link.type
link.type = "image/x-icon"
// @ts-expect-error set link.rel
link.rel = "shortcut icon"
// @ts-expect-error set link.href
link.href = customLogo
document.getElementsByTagName("head")[0].appendChild(link)
}, [customLogo])
useEffect(() => {
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
// @ts-expect-error set link.type
link.type = "image/x-icon";
// @ts-expect-error set link.rel
link.rel = "shortcut icon";
// @ts-expect-error set link.href
link.href = customLogo;
document.getElementsByTagName("head")[0].appendChild(link);
}, [customLogo]);
useEffect(() => {
document.title = siteName || "哪吒监控 Nezha Monitoring"
}, [siteName])
useEffect(() => {
document.title = siteName || "哪吒监控 Nezha Monitoring";
}, [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 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
const customBackgroundImage = backgroundImage;
return (
<div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between header-top">
<section
onClick={() => {
sessionStorage.removeItem("selectedGroup")
navigate("/")
}}
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
>
<div className="mr-1 flex flex-row items-center justify-start header-logo">
<img
width={40}
height={40}
alt="apple-touch-icon"
src={customLogo}
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
/>
</div>
{isLoading ? <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" /> : siteName || "NEZHA"}
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p>
</section>
<section className="flex items-center gap-2 header-handles">
<div className="hidden sm:flex items-center gap-2">
<Links />
<DashboardLink />
</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"
className={cn("hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black", {
"bg-white/70 dark:bg-black/70": customBackgroundImage,
})}
>
{connected ? onlineCount : <Loader visible={true} />}
<p className="text-muted-foreground">{connected ? t("online") : t("offline")}</p>
<span
className={cn("h-2 w-2 rounded-full bg-green-500", {
"bg-red-500": !connected,
})}
></span>
</Button>
</section>
</section>
<div className="w-full flex justify-between sm:hidden mt-1">
<DashboardLink />
<Links />
</div>
<Overview />
</div>
)
return (
<div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between header-top">
<section
onClick={() => {
sessionStorage.removeItem("selectedGroup");
navigate("/");
}}
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
>
<div className="mr-1 flex flex-row items-center justify-start header-logo">
<img
width={40}
height={40}
alt="apple-touch-icon"
src={customLogo}
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
/>
</div>
{isLoading ? (
<Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" />
) : (
siteName || "NEZHA"
)}
<Separator
orientation="vertical"
className="mx-2 hidden h-4 w-px md:block"
/>
<p className="hidden text-sm font-medium opacity-40 md:block">
{customDesc}
</p>
</section>
<section className="flex items-center gap-2 header-handles">
<div className="hidden sm:flex items-center gap-2">
<Links />
<DashboardLink />
</div>
<SearchButton />
<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"
className={cn(
"hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black",
{
"bg-white/70 dark:bg-black/70": customBackgroundImage,
},
)}
>
{connected ? onlineCount : <Loader visible={true} />}
<p className="text-muted-foreground">
{connected ? t("online") : t("offline")}
</p>
<span
className={cn("h-2 w-2 rounded-full bg-green-500", {
"bg-red-500": !connected,
})}
></span>
</Button>
</section>
</section>
<div className="w-full flex justify-between sm:hidden mt-1">
<DashboardLink />
<Links />
</div>
<Overview />
</div>
);
}
type links = {
link: string
name: string
}
link: string;
name: string;
};
function Links() {
// @ts-expect-error CustomLinks is a global variable
const customLinks = window.CustomLinks as string
// @ts-expect-error CustomLinks is a global variable
const customLinks = window.CustomLinks as string;
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null;
if (!links) return null
if (!links) return null;
return (
<div className="flex items-center gap-2 w-fit">
{links.map((link, index) => {
return (
<a
key={index}
href={link.link}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
>
{link.name}
</a>
)
})}
</div>
)
return (
<div className="flex items-center gap-2 w-fit">
{links.map((link, index) => {
return (
<a
key={index}
href={link.link}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
>
{link.name}
</a>
);
})}
</div>
);
}
export function RefreshToast() {
const { t } = useTranslation()
const navigate = useNavigate()
const { t } = useTranslation();
const navigate = useNavigate();
const { needReconnect } = useWebSocketContext()
const { needReconnect } = useWebSocketContext();
if (!needReconnect) {
return null
}
if (!needReconnect) {
return null;
}
if (needReconnect) {
sessionStorage.removeItem("needRefresh")
setTimeout(() => {
navigate(0)
}, 1000)
}
if (needReconnect) {
sessionStorage.removeItem("needRefresh");
setTimeout(() => {
navigate(0);
}, 1000);
}
return (
<AnimatePresence>
<m.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"
>
<section className="flex items-center gap-1.5">
<LoadingSpinner />
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
</section>
</m.div>
</AnimatePresence>
)
return (
<AnimatePresence>
<m.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 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">
<LoadingSpinner />
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
</section>
</m.div>
</AnimatePresence>
);
}
function DashboardLink() {
const { t } = useTranslation()
const { setNeedReconnect } = useWebSocketContext()
const previousLoginState = useRef<boolean | null>(null)
const {
data: userData,
isFetched,
isLoadingError,
isError,
refetch,
} = useQuery({
queryKey: ["login-user"],
queryFn: () => fetchLoginUser(),
refetchOnMount: false,
refetchOnWindowFocus: true,
refetchIntervalInBackground: true,
refetchInterval: 1000 * 30,
retry: 0,
})
const { t } = useTranslation();
const { setNeedReconnect } = useWebSocketContext();
const previousLoginState = useRef<boolean | null>(null);
const {
data: userData,
isFetched,
isLoadingError,
isError,
refetch,
} = useQuery({
queryKey: ["login-user"],
queryFn: () => fetchLoginUser(),
refetchOnMount: false,
refetchOnWindowFocus: true,
refetchIntervalInBackground: true,
refetchInterval: 1000 * 30,
retry: 0,
});
const isLogin = isError ? false : userData ? !!userData?.data?.id && !!document.cookie : false
const isLogin = isError
? false
: userData
? !!userData?.data?.id && !!document.cookie
: false;
if (isLoadingError) {
previousLoginState.current = isLogin
}
if (isLoadingError) {
previousLoginState.current = isLogin;
}
useEffect(() => {
refetch()
}, [document.cookie])
useEffect(() => {
refetch();
}, [refetch]);
useEffect(() => {
if (isFetched || isError) {
// 只有当登录状态发生变化时才设置needReconnect
if (previousLoginState.current !== null && previousLoginState.current !== isLogin) {
setNeedReconnect(true)
}
previousLoginState.current = isLogin
}
}, [isLogin])
useEffect(() => {
if (isFetched || isError) {
// 只有当登录状态发生变化时才设置needReconnect
if (
previousLoginState.current !== null &&
previousLoginState.current !== isLogin
) {
setNeedReconnect(true);
}
previousLoginState.current = isLogin;
}
}, [isLogin, isError, isFetched, setNeedReconnect]);
return (
<div className="flex items-center gap-2">
<a
href={"/dashboard"}
rel="noopener noreferrer"
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
>
{!isLogin && t("login")}
{isLogin && t("dashboard")}
</a>
</div>
)
return (
<div className="flex items-center gap-2">
<a
href={"/dashboard"}
rel="noopener noreferrer"
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
>
{!isLogin && t("login")}
{isLogin && t("dashboard")}
</a>
</div>
);
}
function Overview() {
const { t } = useTranslation()
const [time, setTime] = useState({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
const { t } = useTranslation();
const time = useCurrentTime();
const [mounted, setMounted] = useState(false);
useEffect(() => {
const timer = setInterval(() => {
setTime({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
}, 1000)
useEffect(() => {
setMounted(true);
}, []);
return () => clearInterval(timer)
}, [])
return (
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
<p className="text-base font-semibold">👋 {t("overview")}</p>
<div className="flex items-center gap-1.5">
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
<NumberFlowGroup>
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5">
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
</div>
</NumberFlowGroup>
</div>
</section>
)
return (
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
<p className="text-base font-semibold">👋 {t("overview")}</p>
<div className="flex items-center gap-1">
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
{mounted ? (
<div className="flex items-center font-medium text-sm">
<AnimateCountClient count={time.hh} minDigits={2} />
<span className="mb-px font-medium text-sm opacity-50">:</span>
<AnimateCountClient count={time.mm} minDigits={2} />
<span className="mb-px font-medium text-sm opacity-50">:</span>
<span className="font-medium text-sm">
<AnimateCountClient count={time.ss} minDigits={2} />
</span>
</div>
) : (
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
)}
</div>
</section>
);
}
export default Header
export default Header;
+27 -27
View File
File diff suppressed because one or more lines are too long
+69 -45
View File
@@ -1,54 +1,78 @@
"use client"
"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"
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
export function LanguageSwitcher() {
const { t, i18n } = useTranslation()
const { t, i18n } = useTranslation();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const locale = i18n.languages[0]
const locale = i18n.languages[0];
const handleSelect = (e: Event, newLocale: string) => {
e.preventDefault() // 阻止默认的关闭行为
i18n.changeLanguage(newLocale)
}
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" },
]
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>
)
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, index) => (
<DropdownMenuItem
key={item.code}
onSelect={(e) => handleSelect(e, item.code)}
className={cn(
"text-xs",
{
"gap-3 bg-muted font-semibold": locale === item.code,
},
{
"rounded-t-[5px]": index === localeItems.length - 1,
"rounded-[5px]":
index !== 0 && index !== localeItems.length - 1,
"rounded-b-[5px]": index === 0,
},
)}
>
{item.name}{" "}
{locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
+65 -49
View File
@@ -1,54 +1,70 @@
import useTooltip from "@/hooks/use-tooltip"
import { AnimatePresence, m } from "framer-motion"
import { memo } from "react"
import { useTranslation } from "react-i18next"
import { AnimatePresence, m } from "framer-motion";
import { memo } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import useTooltip from "@/hooks/use-tooltip";
const MapTooltip = memo(function MapTooltip() {
const { t } = useTranslation()
const { tooltipData } = useTooltip()
const { t } = useTranslation();
const navigate = useNavigate();
const { tooltipData } = useTooltip();
if (!tooltipData) return null
if (!tooltipData) return null;
return (
<AnimatePresence mode="wait">
<m.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(10px)" }}
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
key={tooltipData.country}
style={{
left: tooltipData.centroid[0],
top: tooltipData.centroid[1],
transform: "translate(20%, -50%)",
}}
onMouseEnter={(e) => {
e.stopPropagation()
}}
>
<div>
<p className="font-medium">{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}</p>
<p className="text-neutral-600 dark:text-neutral-400 mb-1">
{tooltipData.count} {t("map.Servers")}
</p>
</div>
<div
className="border-t dark:border-neutral-700 pt-1"
style={{
maxHeight: "200px",
overflowY: "auto",
}}
>
{tooltipData.servers.map((server, index: number) => (
<div key={index} className="flex items-center gap-1.5 py-0.5">
<span className={`w-1.5 h-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}></span>
<span className="text-xs">{server.name}</span>
</div>
))}
</div>
</m.div>
</AnimatePresence>
)
})
return (
<AnimatePresence mode="wait">
<m.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(10px)" }}
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
key={tooltipData.country}
style={{
left: tooltipData.centroid[0],
top: tooltipData.centroid[1],
transform: "translate(20%, -50%)",
}}
onMouseEnter={(e) => {
e.stopPropagation();
}}
>
<div>
<p className="font-medium">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1">
{tooltipData.count} {t("map.Servers")}
</p>
</div>
<div
className="border-t dark:border-neutral-700 pt-1"
style={{
maxHeight: "200px",
overflowY: "auto",
}}
>
{tooltipData.servers.map((server) => (
<button
key={server.id}
type="button"
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
onClick={() => {
sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${server.id}`);
}}
>
<span
className={`h-1.5 w-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}
/>
<span className="text-xs">{server.name}</span>
</button>
))}
</div>
</m.div>
</AnimatePresence>
);
});
export default MapTooltip
export default MapTooltip;
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -1,23 +1,23 @@
import { Loader } from "@/components/loading/Loader"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader } from "@/components/loading/Loader";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function NetworkChartLoading() {
return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
<CardTitle className="flex items-center gap-0.5 text-xl">
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
</CardTitle>
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
</div>
<div className="hidden pr-4 pt-4 sm:block">
<Loader visible={true} />
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<div className="aspect-auto h-[250px] w-full"></div>
</CardContent>
</Card>
)
return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
<CardTitle className="flex items-center gap-0.5 text-xl">
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
</CardTitle>
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
</div>
<div className="hidden pr-4 pt-4 sm:block">
<Loader visible={true} />
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<div className="aspect-auto h-[250px] w-full"></div>
</CardContent>
</Card>
);
}
+81 -54
View File
@@ -1,58 +1,85 @@
import { PublicNoteData, cn } from "@/lib/utils"
import { cn, type PublicNoteData } from "@/lib/utils";
export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) {
if (!parsedData || !parsedData.planDataMod) {
return null
}
export default function PlanInfo({
parsedData,
}: {
parsedData: PublicNoteData;
}) {
if (!parsedData || !parsedData.planDataMod) {
return null;
}
const extraList =
parsedData.planDataMod.extra.split(",").length > 1
? parsedData.planDataMod.extra.split(",")
: parsedData.planDataMod.extra.split(",")[0] === ""
? []
: [parsedData.planDataMod.extra]
const extraList =
parsedData.planDataMod.extra.split(",").length > 1
? parsedData.planDataMod.extra.split(",")
: parsedData.planDataMod.extra.split(",")[0] === ""
? []
: [parsedData.planDataMod.extra];
const networkRoutes = parsedData.planDataMod.networkRoute
? parsedData.planDataMod.networkRoute.split(",")
: [];
return (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && (
<p className={cn("text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
{parsedData.planDataMod.bandwidth}
</p>
)}
{parsedData.planDataMod.trafficVol !== "" && (
<p className={cn("text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
{parsedData.planDataMod.trafficVol}
</p>
)}
{parsedData.planDataMod.IPv4 === "1" && (
<p
className={cn("text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
>
IPv4
</p>
)}
{parsedData.planDataMod.IPv6 === "1" && (
<p className={cn("text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
IPv6
</p>
)}
{parsedData.planDataMod.networkRoute && (
<p className={cn("text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
{parsedData.planDataMod.networkRoute.split(",").map((route, index) => {
return route + (index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "")
})}
</p>
)}
{extraList.map((extra, index) => {
return (
<p
key={index}
className={cn("text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
>
{extra}
</p>
)
})}
</section>
)
return (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && (
<p
className={cn(
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.bandwidth}
</p>
)}
{parsedData.planDataMod.trafficVol !== "" && (
<p
className={cn(
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.trafficVol}
</p>
)}
{parsedData.planDataMod.IPv4 === "1" && (
<p
className={cn(
"text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
IPv4
</p>
)}
{parsedData.planDataMod.IPv6 === "1" && (
<p
className={cn(
"text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
IPv6
</p>
)}
{parsedData.planDataMod.networkRoute && (
<p
className={cn(
"text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{networkRoutes.map((route, index) => {
return route + (index === networkRoutes.length - 1 ? "" : "");
})}
</p>
)}
{extraList.map((extra, index) => {
return (
<p
key={index}
className={cn(
"text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{extra}
</p>
);
})}
</section>
);
}
+24 -12
View File
@@ -1,15 +1,27 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
import { Progress } from "./ui/progress"
import { Progress } from "./ui/progress";
export default function RemainPercentBar({ value, className }: { value: number; className?: string }) {
return (
<Progress
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={value < 30 ? "bg-red-500" : value < 70 ? "bg-orange-400" : "bg-green-500"}
className={cn("h-[3px] rounded-sm w-[70px]", className)}
/>
)
export default function RemainPercentBar({
value,
className,
}: {
value: number;
className?: string;
}) {
return (
<Progress
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={
value < 30
? "bg-red-500"
: value < 70
? "bg-orange-400"
: "bg-green-500"
}
className={cn("h-[3px] rounded-sm w-[70px]", className)}
/>
);
}
+31
View File
@@ -0,0 +1,31 @@
"use client";
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { useCommand } from "@/hooks/use-command";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
export function SearchButton() {
const { openCommand } = useCommand();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return (
<Button
variant="outline"
size="sm"
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
"bg-white/70 dark:bg-black/70": customBackgroundImage,
})}
onClick={openCommand}
title="Search"
>
<MagnifyingGlassIcon className="size-4" />
<span className="sr-only">Search</span>
</Button>
);
}
+271 -187
View File
@@ -1,196 +1,280 @@
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { formatBytes } from "@/lib/format"
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import ServerFlag from "@/components/ServerFlag";
import ServerUsageBar from "@/components/ServerUsageBar";
import { formatBytes } from "@/lib/format";
import {
GetFontLogoClass,
GetOsName,
MageMicrosoftWindows,
} from "@/lib/logo-class";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
import type { NezhaServer } from "@/types/nezha-api";
import BillingInfo from "./billingInfo";
import PlanInfo from "./PlanInfo";
import { Badge } from "./ui/badge";
import { Card } from "./ui/card";
import PlanInfo from "./PlanInfo"
import BillingInfo from "./billingInfo"
import { Badge } from "./ui/badge"
import { Card } from "./ui/card"
export default function ServerCard({
now,
serverInfo,
}: {
now: number;
serverInfo: NezhaServer;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const {
name,
country_code,
online,
cpu,
up,
down,
mem,
stg,
net_in_transfer,
net_out_transfer,
public_note,
platform,
} = formatNezhaInfo(now, serverInfo);
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
const { t } = useTranslation()
const navigate = useNavigate()
const { name, country_code, online, cpu, up, down, mem, stg, net_in_transfer, net_out_transfer, public_note, platform } = formatNezhaInfo(
now,
serverInfo,
)
const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${serverInfo.id}`);
};
const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true")
navigate(`/server/${serverInfo.id}`)
}
const showFlag = true;
const showFlag = true
const customBackgroundImage =
(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;
// @ts-expect-error ShowNetTransfer is a global variable
const showNetTransfer = window.ShowNetTransfer as boolean
// @ts-expect-error FixedTopServerName is a global variable
const fixedTopServerName = window.FixedTopServerName as boolean;
// @ts-expect-error FixedTopServerName is a global variable
const fixedTopServerName = window.FixedTopServerName as boolean
const parsedData = parsePublicNote(public_note);
const parsedData = parsePublicNote(public_note)
return online ? (
<Card
className={cn(
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
{
"flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName,
},
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2", {
"lg:w-40": !fixedTopServerName,
})}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
<div
className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</div>
</section>
<div
className={cn("flex items-center gap-2 -mt-2 lg:hidden", {
"lg:flex": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
<div className="flex flex-col lg:items-start items-center gap-2">
<section
className={cn("grid grid-cols-5 items-center gap-3", {
"lg:grid-cols-6 lg:gap-4": fixedTopServerName,
})}
>
{fixedTopServerName && (
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}>
<div className="text-xs font-semibold">
{platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" />
) : (
<p className={`fl-${GetFontLogoClass(platform)}`} />
)}
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div>
</div>
</div>
)}
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
<div className="flex items-center text-xs font-semibold">
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
</div>
</div>
</section>
{showNetTransfer && (
<section className={"flex items-center w-full justify-between gap-1"}>
<Badge
variant="secondary"
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
>
{t("serverCard.upload")}:{formatBytes(net_out_transfer)}
</Badge>
<Badge
variant="outline"
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
>
{t("serverCard.download")}:{formatBytes(net_in_transfer)}
</Badge>
</section>
)}
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div>
</Card>
) : (
<Card
className={cn(
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]",
{
"flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName,
},
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2", {
"lg:w-40": !fixedTopServerName,
})}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p className={cn("break-normal font-bold tracking-tight max-w-[108px]", showFlag ? "text-xs" : "text-sm")}>{name}</p>
<div
className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</div>
</section>
<div
className={cn("flex items-center gap-2 lg:hidden", {
"lg:flex": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
)
return online ? (
<Card
className={cn(
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
{
"flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName,
},
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2", {
"lg:w-40": !fixedTopServerName,
})}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p
className={cn(
"break-normal font-bold tracking-tight",
showFlag ? "text-xs " : "text-sm",
)}
>
{name}
</p>
<div
className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</div>
</section>
<div
className={cn("flex items-center gap-2 -mt-2 lg:hidden", {
"lg:flex": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
<div className="flex flex-col lg:items-start items-center gap-2">
<section
className={cn("grid grid-cols-5 items-center gap-3", {
"lg:grid-cols-6 lg:gap-4": fixedTopServerName,
})}
>
{fixedTopServerName && (
<div
className={
"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"
}
>
<div className="text-xs font-semibold">
{platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" />
) : (
<p className={`fl-${GetFontLogoClass(platform)}`} />
)}
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.system")}
</p>
<div className="flex items-center text-[10.5px] font-semibold">
{platform.includes("Windows")
? "Windows"
: GetOsName(platform)}
</div>
</div>
</div>
)}
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: up >= 1
? `${up.toFixed(2)}M/s`
: `${(up * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<div className="flex items-center text-xs font-semibold">
{down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: down >= 1
? `${down.toFixed(2)}M/s`
: `${(down * 1024).toFixed(2)}K/s`}
</div>
</div>
</section>
{showNetTransfer && (
<section className={"flex items-center w-full justify-between gap-1"}>
<Badge
variant="secondary"
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
>
{t("serverCard.upload")}:{formatBytes(net_out_transfer)}
</Badge>
<Badge
variant="outline"
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
>
{t("serverCard.download")}:{formatBytes(net_in_transfer)}
</Badge>
</section>
)}
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div>
</Card>
) : (
<Card
className={cn(
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
showNetTransfer
? "lg:min-h-[91px] min-h-[123px]"
: "lg:min-h-[61px] min-h-[93px]",
{
"flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName,
},
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2", {
"lg:w-40": !fixedTopServerName,
})}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p
className={cn(
"break-normal font-bold tracking-tight max-w-[108px]",
showFlag ? "text-xs" : "text-sm",
)}
>
{name}
</p>
<div
className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</div>
</section>
<div
className={cn("flex items-center gap-2 lg:hidden", {
"lg:flex": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
);
}
+233 -136
View File
@@ -1,143 +1,240 @@
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { formatBytes } from "@/lib/format"
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import ServerFlag from "@/components/ServerFlag";
import ServerUsageBar from "@/components/ServerUsageBar";
import { formatBytes } from "@/lib/format";
import {
GetFontLogoClass,
GetOsName,
MageMicrosoftWindows,
} from "@/lib/logo-class";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
import type { NezhaServer } from "@/types/nezha-api";
import BillingInfo from "./billingInfo";
import PlanInfo from "./PlanInfo";
import { Card } from "./ui/card";
import { Separator } from "./ui/separator";
import PlanInfo from "./PlanInfo"
import BillingInfo from "./billingInfo"
import { Card } from "./ui/card"
import { Separator } from "./ui/separator"
export default function ServerCardInline({
now,
serverInfo,
}: {
now: number;
serverInfo: NezhaServer;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const {
name,
country_code,
online,
cpu,
up,
down,
mem,
stg,
platform,
uptime,
net_in_transfer,
net_out_transfer,
public_note,
} = formatNezhaInfo(now, serverInfo);
export default function ServerCardInline({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
const { t } = useTranslation()
const navigate = useNavigate()
const { name, country_code, online, cpu, up, down, mem, stg, platform, uptime, net_in_transfer, net_out_transfer, public_note } = formatNezhaInfo(
now,
serverInfo,
)
const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${serverInfo.id}`);
};
const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true")
navigate(`/server/${serverInfo.id}`)
}
const showFlag = true;
const showFlag = true
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const parsedData = parsePublicNote(public_note);
const parsedData = parsePublicNote(public_note)
return online ? (
<section>
<Card
className={cn(
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative w-28 flex flex-col">
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</section>
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className="flex flex-col gap-1">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
<div className="text-xs font-semibold">
{platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" />
) : (
<p className={`fl-${GetFontLogoClass(platform)}`} />
)}
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div>
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p>
<div className="flex items-center text-xs font-semibold">
{uptime / 86400 >= 1
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
: `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`}
</div>
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
<div className="flex items-center text-xs font-semibold">
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p>
<div className="flex items-center text-xs font-semibold">{formatBytes(net_out_transfer)}</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p>
<div className="flex items-center text-xs font-semibold">{formatBytes(net_in_transfer)}</div>
</div>
</section>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div>
</Card>
</section>
) : (
<Card
className={cn(
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p className={cn("break-normal font-bold w-28 tracking-tight", showFlag ? "text-xs" : "text-sm")}>{name}</p>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
</section>
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
)
return online ? (
<section>
<Card
className={cn(
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2 lg:w-36")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative w-28 flex flex-col">
<p
className={cn(
"break-normal font-bold tracking-tight",
showFlag ? "text-xs " : "text-sm",
)}
>
{name}
</p>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</section>
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className="flex flex-col gap-1">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
>
<div className="text-xs font-semibold">
{platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" />
) : (
<p className={`fl-${GetFontLogoClass(platform)}`} />
)}
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.system")}
</p>
<div className="flex items-center text-[10.5px] font-semibold">
{platform.includes("Windows")
? "Windows"
: GetOsName(platform)}
</div>
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.uptime")}
</p>
<div className="flex items-center text-xs font-semibold">
{uptime / 86400 >= 1
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
: `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`}
</div>
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: up >= 1
? `${up.toFixed(2)}M/s`
: `${(up * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<div className="flex items-center text-xs font-semibold">
{down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: down >= 1
? `${down.toFixed(2)}M/s`
: `${(down * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.totalUpload")}
</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_out_transfer)}
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.totalDownload")}
</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_in_transfer)}
</div>
</div>
</section>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div>
</Card>
</section>
) : (
<Card
className={cn(
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2 w-40")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p
className={cn(
"break-normal font-bold w-28 tracking-tight",
showFlag ? "text-xs" : "text-sm",
)}
>
{name}
</p>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</section>
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
);
}
File diff suppressed because it is too large Load Diff
+357 -290
View File
@@ -1,310 +1,377 @@
import { BackIcon } from "@/components/Icon"
import ServerFlag from "@/components/ServerFlag"
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatBytes } from "@/lib/format"
import { cn, formatNezhaInfo } from "@/lib/utils"
import { NezhaWebsocketResponse } from "@/types/nezha-api"
import countries from "i18n-iso-countries"
import enLocale from "i18n-iso-countries/langs/en.json"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import countries from "i18n-iso-countries";
import enLocale from "i18n-iso-countries/langs/en.json";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { BackIcon } from "@/components/Icon";
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
import ServerFlag from "@/components/ServerFlag";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatBytes } from "@/lib/format";
import { cn, formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
export default function ServerDetailOverview({ server_id }: { server_id: string }) {
const { t } = useTranslation()
const navigate = useNavigate()
export default function ServerDetailOverview({
server_id,
}: {
server_id: string;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const [hasHistory, setHasHistory] = useState(false)
const [hasHistory, setHasHistory] = useState(false);
useEffect(() => {
const previousPath = sessionStorage.getItem("fromMainPage")
if (previousPath) {
setHasHistory(true)
}
}, [])
useEffect(() => {
const previousPath = sessionStorage.getItem("fromMainPage");
if (previousPath) {
setHasHistory(true);
}
}, []);
const { lastMessage, connected } = useWebSocketContext()
const { lastMessage, connected } = useWebSocketContext();
if (!connected && !lastMessage) {
return <ServerDetailLoading />
}
if (!connected && !lastMessage) {
return <ServerDetailLoading />;
}
const linkClick = () => {
if (hasHistory) {
navigate(-1)
} else {
navigate("/")
}
}
const linkClick = () => {
if (hasHistory) {
navigate(-1);
} else {
navigate("/");
}
};
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
if (!nezhaWsData) {
return <ServerDetailLoading />
}
if (!nezhaWsData) {
return <ServerDetailLoading />;
}
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
if (!server) {
return <ServerDetailLoading />
}
if (!server) {
return <ServerDetailLoading />;
}
const {
name,
online,
uptime,
version,
arch,
mem_total,
disk_total,
country_code,
platform,
platform_version,
cpu_info,
gpu_info,
load_1,
load_5,
load_15,
net_out_transfer,
net_in_transfer,
last_active_time_string,
boot_time_string,
} = formatNezhaInfo(nezhaWsData.now, server)
const {
name,
online,
uptime,
version,
arch,
mem_total,
disk_total,
country_code,
platform,
platform_version,
cpu_info,
gpu_info,
load_1,
load_5,
load_15,
net_out_transfer,
net_in_transfer,
last_active_time_string,
boot_time_string,
} = formatNezhaInfo(nezhaWsData.now, server);
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
countries.registerLocale(enLocale)
countries.registerLocale(enLocale);
return (
<div
className={cn({
"bg-card/70 p-4 rounded-[10px]": customBackgroundImage,
})}
>
<div
onClick={linkClick}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
>
<BackIcon />
{name}
</div>
<section className="flex flex-wrap gap-2 mt-3">
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p>
<Badge
className={cn("text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", {
" bg-green-800": online,
" bg-red-600": !online,
})}
>
{online ? t("serverDetail.online") : t("serverDetail.offline")}
</Badge>
</section>
</CardContent>
</Card>
{online && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.uptime")}</p>
<div className="text-xs">
{" "}
{uptime / 86400 >= 1
? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}`
: `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`}
</div>
</section>
</CardContent>
</Card>
)}
{version && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.version")}</p>
<div className="text-xs">{version} </div>
</section>
</CardContent>
</Card>
)}
{arch && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.arch")}</p>
<div className="text-xs">{arch} </div>
</section>
</CardContent>
</Card>
)}
return (
<div
className={cn({
"bg-card/70 p-4 rounded-[10px]": customBackgroundImage,
})}
>
<div
onClick={linkClick}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
>
<BackIcon />
{name}
</div>
<section className="flex flex-wrap gap-2 mt-3">
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.status")}
</p>
<Badge
className={cn(
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
{
" bg-green-800": online,
" bg-red-600": !online,
},
)}
>
{online ? t("serverDetail.online") : t("serverDetail.offline")}
</Badge>
</section>
</CardContent>
</Card>
{online && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.uptime")}
</p>
<div className="text-xs">
{" "}
{uptime / 86400 >= 1
? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}`
: `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`}
</div>
</section>
</CardContent>
</Card>
)}
{version && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.version")}
</p>
<div className="text-xs">{version} </div>
</section>
</CardContent>
</Card>
)}
{arch && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.arch")}
</p>
<div className="text-xs">{arch} </div>
</section>
</CardContent>
</Card>
)}
{mem_total ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.mem")}</p>
<div className="text-xs">{formatBytes(mem_total)}</div>
</section>
</CardContent>
</Card>
) : null}
{mem_total ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.mem")}
</p>
<div className="text-xs">{formatBytes(mem_total)}</div>
</section>
</CardContent>
</Card>
) : null}
{disk_total ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.disk")}</p>
<div className="text-xs">{formatBytes(disk_total)}</div>
</section>
</CardContent>
</Card>
) : null}
{disk_total ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.disk")}
</p>
<div className="text-xs">{formatBytes(disk_total)}</div>
</section>
</CardContent>
</Card>
) : null}
{country_code && (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
<section className="flex items-start gap-1">
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
</section>
</section>
</CardContent>
</Card>
</TooltipTrigger>
<TooltipContent>
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</section>
<section className="flex flex-wrap gap-2 mt-1">
{platform && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.system")}</p>
<div className="text-xs">
{" "}
{platform} {platform_version ? " - " + platform_version : ""}
</div>
</section>
</CardContent>
</Card>
)}
{cpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="text-xs"> {cpu_info.join(", ")}</div>
</section>
</CardContent>
</Card>
)}
{gpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"GPU"}</p>
<div className="text-xs">{gpu_info.join(", ")}</div>
</section>
</CardContent>
</Card>
)}
</section>
<section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Load"}</p>
<div className="text-xs">
{load_1} / {load_5} / {load_15}
</div>
</section>
</CardContent>
</Card>
{net_out_transfer ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.upload")}</p>
{net_out_transfer ? (
<div className="text-xs"> {formatBytes(net_out_transfer)} </div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
</section>
</CardContent>
</Card>
) : null}
{net_in_transfer ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.download")}</p>
{net_in_transfer ? (
<div className="text-xs"> {formatBytes(net_in_transfer)} </div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
</section>
</CardContent>
</Card>
) : null}
</section>
<section className="flex flex-wrap gap-2 mt-1">
{server?.state.temperatures && server?.state.temperatures.length > 0 && (
<section className="flex flex-wrap gap-2 ml-1.5">
<Accordion type="single" collapsible className="w-fit">
<AccordionItem value="item-1" className="border-none">
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">{t("serverDetail.temperature")}</AccordionTrigger>
<AccordionContent className="pb-0">
<section className="flex items-start flex-wrap gap-2">
{server?.state.temperatures.map((item, index) => (
<div className="text-xs flex items-center" key={index}>
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C
</div>
))}
</section>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
)}
</section>
{country_code && (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.region")}
</p>
<section className="flex items-start gap-1">
<div className="text-xs text-start">
{country_code?.toUpperCase()}
</div>
{country_code && (
<ServerFlag
className="text-[11px] -mt-px"
country_code={country_code}
/>
)}
</section>
</section>
</CardContent>
</Card>
</TooltipTrigger>
<TooltipContent>
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</section>
<section className="flex flex-wrap gap-2 mt-1">
{platform && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.system")}
</p>
<div className="text-xs">
{" "}
{platform} {platform_version ? ` - ${platform_version}` : ""}
</div>
</section>
</CardContent>
</Card>
)}
{cpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="text-xs"> {cpu_info.join(", ")}</div>
</section>
</CardContent>
</Card>
)}
{gpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"GPU"}</p>
<div className="text-xs">{gpu_info.join(", ")}</div>
</section>
</CardContent>
</Card>
)}
</section>
<section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Load"}</p>
<div className="text-xs">
{load_1} / {load_5} / {load_15}
</div>
</section>
</CardContent>
</Card>
{net_out_transfer ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.upload")}
</p>
{net_out_transfer ? (
<div className="text-xs">
{" "}
{formatBytes(net_out_transfer)}{" "}
</div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
</section>
</CardContent>
</Card>
) : null}
{net_in_transfer ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.download")}
</p>
{net_in_transfer ? (
<div className="text-xs">
{" "}
{formatBytes(net_in_transfer)}{" "}
</div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
</section>
</CardContent>
</Card>
) : null}
</section>
<section className="flex flex-wrap gap-2 mt-1">
{server?.state.temperatures &&
server?.state.temperatures.length > 0 && (
<section className="flex flex-wrap gap-2 ml-1.5">
<Accordion type="single" collapsible className="w-fit">
<AccordionItem value="item-1" className="border-none">
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">
{t("serverDetail.temperature")}
</AccordionTrigger>
<AccordionContent className="pb-0">
<section className="flex items-start flex-wrap gap-2">
{server?.state.temperatures.map((item, index) => (
<div className="text-xs flex items-center" key={index}>
<p className="font-semibold">{item.Name}</p>:{" "}
{item.Temperature.toFixed(2)} °C
</div>
))}
</section>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
)}
</section>
<section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.bootTime")}</p>
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p>
<div className="text-xs">{last_active_time_string ? last_active_time_string : "N/A"}</div>
</section>
</CardContent>
</Card>
</section>
</div>
)
<section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.bootTime")}
</p>
<div className="text-xs">
{boot_time_string ? boot_time_string : "N/A"}
</div>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.lastActive")}
</p>
<div className="text-xs">
{last_active_time_string ? last_active_time_string : "N/A"}
</div>
</section>
</CardContent>
</Card>
</section>
</div>
);
}
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { Progress } from "@/components/ui/progress";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export default function ServerDetailSummary({
server_id,
}: {
server_id: number;
}) {
const { lastMessage, connected } = useWebSocketContext();
if (!connected && !lastMessage) {
return null;
}
const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
if (!nezhaWsData) {
return null;
}
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
if (!server) {
return null;
}
const { cpu, mem, disk, up, down, tcp, udp, process } = formatNezhaInfo(
nezhaWsData.now,
server,
);
return (
<div className="mb-2 flex flex-wrap items-center gap-4 server-detail-summary">
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
<section className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">CPU</span>
<span className="font-medium text-[10px]">{cpu.toFixed(2)}%</span>
</section>
<UsageBar value={cpu} />
</section>
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
<section className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">Mem</span>
<span className="font-medium text-[10px]">{mem.toFixed(2)}%</span>
</section>
<UsageBar value={mem} />
</section>
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
<section className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">Disk</span>
<span className="font-medium text-[10px]">{disk.toFixed(2)}%</span>
</section>
<UsageBar value={disk} />
</section>
<section className="flex min-w-[85px] flex-col justify-center px-1.5 py-1">
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">Process</span>
<span className="font-medium text-[10px]">{process}</span>
</section>
</section>
<section className="flex min-w-[70px] flex-col justify-center gap-0.5 px-1.5 py-1">
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">TCP</span>
<span className="font-medium text-[10px]">{tcp}</span>
</section>
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">UDP</span>
<span className="font-medium text-[10px]">{udp}</span>
</section>
</section>
<section className="flex min-w-[120px] flex-col justify-center gap-0.5 px-1.5 py-1">
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">Upload</span>
<span className="font-medium text-[10px]">{up.toFixed(2)}M/s</span>
</section>
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">Download</span>
<span className="font-medium text-[10px]">{down.toFixed(2)}M/s</span>
</section>
</section>
</div>
);
}
type UsageBarProps = {
value: number;
};
function UsageBar({ value }: UsageBarProps) {
return (
<Progress
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={
value > 90
? "bg-red-500"
: value > 70
? "bg-orange-400"
: "bg-green-500"
}
className={"h-[3px] rounded-sm bg-stone-200 dark:bg-stone-800"}
/>
);
}
+43 -33
View File
@@ -1,42 +1,52 @@
import { cn } from "@/lib/utils"
import getUnicodeFlagIcon from "country-flag-icons/unicode"
import { useEffect, useState } from "react"
import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
export default function ServerFlag({ country_code, className }: { country_code: string; className?: string }) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
export default function ServerFlag({
country_code,
className,
}: {
country_code: string;
className?: string;
}) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
// @ts-expect-error ForceUseSvgFlag is a global variable
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean
// @ts-expect-error ForceUseSvgFlag is a global variable
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean;
useEffect(() => {
if (forceUseSvgFlag) {
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
setSupportsEmojiFlags(false)
return
}
useEffect(() => {
if (forceUseSvgFlag) {
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
setSupportsEmojiFlags(false);
return;
}
const checkEmojiSupport = () => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试
if (!ctx) return
ctx.fillStyle = "#000"
ctx.textBaseline = "top"
ctx.font = "32px Arial"
ctx.fillText(emojiFlag, 0, 0)
const checkEmojiSupport = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
if (!ctx) return;
ctx.fillStyle = "#000";
ctx.textBaseline = "top";
ctx.font = "32px Arial";
ctx.fillText(emojiFlag, 0, 0);
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0
setSupportsEmojiFlags(support)
}
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
setSupportsEmojiFlags(support);
};
checkEmojiSupport()
}, [])
checkEmojiSupport();
}, [forceUseSvgFlag]);
if (!country_code) return null
if (!country_code) return null;
return (
<span className={cn("text-[12px] text-muted-foreground", className)}>
{forceUseSvgFlag || !supportsEmojiFlags ? <span className={`fi fi-${country_code}`} /> : getUnicodeFlagIcon(country_code)}
</span>
)
return (
<span className={cn("text-[12px] text-muted-foreground", className)}>
{forceUseSvgFlag || !supportsEmojiFlags ? (
<span className={`fi fi-${country_code}`} />
) : (
getUnicodeFlagIcon(country_code)
)}
</span>
);
}
+200 -149
View File
@@ -1,158 +1,209 @@
// src/components/ServerOverview.tsx (最终完整版)
import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { useStatus } from "@/hooks/use-status";
import { formatBytes } from "@/lib/format";
import { cn } from "@/lib/utils";
import { Globe } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"
import { useStatus } from "@/hooks/use-status"
import { formatBytes} from "@/lib/format"
import { cn } from "@/lib/utils"
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
import { useTranslation } from "react-i18next"
import { Globe } from "lucide-react"
// 扩展 props 类型,以接收域名总数和新的交互逻辑
type ServerOverviewProps = {
online: number
offline: number
total: number
up: number
down: number
upSpeed: number
downSpeed: number
totalDomains: number // 新增:接收域名总数
onViewChange: (view: 'servers' | 'domains') => void // 新增:点击事件回调
activeView: 'servers' | 'domains' // 新增:当前激活的视图
}
online: number;
offline: number;
total: number;
up: number;
down: number;
upSpeed: number;
upSpeed: number;
downSpeed: number;
totalDomains: number; // 新增:接收域名总数
onViewChange: (view: 'servers' | 'domains') => void; // 新增:点击事件回调
activeView: 'servers' | 'domains'; // 新增:当前激活的视图
};
export default function ServerOverview({
online,
offline,
total,
up,
down,
upSpeed,
downSpeed,
totalDomains,
onViewChange,
activeView,
online,
offline,
total,
up,
down,
upSpeed,
downSpeed,
totalDomains,
onViewChange,
activeView,
}: ServerOverviewProps) {
const { t } = useTranslation()
const { status, setStatus } = useStatus()
const { t } = useTranslation();
const { status, setStatus } = useStatus();
// --- 所有原始变量和逻辑保持不变 ---
const disableAnimatedMan = (window as any).DisableAnimatedMan as boolean
const customIllustration = (window as any).CustomIllustration || "/animated-man.webp"
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined
// @ts-expect-error DisableAnimatedMan is a global variable
const disableAnimatedMan = window.DisableAnimatedMan as boolean;
// 新增:一个组合了两个动作的点击处理函数
const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => {
onViewChange('servers'); // 动作1: 确保视图切换回服务器
setStatus(serverStatus); // 动作2: 执行原有的状态筛选
}
// @ts-expect-error CustomIllustration is a global variable
const customIllustration = window.CustomIllustration || "/animated-man.webp";
return (
<>
<section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview">
<Card
onClick={() => handleServerCardClick("all")}
className={cn(
"hover:border-blue-500 cursor-pointer transition-all",
{ "bg-card/70": customBackgroundImage },
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalServers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
</span>
<div className="text-lg font-semibold">{total}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => handleServerCardClick("online")}
className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage },
{ "ring-green-500 ring-2 border-transparent": activeView === 'servers' && status === "online" }
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.onlineServers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span>
<div className="text-lg font-semibold">{online}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => handleServerCardClick("offline")}
className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage },
{ "ring-red-500 ring-2 border-transparent": activeView === 'servers' && status === "offline" }
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.offlineServers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</span>
<div className="text-lg font-semibold">{offline}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => onViewChange('domains')}
className={cn(
"cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage },
{ "ring-indigo-500 ring-2 border-transparent": activeView === 'domains' }
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"></p>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<div className="text-lg font-semibold">{totalDomains}</div>
</div>
</section>
</CardContent>
</Card>
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
<Card
className={cn("hover:ring-purple-500 ring-1 ring-transparent transition-all", { "bg-card/70": customBackgroundImage })}
>
<CardContent className="flex h-full items-center relative px-6 py-3">
<section className="flex flex-col gap-1 w-full">
<div className="flex items-center w-full justify-between"><p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p></div>
<section className="flex items-start flex-row z-10 pr-0 gap-1">
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">{formatBytes(up)}</p>
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">{formatBytes(down)}</p>
</section>
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-[1px]" />{formatBytes(upSpeed)}/s</p>
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowDownCircleIcon className="size-3 mr-0.5" />{formatBytes(downSpeed)}/s</p>
</section>
</section>
{!disableAnimatedMan && (
<img className="absolute right-[-30px] top-[-120px] z-10 w-40 scale-100 group-hover:opacity-50 md:scale-100 transition-all" alt={"animated-man"} src={customIllustration} loading="eager" />
)}
</CardContent>
</Card>
</section>
</>
)
}
// 新增:一个组合了两个动作的点击处理函数
const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => {
onViewChange('servers'); // 动作1: 确保视图切换回服务器
setStatus(serverStatus); // 动作2: 执行原有的状态筛选
};
return (
<section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview">
<Card
onClick={() => {
handleServerCardClick("all");
}}
className={cn("hover:border-blue-500 cursor-pointer transition-all", {
"bg-card/70": customBackgroundImage,
})}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.totalServers")}
</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
</span>
<div className="text-lg font-semibold">{total}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => {
handleServerCardClick("online");
}}
className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
{
"ring-green-500 ring-2 border-transparent": activeView === "servers" && status === "online",
},
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.onlineServers")}
</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span>
<div className="text-lg font-semibold">{online}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => {
handleServerCardClick("offline");
}}
className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
{
"ring-red-500 ring-2 border-transparent": activeView === "servers" && status === "offline",
},
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.offlineServers")}
</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</span>
<div className="text-lg font-semibold">{offline}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => onViewChange("domains")}
className={cn(
"cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
{
"ring-indigo-500 ring-2 border-transparent": activeView === "domains",
},
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"></p>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<div className="text-lg font-semibold">{totalDomains}</div>
</div>
</section>
</CardContent>
</Card>
<Card
className={cn(
"hover:ring-purple-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
)}
>
<CardContent className="flex h-full items-center relative px-6 py-3">
<section className="flex flex-col gap-1 w-full">
<div className="flex items-center w-full justify-between">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.network")}
</p>
</div>
<section className="flex items-start flex-row z-10 pr-0 gap-1">
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
{formatBytes(up)}
</p>
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
{formatBytes(down)}
</p>
</section>
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
<p className="text-[11px] flex items-center text-nowrap font-semibold">
<ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-px" />
{formatBytes(upSpeed)}/s
</p>
<p className="text-[11px] flex items-center text-nowrap font-semibold">
<ArrowDownCircleIcon className="size-3 mr-0.5" />
{formatBytes(downSpeed)}/s
</p>
</section>
</section>
{!disableAnimatedMan && (
<img
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
alt={"animated-man"}
src={customIllustration}
loading="eager"
/>
)}
</CardContent>
</Card>
</section>
);
}
+18 -12
View File
@@ -1,17 +1,23 @@
import { Progress } from "@/components/ui/progress"
import { Progress } from "@/components/ui/progress";
type ServerUsageBarProps = {
value: number
}
value: number;
};
export default function ServerUsageBar({ value }: ServerUsageBarProps) {
return (
<Progress
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={value > 90 ? "bg-red-500" : value > 70 ? "bg-orange-400" : "bg-green-500"}
className={"h-[3px] rounded-sm"}
/>
)
return (
<Progress
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={
value > 90
? "bg-red-500"
: value > 70
? "bg-orange-400"
: "bg-green-500"
}
className={"h-[3px] rounded-sm"}
/>
);
}
+88 -67
View File
@@ -1,79 +1,100 @@
import { fetchService } from "@/lib/nezha-api"
import { NezhaServer, ServiceData } from "@/types/nezha-api"
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"
import { useQuery } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { fetchService } from "@/lib/nezha-api";
import type { NezhaServer, ServiceData } from "@/types/nezha-api";
import { CycleTransferStatsCard } from "./CycleTransferStats"
import ServiceTrackerClient from "./ServiceTrackerClient"
import { Loader } from "./loading/Loader"
import { CycleTransferStatsCard } from "./CycleTransferStats";
import { Loader } from "./loading/Loader";
import ServiceTrackerClient from "./ServiceTrackerClient";
export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
const { t } = useTranslation()
const { data: serviceData, isLoading } = useQuery({
queryKey: ["service"],
queryFn: () => fetchService(),
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: 10000,
})
const { t } = useTranslation();
const { data: serviceData, isLoading } = useQuery({
queryKey: ["service"],
queryFn: () => fetchService(),
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: 10000,
});
const processServiceData = (serviceData: ServiceData) => {
const days = serviceData.up.map((up, index) => {
const totalChecks = up + serviceData.down[index]
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0
return {
completed: up > serviceData.down[index],
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
uptime: dailyUptime,
delay: serviceData.delay[index] || 0,
}
})
const processServiceData = (serviceData: ServiceData) => {
const days = serviceData.up.map((up, index) => {
const totalChecks = up + serviceData.down[index];
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0;
return {
completed: up > serviceData.down[index],
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
uptime: dailyUptime,
delay: serviceData.delay[index] || 0,
};
});
const totalUp = serviceData.up.reduce((a, b) => a + b, 0)
const totalChecks = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
const uptime = (totalUp / totalChecks) * 100
const totalUp = serviceData.up.reduce((a, b) => a + b, 0);
const totalChecks =
serviceData.up.reduce((a, b) => a + b, 0) +
serviceData.down.reduce((a, b) => a + b, 0);
const uptime = (totalUp / totalChecks) * 100;
const avgDelay = serviceData.delay.length > 0 ? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length : 0
const avgDelay =
serviceData.delay.length > 0
? serviceData.delay.reduce((a, b) => a + b, 0) /
serviceData.delay.length
: 0;
return { days, uptime, avgDelay }
}
return { days, uptime, avgDelay };
};
if (isLoading) {
return (
<div className="mt-4 text-sm font-medium flex items-center gap-1">
<Loader visible={true} />
{t("serviceTracker.loading")}
</div>
)
}
if (isLoading) {
return (
<div className="mt-4 text-sm font-medium flex items-center gap-1">
<Loader visible={true} />
{t("serviceTracker.loading")}
</div>
);
}
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) {
return (
<div className="mt-4 text-sm font-medium flex items-center gap-1">
<ExclamationTriangleIcon className="w-4 h-4" />
{t("serviceTracker.noService")}
</div>
)
}
if (
!serviceData?.data?.services &&
!serviceData?.data?.cycle_transfer_stats
) {
return (
<div className="mt-4 text-sm font-medium flex items-center gap-1">
<ExclamationTriangleIcon className="w-4 h-4" />
{t("serviceTracker.noService")}
</div>
);
}
return (
<div className="mt-4 w-full mx-auto ">
{serviceData.data.cycle_transfer_stats && (
<div>
<CycleTransferStatsCard serverList={serverList} cycleStats={serviceData.data.cycle_transfer_stats} />
</div>
)}
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && (
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
{Object.entries(serviceData.data.services).map(([name, data]) => {
const { days, uptime, avgDelay } = processServiceData(data)
return <ServiceTrackerClient key={name} days={days} title={data.service_name} uptime={uptime} avgDelay={avgDelay} />
})}
</section>
)}
</div>
)
return (
<div className="mt-4 w-full mx-auto ">
{serviceData.data.cycle_transfer_stats && (
<div>
<CycleTransferStatsCard
serverList={serverList}
cycleStats={serviceData.data.cycle_transfer_stats}
/>
</div>
)}
{serviceData.data.services &&
Object.keys(serviceData.data.services).length > 0 && (
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
{Object.entries(serviceData.data.services).map(([name, data]) => {
const { days, uptime, avgDelay } = processServiceData(data);
return (
<ServiceTrackerClient
key={name}
days={days}
title={data.service_name}
uptime={uptime}
avgDelay={avgDelay}
/>
);
})}
</section>
)}
</div>
);
}
export default ServiceTracker
export default ServiceTracker;
+154 -106
View File
@@ -1,118 +1,166 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import React from "react"
import { useTranslation } from "react-i18next"
import type React from "react";
import { useTranslation } from "react-i18next";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Separator } from "./ui/separator"
import { Separator } from "./ui/separator";
interface ServiceTrackerProps {
days: Array<{
completed: boolean
date?: Date
uptime: number
delay: number
}>
className?: string
title?: string
uptime?: number
avgDelay?: number
days: Array<{
completed: boolean;
date?: Date;
uptime: number;
delay: number;
}>;
className?: string;
title?: string;
uptime?: number;
avgDelay?: number;
}
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
const { t } = useTranslation()
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
days,
className,
title,
uptime = 100,
avgDelay = 0,
}) => {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const getUptimeColor = (uptime: number) => {
if (uptime >= 99) return "text-emerald-500"
if (uptime >= 95) return "text-amber-500"
return "text-rose-500"
}
const getUptimeColor = (uptime: number) => {
if (uptime >= 99) return "text-emerald-500";
if (uptime >= 95) return "text-amber-500";
return "text-rose-500";
};
const getDelayColor = (delay: number) => {
if (delay < 100) return "text-emerald-500"
if (delay < 300) return "text-amber-500"
return "text-rose-500"
}
const getDelayColor = (delay: number) => {
if (delay < 100) return "text-emerald-500";
if (delay < 300) return "text-amber-500";
return "text-rose-500";
};
const getStatusColor = (uptime: number) => {
if (uptime >= 99) return "bg-emerald-500"
if (uptime >= 95) return "bg-amber-500"
return "bg-rose-500"
}
const getStatusColor = (uptime: number) => {
if (uptime >= 99) return "bg-emerald-500";
if (uptime >= 95) return "bg-amber-500";
return "bg-rose-500";
};
return (
<div
className={cn(
"w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
{
"bg-card/70": customBackgroundImage,
},
)}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} />
<span className="font-medium text-sm">{title}</span>
</div>
<div className="flex items-center gap-3">
<span className={cn("font-medium text-sm transition-colors", getDelayColor(avgDelay))}>{avgDelay.toFixed(0)}ms</span>
<Separator className="h-4" orientation="vertical" />
<span className={cn("font-medium text-sm transition-colors", getUptimeColor(uptime))}>
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
</span>
</div>
</div>
return (
<div
className={cn(
"w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
{
"bg-card/70": customBackgroundImage,
},
)}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div
className={cn(
"w-2.5 h-2.5 rounded-full transition-colors",
getStatusColor(uptime),
)}
/>
<span className="font-medium text-sm">{title}</span>
</div>
<div className="flex items-center gap-3">
<span
className={cn(
"font-medium text-sm transition-colors",
getDelayColor(avgDelay),
)}
>
{avgDelay.toFixed(0)}ms
</span>
<Separator className="h-4" orientation="vertical" />
<span
className={cn(
"font-medium text-sm transition-colors",
getUptimeColor(uptime),
)}
>
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
</span>
</div>
</div>
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
{days.map((day, index) => (
<TooltipProvider delayDuration={50} key={index}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_theme(colors.white/10%)]",
day.completed
? "bg-gradient-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_theme(colors.green.600/30%)]"
: "bg-gradient-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_theme(colors.red.600/30%)]",
)}
/>
</TooltipTrigger>
<TooltipContent className="p-0 overflow-hidden">
<div className="px-3 py-2 bg-popover">
<p className="font-medium text-sm mb-2">{day.date?.toLocaleDateString()}</p>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span>
<span className={cn("text-xs font-medium", day.uptime > 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}%</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">{t("serviceTracker.delay")}:</span>
<span
className={cn(
"text-xs font-medium",
day.delay < 100 ? "text-green-500" : day.delay < 300 ? "text-yellow-500" : "text-red-500",
)}
>
{day.delay.toFixed(0)}ms
</span>
</div>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
{days.map((day, index) => (
<TooltipProvider delayDuration={50} key={index}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_--theme(--color-white/10%)]",
day.completed
? "bg-linear-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_--theme(--color-green-600/30%)]"
: "bg-linear-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_--theme(--color-red-600/30%)]",
)}
/>
</TooltipTrigger>
<TooltipContent className="p-0 overflow-hidden">
<div className="px-3 py-2 bg-popover">
<p className="font-medium text-sm mb-2">
{day.date?.toLocaleDateString()}
</p>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">
{t("serviceTracker.uptime")}:
</span>
<span
className={cn(
"text-xs font-medium",
day.uptime > 95 ? "text-green-500" : "text-red-500",
)}
>
{day.uptime.toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">
{t("serviceTracker.delay")}:
</span>
<span
className={cn(
"text-xs font-medium",
day.delay < 100
? "text-green-500"
: day.delay < 300
? "text-yellow-500"
: "text-red-500",
)}
>
{day.delay.toFixed(0)}ms
</span>
</div>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
<span>30 {t("serviceTracker.daysAgo")}</span>
<span>{t("serviceTracker.today")}</span>
</div>
</div>
)
}
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
<span>30 {t("serviceTracker.daysAgo")}</span>
<span>{t("serviceTracker.today")}</span>
</div>
</div>
);
};
export default ServiceTrackerClient
export default ServiceTrackerClient;
+56 -40
View File
@@ -1,42 +1,58 @@
import { cn } from "@/lib/utils"
import { m } from "framer-motion"
import { useTranslation } from "react-i18next"
import { m } from "framer-motion";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: string[]; currentTab: string; setCurrentTab: (tab: string) => void }) {
const { t } = useTranslation()
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
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,
})}
>
{tabs.map((tab: string) => (
<div
key={tab}
onClick={() => setCurrentTab(tab)}
className={cn(
"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 && (
<m.div
layoutId="tab-switch-active"
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={{
originY: "0px",
borderRadius: 46,
}}
/>
)}
<div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{t("tabSwitch." + tab)}</p>
</div>
</div>
))}
</div>
</div>
)
export default function TabSwitch({
tabs,
currentTab,
setCurrentTab,
}: {
tabs: string[];
currentTab: string;
setCurrentTab: (tab: string) => void;
}) {
const { t } = useTranslation();
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
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,
},
)}
>
{tabs.map((tab: string) => (
<div
key={tab}
onClick={() => setCurrentTab(tab)}
className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
currentTab === tab
? "text-black dark:text-white"
: "text-stone-400 dark:text-stone-500",
)}
>
{currentTab === tab && (
<m.div
layoutId="tab-switch-active"
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={{
originY: "0px",
borderRadius: 46,
}}
/>
)}
<div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{t(`tabSwitch.${tab}`)}</p>
</div>
</div>
))}
</div>
</div>
);
}
+29 -27
View File
@@ -1,39 +1,41 @@
"use client"
"use client";
import { useTheme } from "@/hooks/use-theme"
import { useEffect } from "react"
import { useEffect } from "react";
import { useTheme } from "@/hooks/use-theme";
export function ThemeColorManager() {
const { theme } = useTheme()
const { theme } = useTheme();
useEffect(() => {
const updateThemeColor = () => {
const currentTheme = theme
const meta = document.querySelector('meta[name="theme-color"]')
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)
}
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%)" // 浅色模式背景色
const themeColor =
currentTheme === "dark"
? "hsl(30 15% 8%)" // 深色模式背景色
: "hsl(0 0% 98%)"; // 浅色模式背景色
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
}
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
};
// Update on mount and theme change
updateThemeColor()
// Update on mount and theme change
updateThemeColor();
// Listen for system theme changes
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
mediaQuery.addEventListener("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 () => mediaQuery.removeEventListener("change", updateThemeColor);
}, [theme]);
return null
return null;
}
+72 -47
View File
@@ -1,56 +1,81 @@
import { ReactNode, createContext, useEffect, useState } from "react"
import { createContext, type ReactNode, useEffect, useState } from "react";
export type Theme = "dark" | "light" | "system"
export type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: ReactNode
defaultTheme?: Theme
storageKey?: string
}
children: ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
storageKey = "vite-ui-theme",
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || "system",
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.add("disable-transitions");
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);
const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions");
}, 0);
return () => window.clearTimeout(timeoutId);
}
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);
const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions");
}, 0);
return () => window.clearTimeout(timeoutId);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || "system")
useEffect(() => {
const root = window.document.documentElement
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>
}
export { ThemeProviderContext }
export { ThemeProviderContext };
+69 -46
View File
@@ -1,53 +1,76 @@
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 { CheckCircleIcon } from "@heroicons/react/20/solid";
import { Moon, Sun } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { 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 { useTheme } from "../hooks/use-theme"
import { useTheme } from "../hooks/use-theme";
export function ModeToggle() {
const { t } = useTranslation()
const { setTheme, theme } = useTheme()
const { t } = useTranslation();
const { setTheme, theme } = useTheme();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const handleSelect = (e: Event, newTheme: Theme) => {
e.preventDefault()
setTheme(newTheme)
}
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>
)
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("rounded-b-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "light",
})}
onSelect={(e) => handleSelect(e, "light")}
>
{t("theme.light")}
{theme === "light" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
<DropdownMenuItem
className={cn("rounded-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "dark",
})}
onSelect={(e) => handleSelect(e, "dark")}
>
{t("theme.dark")}
{theme === "dark" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
<DropdownMenuItem
className={cn("rounded-t-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "system",
})}
onSelect={(e) => handleSelect(e, "system")}
>
{t("theme.system")}
{theme === "system" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+100 -62
View File
@@ -1,68 +1,106 @@
import { PublicNoteData, cn, getDaysBetweenDatesWithAutoRenewal } from "@/lib/utils"
import { useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next";
import {
cn,
getDaysBetweenDatesWithAutoRenewal,
type PublicNoteData,
} from "@/lib/utils";
import RemainPercentBar from "./RemainPercentBar"
import RemainPercentBar from "./RemainPercentBar";
export default function BillingInfo({ parsedData }: { parsedData: PublicNoteData }) {
const { t } = useTranslation()
if (!parsedData || !parsedData.billingDataMod) {
return null
}
export default function BillingInfo({
parsedData,
}: {
parsedData: PublicNoteData;
}) {
const { t } = useTranslation();
if (!parsedData || !parsedData.billingDataMod) {
return null;
}
let isNeverExpire = false
let daysLeftObject = {
days: 0,
cycleLabel: "",
remainingPercentage: 0,
}
let isNeverExpire = false;
let daysLeftObject = {
days: 0,
cycleLabel: "",
remainingPercentage: 0,
};
const hasBillingDates =
Boolean(parsedData.billingDataMod.startDate) ||
Boolean(parsedData.billingDataMod.endDate);
if (parsedData?.billingDataMod?.endDate) {
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
isNeverExpire = true
} else {
try {
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(parsedData.billingDataMod)
} catch (error) {
console.error(error)
return (
<div className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.remaining")}: {t("billingInfo.error")}
</div>
)
}
}
}
if (parsedData?.billingDataMod?.endDate) {
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
isNeverExpire = true;
} else {
try {
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(
parsedData.billingDataMod,
);
} catch (error) {
console.error(error);
return (
<div className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.remaining")}: {t("billingInfo.error")}
</div>
);
}
}
}
return daysLeftObject.days >= 0 ? (
<>
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p>
) : null}
<div className={cn("text-[10px] text-muted-foreground")}>
{t("billingInfo.remaining")}: {isNeverExpire ? t("billingInfo.indefinite") : daysLeftObject.days + " " + t("billingInfo.days")}
</div>
{!isNeverExpire && <RemainPercentBar className="mt-0.5" value={daysLeftObject.remainingPercentage * 100} />}
</>
) : (
<>
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p>
) : null}
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.expired")}: {daysLeftObject.days * -1} {t("billingInfo.days")}
</p>
</>
)
return daysLeftObject.days >= 0 ? (
<>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}>
{t("billingInfo.free")}
</p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}>
{t("billingInfo.usage-baseed")}
</p>
) : null}
{hasBillingDates && (
<div className={cn("text-[10px] text-muted-foreground")}>
{t("billingInfo.remaining")}:{" "}
{isNeverExpire
? t("billingInfo.indefinite")
: `${daysLeftObject.days} ${t("billingInfo.days")}`}
</div>
)}
{hasBillingDates && !isNeverExpire && (
<RemainPercentBar
className="mt-0.5"
value={daysLeftObject.remainingPercentage * 100}
/>
)}
</>
) : (
<>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}>
{t("billingInfo.free")}
</p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}>
{t("billingInfo.usage-baseed")}
</p>
) : null}
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.expired")}: {daysLeftObject.days * -1}{" "}
{t("billingInfo.days")}
</p>
</>
);
}
+24
View File
@@ -0,0 +1,24 @@
export default function ChartSkeleton({
width,
height,
}: {
width?: number | string;
height?: number | string;
}) {
const resolvedWidth = typeof width === "number" ? `${width}px` : width;
const resolvedHeight = typeof height === "number" ? `${height}px` : height;
return (
<div
className="relative h-full w-full overflow-hidden"
style={{
width: resolvedWidth || "100%",
height: resolvedHeight || "100%",
}}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="size-4 rounded-full border-2 border-muted-foreground/20 border-t-muted-foreground/70 animate-spin" />
</div>
</div>
);
}
+28 -28
View File
@@ -1,32 +1,32 @@
const bars = Array(8).fill(0)
const bars = Array(8).fill(0);
export const Loader = ({ visible }: { visible: boolean }) => {
return (
<div className="hamster-loading-wrapper" data-visible={visible}>
<div className="hamster-spinner">
{bars.map((_, i) => (
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
))}
</div>
</div>
)
}
return (
<div className="hamster-loading-wrapper" data-visible={visible}>
<div className="hamster-spinner">
{bars.map((_, i) => (
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
))}
</div>
</div>
);
};
export const LoadingSpinner = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={"size-4 animate-spin"}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
)
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={"size-4 animate-spin"}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
};
+30 -30
View File
@@ -1,38 +1,38 @@
import { Skeleton } from "@/components/ui/skeleton"
import { useNavigate } from "react-router-dom"
import { useNavigate } from "react-router-dom";
import { Skeleton } from "@/components/ui/skeleton";
import { BackIcon } from "../Icon"
import { BackIcon } from "../Icon";
export function ServerDetailChartLoading() {
return (
<div>
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section>
</div>
)
return (
<div>
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section>
</div>
);
}
export function ServerDetailLoading() {
const navigate = useNavigate()
const navigate = useNavigate();
return (
<div className="mx-auto w-full max-w-5xl px-0">
<div
onClick={() => {
navigate("/")
}}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
>
<BackIcon />
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div>
)
return (
<div className="mx-auto w-full max-w-5xl px-0">
<div
onClick={() => {
navigate("/");
}}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
>
<BackIcon />
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div>
);
}
+1 -1
View File
@@ -1 +1 @@
export { domMax as default } from "framer-motion"
export { domMax as default } from "framer-motion";
+9 -8
View File
@@ -1,11 +1,12 @@
import { LazyMotion } from "framer-motion"
import { LazyMotion } from "framer-motion";
const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
const loadFeatures = () =>
import("./framer-lazy-feature").then((res) => res.default);
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
return (
<LazyMotion features={loadFeatures} strict key="framer">
{children}
</LazyMotion>
)
}
return (
<LazyMotion features={loadFeatures} strict key="framer">
{children}
</LazyMotion>
);
};
+44 -38
View File
@@ -1,49 +1,55 @@
import { cn } from "@/lib/utils"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />)
AccordionItem.displayName = "AccordionItem"
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -1,89 +1,107 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
interface Props {
max: number
value: number
min: number
className?: string
primaryColor?: string
max: number;
value: number;
min: number;
className?: string;
primaryColor?: string;
}
export default function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, primaryColor, className }: Props) {
const circumference = 2 * Math.PI * 45
const percentPx = circumference / 100
const currentPercent = ((value - min) / (max - min)) * 100
export default function AnimatedCircularProgressBar({
max = 100,
min = 0,
value = 0,
primaryColor,
className,
}: Props) {
const circumference = 2 * Math.PI * 45;
const percentPx = circumference / 100;
const currentPercent = ((value - min) / (max - min)) * 100;
return (
<div
className={cn("relative size-40 text-2xl font-semibold", className)}
style={
{
"--circle-size": "100px",
"--circumference": circumference,
"--percent-to-px": `${percentPx}px`,
"--gap-percent": "5",
"--offset-factor": "0",
"--transition-length": "1s",
"--transition-step": "200ms",
"--delay": "0s",
"--percent-to-deg": "3.6deg",
transform: "translateZ(0)",
} as React.CSSProperties
}
>
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
{currentPercent <= 90 && currentPercent >= 0 && (
<circle
cx="50"
cy="50"
r="45"
strokeWidth="10"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
className="opacity-100 stroke-muted"
style={
{
"--stroke-percent": 90 - currentPercent,
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transform: "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
transition: "all var(--transition-length) ease var(--delay)",
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
)}
<circle
cx="50"
cy="50"
r="45"
strokeWidth="10"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
className={cn("opacity-100 stroke-current", {
"stroke-[var(--stroke-primary-color)]": primaryColor,
})}
style={
{
"--stroke-primary-color": primaryColor,
"--stroke-percent": currentPercent,
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transition: "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
transitionProperty: "stroke-dasharray,transform",
transform: "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
</svg>
<span
data-current-value={currentPercent}
className="duration-&lsqb;var(--transition-length)&rsqb; delay-&lsqb;var(--delay)&rsqb; absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
>
{currentPercent}
</span>
</div>
)
return (
<div
className={cn("relative size-40 text-2xl font-semibold", className)}
style={
{
"--circle-size": "100px",
"--circumference": circumference,
"--percent-to-px": `${percentPx}px`,
"--gap-percent": "5",
"--offset-factor": "0",
"--transition-length": "1s",
"--transition-step": "200ms",
"--delay": "0s",
"--percent-to-deg": "3.6deg",
transform: "translateZ(0)",
} as React.CSSProperties
}
>
<svg
fill="none"
className="size-full"
strokeWidth="2"
viewBox="0 0 100 100"
>
{currentPercent <= 90 && currentPercent >= 0 && (
<circle
cx="50"
cy="50"
r="45"
strokeWidth="10"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
className="opacity-100 stroke-muted"
style={
{
"--stroke-percent": 90 - currentPercent,
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
strokeDasharray:
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transform:
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
transition: "all var(--transition-length) ease var(--delay)",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
)}
<circle
cx="50"
cy="50"
r="45"
strokeWidth="10"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
className={cn("opacity-100 stroke-current", {
"stroke-(--stroke-primary-color)": primaryColor,
})}
style={
{
"--stroke-primary-color": primaryColor,
"--stroke-percent": currentPercent,
strokeDasharray:
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transition:
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
transitionProperty: "stroke-dasharray,transform",
transform:
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
</svg>
<span
data-current-value={currentPercent}
className="duration-&lsqb;var(--transition-length)&rsqb; delay-&lsqb;var(--delay)&rsqb; absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
>
{currentPercent}
</span>
</div>
);
}
+28 -21
View File
@@ -1,28 +1,35 @@
import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };
+49 -36
View File
@@ -1,42 +1,55 @@
import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
})
Button.displayName = "Button"
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };
+78 -31
View File
@@ -1,38 +1,85 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", className)}
{...props}
/>
))
Card.displayName = "Card"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
))
CardHeader.displayName = "CardHeader"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
))
CardTitle.displayName = "CardTitle"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
CardDescription.displayName = "CardDescription"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
))
CardFooter.displayName = "CardFooter"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
+346 -226
View File
@@ -1,277 +1,397 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
}
[k: string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig
}
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null)
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext)
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
}
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null
}
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
)
}
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
HTMLDivElement,
React.ComponentProps<"div"> & {
active?: boolean;
payload?: any[];
label?: any;
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
labelFormatter?: (value: any, payload: any[]) => React.ReactNode;
formatter?: (
value: any,
name: any,
item: any,
index: number,
payload: any,
) => React.ReactNode;
color?: string;
labelClassName?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart()
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
}
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null
}
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot"
payload.sort((a, b) => {
return Number(b.value) - Number(a.value);
});
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && <span className="font-mono font-medium tabular-nums text-foreground">{item.value.toLocaleString()}</span>}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
},
)
ChartTooltipContent.displayName = "ChartTooltip"
return (
<div
ref={ref}
className={cn(
"grid min-w-32 items-start gap-1.5 overflow-hidden rounded-sm border border-border/50 bg-stone-100 text-xs dark:bg-stone-900",
className,
)}
>
{!nestLabel && (
<div className="mx-auto -mb-1 px-2.5 pt-1">
{!nestLabel ? tooltipLabel : null}
</div>
)}
const ChartLegend = RechartsPrimitive.Legend
<div
className={cn("grid gap-1.5 bg-white px-2.5 py-1.5 dark:bg-black", {
"border-t": !nestLabel,
})}
>
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-border bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span
className={cn(
"ml-2 font-medium text-foreground tabular-nums",
payload.length === 1 && "-ml-9",
)}
>
{typeof item.value === "number"
? item.value.toFixed(2).toLocaleString()
: item.value}{" "}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart()
HTMLDivElement,
React.ComponentProps<"div"> & {
payload?: any[];
verticalAlign?: "top" | "bottom" | "middle";
hideIcon?: boolean;
nameKey?: string;
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref,
) => {
const { config } = useChart();
if (!payload?.length) {
return null
}
if (!payload?.length) {
return null;
}
return (
<div ref={ref} className={cn("flex flex-wrap items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
ref={ref}
className={cn(
"flex flex-wrap items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div key={item.value} className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
})
ChartLegendContent.displayName = "ChartLegend"
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{key}
</div>
);
})}
</div>
);
},
);
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string") {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
}
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};
+25 -22
View File
@@ -1,24 +1,27 @@
import { cn } from "@/lib/utils"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>>(
({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
),
)
Checkbox.displayName = CheckboxPrimitive.Root.displayName
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
export { Checkbox };
+141 -89
View File
@@ -1,107 +1,159 @@
"use client"
"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"
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";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
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 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>
)
}
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 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>
),
)
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center 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-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName
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} />
),
)
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
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} />,
)
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
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}
/>
),
)
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
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
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}
/>
),
)
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-hidden 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
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"
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 }
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
+104 -60
View File
@@ -1,76 +1,120 @@
import { cn } from "@/lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
const Dialog = DialogPrimitive.Root
import { cn } from "@/lib/utils";
const DialogTrigger = DialogPrimitive.Trigger
const Dialog = DialogPrimitive.Root;
const DialogPortal = DialogPrimitive.Portal
const DialogTrigger = DialogPrimitive.Trigger;
const DialogClose = DialogPrimitive.Close
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>(
({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
),
)
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />)
DialogDescription.displayName = DialogPrimitive.Description.displayName
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+165 -140
View File
@@ -1,172 +1,197 @@
import { cn } from "@/lib/utils"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => <DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />)
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
+21 -18
View File
@@ -1,21 +1,24 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };
+19 -10
View File
@@ -1,14 +1,23 @@
import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />)
Label.displayName = LabelPrimitive.Root.displayName
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };
+22 -22
View File
@@ -1,28 +1,28 @@
import { cn } from "@/lib/utils"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent }
export { Popover, PopoverTrigger, PopoverContent };
+26 -16
View File
@@ -1,20 +1,30 @@
import { cn } from "@/lib/utils"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string
}
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
}
>(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root ref={ref} className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)} {...props}>
<ProgressPrimitive.Indicator
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className={cn(
"h-full w-full flex-1 bg-primary transition-all",
indicatorClassName,
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress }
export { Progress };
+135 -104
View File
@@ -1,126 +1,157 @@
import { cn } from "@/lib/utils"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(
({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
),
)
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>(
({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
),
)
SelectItem.displayName = SelectPrimitive.Item.displayName
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />)
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
+26 -16
View File
@@ -1,18 +1,28 @@
import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<React.ElementRef<typeof SeparatorPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>(
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
{...props}
/>
),
)
Separator.displayName = SeparatorPrimitive.Root.displayName
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator }
export { Separator };
+12 -4
View File
@@ -1,7 +1,15 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton }
export { Skeleton };
+24 -23
View File
@@ -1,25 +1,26 @@
import { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(
({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
),
)
Switch.displayName = SwitchPrimitives.Root.displayName
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch }
export { Switch };
+107 -41
View File
@@ -1,50 +1,116 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
))
Table.displayName = "Table"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
))
TableBody.displayName = "TableBody"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
))
TableFooter.displayName = "TableFooter"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium last:[&>tr]:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => (
<tr ref={ref} className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />
))
TableRow.displayName = "TableRow"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
))
TableCell.displayName = "TableCell"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
))
TableCaption.displayName = "TableCaption"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
+20 -20
View File
@@ -1,27 +1,27 @@
import { cn } from "@/lib/utils"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+12
View File
@@ -0,0 +1,12 @@
import { createContext } from "react";
export interface CommandContextType {
isOpen: boolean;
openCommand: () => void;
closeCommand: () => void;
toggleCommand: () => void;
}
export const CommandContext = createContext<CommandContextType | undefined>(
undefined,
);
+24
View File
@@ -0,0 +1,24 @@
import { type ReactNode, useCallback, useState } from "react";
import { CommandContext } from "./command-context";
export function CommandProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const openCommand = useCallback(() => setIsOpen(true), []);
const closeCommand = useCallback(() => setIsOpen(false), []);
const toggleCommand = useCallback(() => setIsOpen((prev) => !prev), []);
return (
<CommandContext.Provider
value={{
isOpen,
openCommand,
closeCommand,
toggleCommand,
}}
>
{children}
</CommandContext.Provider>
);
}
+35 -10
View File
@@ -1,18 +1,43 @@
import { createContext } from "react"
import { createContext } from "react";
export type SortType = "default" | "name" | "uptime" | "system" | "cpu" | "mem" | "disk" | "up" | "down" | "up total" | "down total"
export type SortType =
| "default"
| "name"
| "uptime"
| "system"
| "cpu"
| "mem"
| "disk"
| "up"
| "down"
| "up total"
| "down total";
export const SORT_TYPES: SortType[] = ["default", "name", "uptime", "system", "cpu", "mem", "disk", "up", "down", "up total", "down total"]
export const SORT_TYPES: SortType[] = [
"default",
"name",
"uptime",
"system",
"cpu",
"mem",
"disk",
"up",
"down",
"up total",
"down total",
];
export type SortOrder = "asc" | "desc"
export type SortOrder = "asc" | "desc";
export const SORT_ORDERS: SortOrder[] = ["desc", "asc"]
export const SORT_ORDERS: SortOrder[] = ["desc", "asc"];
export interface SortContextType {
sortType: SortType
sortOrder: SortOrder
setSortType: (sortType: SortType) => void
setSortOrder: (sortOrder: SortOrder) => void
sortType: SortType;
sortOrder: SortOrder;
setSortType: (sortType: SortType) => void;
setSortOrder: (sortOrder: SortOrder) => void;
}
export const SortContext = createContext<SortContextType | undefined>(undefined)
export const SortContext = createContext<SortContextType | undefined>(
undefined,
);
+11 -5
View File
@@ -1,10 +1,16 @@
import { ReactNode, useState } from "react"
import { type ReactNode, useState } from "react";
import { SortContext, SortOrder, SortType } from "./sort-context"
import { SortContext, type SortOrder, type SortType } from "./sort-context";
export function SortProvider({ children }: { children: ReactNode }) {
const [sortType, setSortType] = useState<SortType>("default")
const [sortOrder, setSortOrder] = useState<SortOrder>("desc")
const [sortType, setSortType] = useState<SortType>("default");
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
return <SortContext.Provider value={{ sortType, setSortType, sortOrder, setSortOrder }}>{children}</SortContext.Provider>
return (
<SortContext.Provider
value={{ sortType, setSortType, sortOrder, setSortOrder }}
>
{children}
</SortContext.Provider>
);
}
+7 -5
View File
@@ -1,10 +1,12 @@
import { createContext } from "react"
import { createContext } from "react";
export type Status = "all" | "online" | "offline"
export type Status = "all" | "online" | "offline";
export interface StatusContextType {
status: Status
setStatus: (status: Status) => void
status: Status;
setStatus: (status: Status) => void;
}
export const StatusContext = createContext<StatusContextType | undefined>(undefined)
export const StatusContext = createContext<StatusContextType | undefined>(
undefined,
);
+8 -4
View File
@@ -1,9 +1,13 @@
import { ReactNode, useState } from "react"
import { type ReactNode, useState } from "react";
import { Status, StatusContext } from "./status-context"
import { type Status, StatusContext } from "./status-context";
export function StatusProvider({ children }: { children: ReactNode }) {
const [status, setStatus] = useState<Status>("all")
const [status, setStatus] = useState<Status>("all");
return <StatusContext.Provider value={{ status, setStatus }}>{children}</StatusContext.Provider>
return (
<StatusContext.Provider value={{ status, setStatus }}>
{children}
</StatusContext.Provider>
);
}
+14 -11
View File
@@ -1,18 +1,21 @@
import { createContext } from "react"
import { createContext } from "react";
export interface TooltipData {
centroid: [number, number]
country: string
count: number
servers: Array<{
name: string
status: boolean
}>
centroid: [number, number];
country: string;
count: number;
servers: Array<{
id: number;
name: string;
status: boolean;
}>;
}
interface TooltipContextType {
tooltipData: TooltipData | null
setTooltipData: (data: TooltipData | null) => void
tooltipData: TooltipData | null;
setTooltipData: (data: TooltipData | null) => void;
}
export const TooltipContext = createContext<TooltipContextType | undefined>(undefined)
export const TooltipContext = createContext<TooltipContextType | undefined>(
undefined,
);
+8 -4
View File
@@ -1,9 +1,13 @@
import { ReactNode, useState } from "react"
import { type ReactNode, useState } from "react";
import { TooltipContext, TooltipData } from "./tooltip-context"
import { TooltipContext, type TooltipData } from "./tooltip-context";
export function TooltipProvider({ children }: { children: ReactNode }) {
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null)
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null);
return <TooltipContext.Provider value={{ tooltipData, setTooltipData }}>{children}</TooltipContext.Provider>
return (
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
{children}
</TooltipContext.Provider>
);
}
+14 -14
View File
@@ -1,19 +1,19 @@
import { createContext } from "react"
import { createContext } from "react";
export interface WebSocketContextType {
lastMessage: { data: string } | null
connected: boolean
messageHistory: { data: string }[]
reconnect: () => void
needReconnect: boolean
setNeedReconnect: (needReconnect: boolean) => void
lastMessage: { data: string } | null;
connected: boolean;
messageHistory: { data: string }[];
reconnect: () => void;
needReconnect: boolean;
setNeedReconnect: (needReconnect: boolean) => void;
}
export const WebSocketContext = createContext<WebSocketContextType>({
lastMessage: null,
connected: false,
messageHistory: [],
reconnect: () => {},
needReconnect: false,
setNeedReconnect: () => {},
})
lastMessage: null,
connected: false,
messageHistory: [],
reconnect: () => {},
needReconnect: false,
setNeedReconnect: () => {},
});
+123 -109
View File
@@ -1,132 +1,146 @@
import React, { useEffect, useRef, useState } from "react"
import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { WebSocketContext, WebSocketContextType } from "./websocket-context"
import {
WebSocketContext,
type WebSocketContextType,
} from "./websocket-context";
interface WebSocketProviderProps {
url: string
children: React.ReactNode
url: string;
children: React.ReactNode;
}
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => {
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态
const [connected, setConnected] = useState(false)
const [needReconnect, setNeedReconnect] = useState(false)
const ws = useRef<WebSocket | null>(null)
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
const maxReconnectAttempts = 30
const reconnectAttempts = useRef(0)
const isConnecting = useRef(false)
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
url,
children,
}) => {
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null);
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]); // 新增历史消息状态
const [connected, setConnected] = useState(false);
const [needReconnect, setNeedReconnect] = useState(false);
const ws = useRef<WebSocket | null>(null);
const reconnectTimeout = useRef<NodeJS.Timeout>(null);
const maxReconnectAttempts = 30;
const reconnectAttempts = useRef(0);
const isConnecting = useRef(false);
const cleanup = () => {
if (ws.current) {
// 移除所有事件监听器
ws.current.onopen = null
ws.current.onclose = null
ws.current.onmessage = null
ws.current.onerror = null
const cleanup = useCallback(() => {
if (ws.current) {
// 移除所有事件监听器
ws.current.onopen = null;
ws.current.onclose = null;
ws.current.onmessage = null;
ws.current.onerror = null;
if (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING) {
ws.current.close()
}
ws.current = null
}
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current)
reconnectTimeout.current = null
}
setConnected(false)
}
if (
ws.current.readyState === WebSocket.OPEN ||
ws.current.readyState === WebSocket.CONNECTING
) {
ws.current.close();
}
ws.current = null;
}
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
reconnectTimeout.current = null;
}
setConnected(false);
}, []);
const connect = () => {
if (isConnecting.current) {
console.log("Connection already in progress")
return
}
const connect = useCallback(() => {
if (isConnecting.current) {
console.log("Connection already in progress");
return;
}
cleanup()
isConnecting.current = true
cleanup();
isConnecting.current = true;
try {
const wsUrl = new URL(url, window.location.origin)
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
try {
const wsUrl = new URL(url, window.location.origin);
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
ws.current = new WebSocket(wsUrl.toString())
ws.current = new WebSocket(wsUrl.toString());
ws.current.onopen = () => {
console.log("WebSocket connected")
setConnected(true)
reconnectAttempts.current = 0
isConnecting.current = false
}
ws.current.onopen = () => {
console.log("WebSocket connected");
setConnected(true);
reconnectAttempts.current = 0;
isConnecting.current = false;
};
ws.current.onclose = () => {
console.log("WebSocket disconnected")
setConnected(false)
ws.current = null
isConnecting.current = false
ws.current.onclose = () => {
console.log("WebSocket disconnected");
setConnected(false);
ws.current = null;
isConnecting.current = false;
if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectTimeout.current = setTimeout(() => {
reconnectAttempts.current++
connect()
}, 3000)
}
}
if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectTimeout.current = setTimeout(() => {
reconnectAttempts.current++;
connect();
}, 3000);
}
};
ws.current.onmessage = (event) => {
const newMessage = { data: event.data }
setLastMessage(newMessage)
// 更新历史消息,保持最新的30条记录
setMessageHistory((prev) => {
const updated = [newMessage, ...prev]
return updated.slice(0, 30)
})
}
ws.current.onmessage = (event) => {
const newMessage = { data: event.data };
setLastMessage(newMessage);
// 更新历史消息,保持最新的30条记录
setMessageHistory((prev) => {
const updated = [newMessage, ...prev];
return updated.slice(0, 30);
});
};
ws.current.onerror = (error) => {
console.error("WebSocket error:", error)
isConnecting.current = false
}
} catch (error) {
console.error("WebSocket connection error:", error)
isConnecting.current = false
}
}
ws.current.onerror = (error) => {
console.error("WebSocket error:", error);
isConnecting.current = false;
};
} catch (error) {
console.error("WebSocket connection error:", error);
isConnecting.current = false;
}
}, [cleanup, url]);
const reconnect = () => {
reconnectAttempts.current = 0
// 等待一个小延时确保清理完成
cleanup()
setTimeout(() => {
connect()
}, 1000)
}
const reconnect = () => {
reconnectAttempts.current = 0;
// 等待一个小延时确保清理完成
cleanup();
setTimeout(() => {
connect();
}, 1000);
};
useEffect(() => {
connect()
useEffect(() => {
connect();
// 添加页面卸载事件监听
const handleBeforeUnload = () => {
cleanup()
}
// 添加页面卸载事件监听
const handleBeforeUnload = () => {
cleanup();
};
window.addEventListener("beforeunload", handleBeforeUnload)
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
cleanup()
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [url])
return () => {
cleanup();
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [cleanup, connect]);
const contextValue: WebSocketContextType = {
lastMessage,
connected,
messageHistory,
reconnect,
needReconnect,
setNeedReconnect,
}
const contextValue: WebSocketContextType = {
lastMessage,
connected,
messageHistory,
reconnect,
needReconnect,
setNeedReconnect,
};
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
}
return (
<WebSocketContext.Provider value={contextValue}>
{children}
</WebSocketContext.Provider>
);
};
+54 -46
View File
@@ -1,60 +1,68 @@
import { useEffect, useState } from "react"
import { useEffect, useState } from "react";
declare global {
interface Window {
CustomBackgroundImage: string
CustomMobileBackgroundImage: string
ForceShowServices: boolean
ForceCardInline: boolean
ForceShowMap: boolean
ForcePeakCutEnabled: boolean
}
interface Window {
CustomBackgroundImage: string;
CustomMobileBackgroundImage: string;
ForceShowServices: boolean;
ForceCardInline: boolean;
ForceShowMap: boolean;
ForcePeakCutEnabled: boolean;
}
}
const BACKGROUND_CHANGE_EVENT = "backgroundChange"
const BACKGROUND_CHANGE_EVENT = "backgroundChange";
export function useBackground() {
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(undefined)
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(
undefined,
);
useEffect(() => {
// 监听背景变化
const handleBackgroundChange = () => {
setBackgroundImage(window.CustomBackgroundImage || 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 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)
// 设置一个轮询来检查初始背景
const intervalId = setInterval(() => {
if (
window.CustomBackgroundImage ||
sessionStorage.getItem("savedBackgroundImage")
) {
checkInitialBackground();
clearInterval(intervalId);
}
}, 100);
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange);
return () => {
window.removeEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
clearInterval(intervalId)
}
}, [])
return () => {
window.removeEventListener(
BACKGROUND_CHANGE_EVENT,
handleBackgroundChange,
);
clearInterval(intervalId);
};
}, []);
const updateBackground = (newBackground: string | undefined) => {
window.CustomBackgroundImage = newBackground || ""
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT))
}
const updateBackground = (newBackground: string | undefined) => {
window.CustomBackgroundImage = newBackground || "";
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT));
};
return { backgroundImage, updateBackground }
return { backgroundImage, updateBackground };
}
+19 -19
View File
@@ -1,26 +1,26 @@
import { NezhaWebsocketResponse } from "@/types/nezha-api"
import { useEffect, useState } from "react"
import { useEffect, useState } from "react";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export function useChartHistory<T>(
messageHistory: { data: string }[],
serverId: number,
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
messageHistory: { data: string }[],
serverId: number,
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
) {
const [data, setData] = useState<T[]>([])
const [data, setData] = useState<T[]>([]);
useEffect(() => {
if (messageHistory.length > 0 && data.length === 0) {
const historyData = messageHistory
.map((msg) => {
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
return formatFn(wsData, serverId)
})
.filter((item): item is T => item !== null)
.reverse()
useEffect(() => {
if (messageHistory.length > 0 && data.length === 0) {
const historyData = messageHistory
.map((msg) => {
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse;
return formatFn(wsData, serverId);
})
.filter((item): item is T => item !== null)
.reverse();
setData(historyData)
}
}, [messageHistory])
setData(historyData);
}
}, [messageHistory, data.length, formatFn, serverId]);
return data
return data;
}
+10
View File
@@ -0,0 +1,10 @@
import { useContext } from "react";
import { CommandContext } from "@/context/command-context";
export function useCommand() {
const context = useContext(CommandContext);
if (context === undefined) {
throw new Error("useCommand must be used within a CommandProvider");
}
return context;
}
+7 -7
View File
@@ -1,10 +1,10 @@
import { SortContext } from "@/context/sort-context"
import { useContext } from "react"
import { useContext } from "react";
import { SortContext } from "@/context/sort-context";
export function useSort() {
const context = useContext(SortContext)
if (context === undefined) {
throw new Error("useStatus must be used within a SortProvider")
}
return context
const context = useContext(SortContext);
if (context === undefined) {
throw new Error("useStatus must be used within a SortProvider");
}
return context;
}
+7 -7
View File
@@ -1,11 +1,11 @@
import { useContext } from "react"
import { useContext } from "react";
import { StatusContext } from "../context/status-context"
import { StatusContext } from "../context/status-context";
export function useStatus() {
const context = useContext(StatusContext)
if (context === undefined) {
throw new Error("useStatus must be used within a StatusProvider")
}
return context
const context = useContext(StatusContext);
if (context === undefined) {
throw new Error("useStatus must be used within a StatusProvider");
}
return context;
}
+8 -8
View File
@@ -1,13 +1,13 @@
import { useContext } from "react"
import { useContext } from "react";
import { ThemeProviderContext } from "../components/ThemeProvider"
import { ThemeProviderContext } from "../components/ThemeProvider";
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
const context = useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider")
}
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context
}
return context;
};
+9 -9
View File
@@ -1,12 +1,12 @@
import { TooltipContext } from "@/context/tooltip-context"
import { useContext } from "react"
import { useContext } from "react";
import { TooltipContext } from "@/context/tooltip-context";
export const useTooltip = () => {
const context = useContext(TooltipContext)
if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider")
}
return context
}
const context = useContext(TooltipContext);
if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider");
}
return context;
};
export default useTooltip
export default useTooltip;
+10 -8
View File
@@ -1,11 +1,13 @@
import { useContext } from "react"
import { useContext } from "react";
import { WebSocketContext } from "../context/websocket-context"
import { WebSocketContext } from "../context/websocket-context";
export const useWebSocketContext = () => {
const context = useContext(WebSocketContext)
if (context === undefined) {
throw new Error("useWebSocketContext must be used within a WebSocketProvider")
}
return context
}
const context = useContext(WebSocketContext);
if (context === undefined) {
throw new Error(
"useWebSocketContext must be used within a WebSocketProvider",
);
}
return context;
};
+43 -43
View File
@@ -1,54 +1,54 @@
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import deTranslation from "./locales/de/translation.json"
import enTranslation from "./locales/en/translation.json"
import esTranslation from "./locales/es/translation.json"
import ruTranslation from "./locales/ru/translation.json"
import taTranslation from "./locales/ta/translation.json"
import zhCNTranslation from "./locales/zh-CN/translation.json"
import zhTWTranslation from "./locales/zh-TW/translation.json"
import deTranslation from "./locales/de/translation.json";
import enTranslation from "./locales/en/translation.json";
import esTranslation from "./locales/es/translation.json";
import ruTranslation from "./locales/ru/translation.json";
import taTranslation from "./locales/ta/translation.json";
import zhCNTranslation from "./locales/zh-CN/translation.json";
import zhTWTranslation from "./locales/zh-TW/translation.json";
const resources = {
"en-US": {
translation: enTranslation,
},
"zh-CN": {
translation: zhCNTranslation,
},
"zh-TW": {
translation: zhTWTranslation,
},
"de-DE": {
translation: deTranslation,
},
"es-ES": {
translation: esTranslation,
},
"ru-RU": {
translation: ruTranslation,
},
"ta-IN": {
translation: taTranslation,
},
}
"en-US": {
translation: enTranslation,
},
"zh-CN": {
translation: zhCNTranslation,
},
"zh-TW": {
translation: zhTWTranslation,
},
"de-DE": {
translation: deTranslation,
},
"es-ES": {
translation: esTranslation,
},
"ru-RU": {
translation: ruTranslation,
},
"ta-IN": {
translation: taTranslation,
},
};
const getStoredLanguage = () => {
return localStorage.getItem("language") || "en-US"
}
return localStorage.getItem("language") || "en-US";
};
i18n.use(initReactI18next).init({
resources,
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
fallbackLng: "en-US", // 当前语言的翻译没有找到时,使用的备选语言
interpolation: {
escapeValue: false, // react已经安全地转义
},
})
resources,
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
fallbackLng: "en-US", // 当前语言的翻译没有找到时,使用的备选语言
interpolation: {
escapeValue: false, // react已经安全地转义
},
});
// 添加语言改变时的处理函数
i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng)
})
localStorage.setItem("language", lng);
});
export default i18n
export default i18n;
+313 -164
View File
@@ -1,234 +1,383 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
@plugin "tailwindcss-animate";
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
@custom-variant dark (&:is(.dark *));
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@theme {
--font-sans: var(--font-sans);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 1rem;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
.dark {
--background: 30 15% 8%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
}
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@layer base {
* {
@apply border-border;
}
html {
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground;
/* font-feature-settings: "rlig" 1, "calt" 1; */
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
}
@utility step {
counter-increment: step;
&:before {
@apply border-background bg-muted absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 text-center -indent-px font-mono text-base font-medium;
@apply mt-[-4px] ml-[-50px];
content: counter(step);
}
}
@layer utilities {
.step {
counter-increment: step;
}
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 1rem;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
--timing: cubic-bezier(0.4, 0, 0.2, 1);
}
.dark {
--background: 30 15% 8%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
}
}
@layer base {
* {
@apply border-border;
}
html {
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground;
/* font-feature-settings: "rlig" 1, "calt" 1; */
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
}
}
@layer base {
/* Avoid color fade when toggling themes. */
html.disable-transitions *,
html.disable-transitions *::before,
html.disable-transitions *::after {
transition: none;
}
}
@media (max-width: 640px) {
.container {
@apply px-4;
}
.container {
@apply px-4;
}
}
::selection {
@apply bg-stone-300 dark:bg-stone-800;
@apply bg-stone-300 dark:bg-stone-800;
}
.hamster-loading-wrapper {
--size: 12px;
height: var(--size);
width: var(--size);
inset: 0;
z-index: 10;
--size: 12px;
height: var(--size);
width: var(--size);
inset: 0;
z-index: 10;
}
.hamster-loading-wrapper[data-visible="false"] {
transform-origin: center;
animation: hamster-fade-out 0.2s ease forwards;
transform-origin: center;
animation: hamster-fade-out 0.2s ease forwards;
}
.hamster-spinner {
position: relative;
top: 50%;
left: 50%;
height: var(--size);
width: var(--size);
position: relative;
top: 50%;
left: 50%;
height: var(--size);
width: var(--size);
}
.hamster-loading-bar {
--gray11: hsl(0, 0%, 43.5%);
animation: hamster-spin 0.8s linear infinite;
background: var(--gray11);
border-radius: 6px;
height: 13%;
left: -10%;
position: absolute;
top: -3.9%;
width: 30%;
--gray11: hsl(0, 0%, 43.5%);
animation: hamster-spin 0.8s linear infinite;
background: var(--gray11);
border-radius: 6px;
height: 13%;
left: -10%;
position: absolute;
top: -3.9%;
width: 30%;
}
.hamster-loading-bar:nth-child(1) {
animation-delay: -0.8s;
transform: rotate(0deg) translate(120%);
animation-delay: -0.8s;
transform: rotate(0deg) translate(120%);
}
.hamster-loading-bar:nth-child(2) {
animation-delay: -0.7s;
transform: rotate(45deg) translate(120%);
animation-delay: -0.7s;
transform: rotate(45deg) translate(120%);
}
.hamster-loading-bar:nth-child(3) {
animation-delay: -0.6s;
transform: rotate(90deg) translate(120%);
animation-delay: -0.6s;
transform: rotate(90deg) translate(120%);
}
.hamster-loading-bar:nth-child(4) {
animation-delay: -0.5s;
transform: rotate(135deg) translate(120%);
animation-delay: -0.5s;
transform: rotate(135deg) translate(120%);
}
.hamster-loading-bar:nth-child(5) {
animation-delay: -0.4s;
transform: rotate(180deg) translate(120%);
animation-delay: -0.4s;
transform: rotate(180deg) translate(120%);
}
.hamster-loading-bar:nth-child(6) {
animation-delay: -0.3s;
transform: rotate(225deg) translate(120%);
animation-delay: -0.3s;
transform: rotate(225deg) translate(120%);
}
.hamster-loading-bar:nth-child(7) {
animation-delay: -0.2s;
transform: rotate(270deg) translate(120%);
animation-delay: -0.2s;
transform: rotate(270deg) translate(120%);
}
.hamster-loading-bar:nth-child(8) {
animation-delay: -0.1s;
transform: rotate(315deg) translate(120%);
animation-delay: -0.1s;
transform: rotate(315deg) translate(120%);
}
@keyframes hamster-fade-in {
0% {
opacity: 0;
transform: scale(0.8);
}
100% {
opacity: 1;
transform: scale(1);
}
0% {
opacity: 0;
transform: scale(0.8);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes hamster-fade-out {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.8);
}
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.8);
}
}
@keyframes hamster-spin {
0% {
opacity: 1;
}
100% {
opacity: 0.15;
}
0% {
opacity: 1;
}
100% {
opacity: 0.15;
}
}
.scrollbar-hidden {
scrollbar-width: none; /* Firefox */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hidden::-webkit-scrollbar {
display: none; /* Chrome, Safari 和 Opera */
display: none; /* Chrome, Safari 和 Opera */
}
/* Thanks to next.js. */
[data-issues-count-animation] {
display: flex;
justify-content: center;
align-items: center;
}
[data-issues-count-animation] > div {
text-align: center;
}
[data-issues-count-exit].animate {
animation: fadeOut 300ms var(--timing) forwards;
}
[data-issues-count-enter].animate {
animation: fadeIn 300ms var(--timing) forwards;
}
[data-issues-count-plural] {
display: inline-block;
animation: fadeIn 300ms var(--timing) forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
filter: blur(2px);
transform: translateY(8px);
}
100% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
}
@keyframes fadeOut {
0% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-12px);
filter: blur(2px);
}
}
+16 -6
View File
@@ -1,11 +1,21 @@
export function formatBytes(bytes: number, decimals: number = 2) {
if (!+bytes) return "0 Bytes"
if (!+bytes) return "0 Bytes";
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = [
"Bytes",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
const i = Math.floor(Math.log(bytes) / Math.log(k))
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
}
File diff suppressed because one or more lines are too long
+207 -203
View File
@@ -1,208 +1,212 @@
export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = {
// 亚洲
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
AZ: { lat: 40.5, lng: 47.5, name: "Azerbaijan" }, // 阿塞拜疆
BD: { lat: 24.0, lng: 90.0, name: "Bangladesh" }, // 孟加拉国
BH: { lat: 26.0, lng: 50.55, name: "Bahrain" }, // 巴林
BT: { lat: 27.5, lng: 90.5, name: "Bhutan" }, // 不丹
BN: { lat: 4.5, lng: 114.6667, name: "Brunei" }, // 文莱
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
ID: { lat: -5.0, lng: 120.0, name: "Indonesia" }, // 印度尼西亚
IR: { lat: 32.0, lng: 53.0, name: "Iran" }, // 伊朗
IQ: { lat: 33.0, lng: 44.0, name: "Iraq" }, // 伊拉克
IL: { lat: 31.5, lng: 34.75, name: "Israel" }, // 以色列
JP: { lat: 36.0, lng: 138.0, name: "Japan" }, // 日本
JO: { lat: 31.0, lng: 36.0, name: "Jordan" }, // 约旦
KZ: { lat: 48.0, lng: 68.0, name: "Kazakhstan" }, // 哈萨克斯坦
KW: { lat: 29.3375, lng: 47.6581, name: "Kuwait" }, // 科威特
KG: { lat: 41.0, lng: 75.0, name: "Kyrgyzstan" }, // 吉尔吉斯斯坦
LA: { lat: 18.0, lng: 105.0, name: "Laos" }, // 老挝
LB: { lat: 33.8333, lng: 35.8333, name: "Lebanon" }, // 黎巴嫩
MY: { lat: 2.5, lng: 112.5, name: "Malaysia" }, // 马来西亚
MV: { lat: 3.25, lng: 73.0, name: "Maldives" }, // 马尔代夫
MN: { lat: 46.0, lng: 105.0, name: "Mongolia" }, // 蒙古
MM: { lat: 22.0, lng: 98.0, name: "Myanmar" }, // 缅甸
NP: { lat: 28.0, lng: 84.0, name: "Nepal" }, // 尼泊尔
OM: { lat: 21.0, lng: 57.0, name: "Oman" }, // 阿曼
PK: { lat: 30.0, lng: 70.0, name: "Pakistan" }, // 巴基斯坦
PH: { lat: 13.0, lng: 122.0, name: "Philippines" }, // 菲律宾
QA: { lat: 25.5, lng: 51.25, name: "Qatar" }, // 卡塔
SA: { lat: 25.0, lng: 45.0, name: "Saudi Arabia" }, // 沙特阿拉伯
SG: { lat: 1.3667, lng: 103.8, name: "Singapore" }, // 新加坡
KR: { lat: 37.0, lng: 127.5, name: "South Korea" }, // 韩国
LK: { lat: 7.0, lng: 81.0, name: "Sri Lanka" }, // 斯里兰
SY: { lat: 35.0, lng: 38.0, name: "Syria" }, // 叙利亚
TW: { lat: 23.5, lng: 121.0, name: "Taiwan" }, // 台湾
TJ: { lat: 39.0, lng: 71.0, name: "Tajikistan" }, // 塔吉克斯坦
TH: { lat: 15.0, lng: 100.0, name: "Thailand" }, // 泰国
TR: { lat: 39.0, lng: 35.0, name: "Turkey" }, // 土耳其
TM: { lat: 40.0, lng: 60.0, name: "Turkmenistan" }, // 土库曼斯坦
AE: { lat: 24.0, lng: 54.0, name: "United Arab Emirates" }, // 阿联酋
UZ: { lat: 41.0, lng: 64.0, name: "Uzbekistan" }, // 乌兹别克斯坦
VN: { lat: 16.0, lng: 106.0, name: "Vietnam" }, // 越南
YE: { lat: 15.0, lng: 48.0, name: "Yemen" }, // 也门
PS: { lat: 32.0, lng: 35.25, name: "Palestine" }, // 巴勒斯坦
export const countryCoordinates: Record<
string,
{ lat: number; lng: number; name: string }
> = {
// 亚洲
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
AZ: { lat: 40.5, lng: 47.5, name: "Azerbaijan" }, // 阿塞拜疆
BD: { lat: 24.0, lng: 90.0, name: "Bangladesh" }, // 孟加拉国
BH: { lat: 26.0, lng: 50.55, name: "Bahrain" }, // 巴林
BT: { lat: 27.5, lng: 90.5, name: "Bhutan" }, // 不丹
BN: { lat: 4.5, lng: 114.6667, name: "Brunei" }, // 文莱
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
MO: { lat: 22.1667, lng: 113.55, name: "Macau" }, // 澳门
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
ID: { lat: -5.0, lng: 120.0, name: "Indonesia" }, // 印度尼西亚
IR: { lat: 32.0, lng: 53.0, name: "Iran" }, // 伊朗
IQ: { lat: 33.0, lng: 44.0, name: "Iraq" }, // 伊拉克
IL: { lat: 31.5, lng: 34.75, name: "Israel" }, // 以色列
JP: { lat: 36.0, lng: 138.0, name: "Japan" }, // 日本
JO: { lat: 31.0, lng: 36.0, name: "Jordan" }, // 约旦
KZ: { lat: 48.0, lng: 68.0, name: "Kazakhstan" }, // 哈萨克斯坦
KW: { lat: 29.3375, lng: 47.6581, name: "Kuwait" }, // 科威特
KG: { lat: 41.0, lng: 75.0, name: "Kyrgyzstan" }, // 吉尔吉斯斯坦
LA: { lat: 18.0, lng: 105.0, name: "Laos" }, // 老挝
LB: { lat: 33.8333, lng: 35.8333, name: "Lebanon" }, // 黎巴嫩
MY: { lat: 2.5, lng: 112.5, name: "Malaysia" }, // 马来西亚
MV: { lat: 3.25, lng: 73.0, name: "Maldives" }, // 马尔代夫
MN: { lat: 46.0, lng: 105.0, name: "Mongolia" }, // 蒙古
MM: { lat: 22.0, lng: 98.0, name: "Myanmar" }, // 缅甸
NP: { lat: 28.0, lng: 84.0, name: "Nepal" }, // 尼泊
OM: { lat: 21.0, lng: 57.0, name: "Oman" }, // 阿曼
PK: { lat: 30.0, lng: 70.0, name: "Pakistan" }, // 巴基斯坦
PH: { lat: 13.0, lng: 122.0, name: "Philippines" }, // 菲律宾
QA: { lat: 25.5, lng: 51.25, name: "Qatar" }, // 卡塔尔
SA: { lat: 25.0, lng: 45.0, name: "Saudi Arabia" }, // 沙特阿拉伯
SG: { lat: 1.3667, lng: 103.8, name: "Singapore" }, // 新加坡
KR: { lat: 37.0, lng: 127.5, name: "South Korea" }, // 韩国
LK: { lat: 7.0, lng: 81.0, name: "Sri Lanka" }, // 斯里兰卡
SY: { lat: 35.0, lng: 38.0, name: "Syria" }, // 叙利亚
TW: { lat: 23.5, lng: 121.0, name: "Taiwan" }, // 台湾
TJ: { lat: 39.0, lng: 71.0, name: "Tajikistan" }, // 塔吉克斯坦
TH: { lat: 15.0, lng: 100.0, name: "Thailand" }, // 泰国
TR: { lat: 39.0, lng: 35.0, name: "Turkey" }, // 土耳其
TM: { lat: 40.0, lng: 60.0, name: "Turkmenistan" }, // 土库曼斯坦
AE: { lat: 24.0, lng: 54.0, name: "United Arab Emirates" }, // 阿联酋
UZ: { lat: 41.0, lng: 64.0, name: "Uzbekistan" }, // 乌兹别克斯坦
VN: { lat: 16.0, lng: 106.0, name: "Vietnam" }, // 越南
YE: { lat: 15.0, lng: 48.0, name: "Yemen" }, // 也门
PS: { lat: 32.0, lng: 35.25, name: "Palestine" }, // 巴勒斯坦
// 欧洲
AL: { lat: 41.0, lng: 20.0, name: "Albania" }, // 阿尔巴尼亚
AD: { lat: 42.5, lng: 1.6, name: "Andorra" }, // 安道尔
AT: { lat: 47.3333, lng: 13.3333, name: "Austria" }, // 奥地利
BY: { lat: 53.0, lng: 28.0, name: "Belarus" }, // 白俄罗斯
BE: { lat: 50.8333, lng: 4.0, name: "Belgium" }, // 比利时
BA: { lat: 44.0, lng: 18.0, name: "Bosnia and Herzegovina" }, // 波黑
BG: { lat: 43.0, lng: 25.0, name: "Bulgaria" }, // 保加利亚
HR: { lat: 45.1667, lng: 15.5, name: "Croatia" }, // 克罗地亚
CZ: { lat: 49.75, lng: 15.5, name: "Czech Republic" }, // 捷克
DK: { lat: 56.0, lng: 10.0, name: "Denmark" }, // 丹麦
EE: { lat: 59.0, lng: 26.0, name: "Estonia" }, // 爱沙尼亚
FI: { lat: 64.0, lng: 26.0, name: "Finland" }, // 芬兰
FR: { lat: 46.0, lng: 2.0, name: "France" }, // 法国
DE: { lat: 51.0, lng: 9.0, name: "Germany" }, // 德国
GR: { lat: 39.0, lng: 22.0, name: "Greece" }, // 希腊
HU: { lat: 47.0, lng: 20.0, name: "Hungary" }, // 匈牙利
IS: { lat: 65.0, lng: -18.0, name: "Iceland" }, // 冰岛
IE: { lat: 53.0, lng: -8.0, name: "Ireland" }, // 爱尔兰
IT: { lat: 42.8333, lng: 12.8333, name: "Italy" }, // 意大利
LV: { lat: 57.0, lng: 25.0, name: "Latvia" }, // 拉脱维亚
LI: { lat: 47.1667, lng: 9.5333, name: "Liechtenstein" }, // 列支敦士登
LT: { lat: 56.0, lng: 24.0, name: "Lithuania" }, // 立陶宛
LU: { lat: 49.75, lng: 6.1667, name: "Luxembourg" }, // 卢森堡
MT: { lat: 35.8333, lng: 14.5833, name: "Malta" }, // 马耳他
MD: { lat: 47.0, lng: 29.0, name: "Moldova" }, // 摩尔多瓦
MC: { lat: 43.7333, lng: 7.4, name: "Monaco" }, // 摩纳哥
ME: { lat: 42.0, lng: 19.0, name: "Montenegro" }, // 黑山
NL: { lat: 52.5, lng: 5.75, name: "Netherlands" }, // 荷兰
NO: { lat: 62.0, lng: 10.0, name: "Norway" }, // 挪威
PL: { lat: 52.0, lng: 20.0, name: "Poland" }, // 波兰
PT: { lat: 39.5, lng: -8.0, name: "Portugal" }, // 葡萄牙
RO: { lat: 46.0, lng: 25.0, name: "Romania" }, // 罗马尼亚
RU: { lat: 60.0, lng: 100.0, name: "Russia" }, // 俄罗斯
SM: { lat: 43.7667, lng: 12.4167, name: "San Marino" }, // 圣马力诺
RS: { lat: 44.0, lng: 21.0, name: "Serbia" }, // 塞尔维亚
SK: { lat: 48.6667, lng: 19.5, name: "Slovakia" }, // 斯洛伐克
SI: { lat: 46.0, lng: 15.0, name: "Slovenia" }, // 斯洛文尼亚
ES: { lat: 40.0, lng: -4.0, name: "Spain" }, // 西班牙
SE: { lat: 62.0, lng: 15.0, name: "Sweden" }, // 瑞典
CH: { lat: 47.0, lng: 8.0, name: "Switzerland" }, // 瑞士
UA: { lat: 49.0, lng: 32.0, name: "Ukraine" }, // 乌克兰
GB: { lat: 54.0, lng: -2.0, name: "United Kingdom" }, // 英国
VA: { lat: 41.9, lng: 12.45, name: "Vatican City" }, // 梵蒂冈
// 欧洲
AL: { lat: 41.0, lng: 20.0, name: "Albania" }, // 阿尔巴尼亚
AD: { lat: 42.5, lng: 1.6, name: "Andorra" }, // 安道尔
AT: { lat: 47.3333, lng: 13.3333, name: "Austria" }, // 奥地利
BY: { lat: 53.0, lng: 28.0, name: "Belarus" }, // 白俄罗斯
BE: { lat: 50.8333, lng: 4.0, name: "Belgium" }, // 比利时
BA: { lat: 44.0, lng: 18.0, name: "Bosnia and Herzegovina" }, // 波黑
BG: { lat: 43.0, lng: 25.0, name: "Bulgaria" }, // 保加利亚
HR: { lat: 45.1667, lng: 15.5, name: "Croatia" }, // 克罗地亚
CZ: { lat: 49.75, lng: 15.5, name: "Czech Republic" }, // 捷克
DK: { lat: 56.0, lng: 10.0, name: "Denmark" }, // 丹麦
EE: { lat: 59.0, lng: 26.0, name: "Estonia" }, // 爱沙尼亚
FI: { lat: 64.0, lng: 26.0, name: "Finland" }, // 芬兰
FR: { lat: 46.0, lng: 2.0, name: "France" }, // 法国
DE: { lat: 51.0, lng: 9.0, name: "Germany" }, // 德国
GR: { lat: 39.0, lng: 22.0, name: "Greece" }, // 希腊
HU: { lat: 47.0, lng: 20.0, name: "Hungary" }, // 匈牙利
IS: { lat: 65.0, lng: -18.0, name: "Iceland" }, // 冰岛
IE: { lat: 53.0, lng: -8.0, name: "Ireland" }, // 爱尔兰
IT: { lat: 42.8333, lng: 12.8333, name: "Italy" }, // 意大利
LV: { lat: 57.0, lng: 25.0, name: "Latvia" }, // 拉脱维亚
LI: { lat: 47.1667, lng: 9.5333, name: "Liechtenstein" }, // 列支敦士登
LT: { lat: 56.0, lng: 24.0, name: "Lithuania" }, // 立陶宛
LU: { lat: 49.75, lng: 6.1667, name: "Luxembourg" }, // 卢森堡
MT: { lat: 35.8333, lng: 14.5833, name: "Malta" }, // 马耳他
MD: { lat: 47.0, lng: 29.0, name: "Moldova" }, // 摩尔多瓦
MC: { lat: 43.7333, lng: 7.4, name: "Monaco" }, // 摩纳哥
ME: { lat: 42.0, lng: 19.0, name: "Montenegro" }, // 黑山
NL: { lat: 52.5, lng: 5.75, name: "Netherlands" }, // 荷兰
NO: { lat: 62.0, lng: 10.0, name: "Norway" }, // 挪威
PL: { lat: 52.0, lng: 20.0, name: "Poland" }, // 波兰
PT: { lat: 39.5, lng: -8.0, name: "Portugal" }, // 葡萄牙
RO: { lat: 46.0, lng: 25.0, name: "Romania" }, // 罗马尼亚
RU: { lat: 60.0, lng: 100.0, name: "Russia" }, // 俄罗斯
SM: { lat: 43.7667, lng: 12.4167, name: "San Marino" }, // 圣马力诺
RS: { lat: 44.0, lng: 21.0, name: "Serbia" }, // 塞尔维亚
SK: { lat: 48.6667, lng: 19.5, name: "Slovakia" }, // 斯洛伐克
SI: { lat: 46.0, lng: 15.0, name: "Slovenia" }, // 斯洛文尼亚
ES: { lat: 40.0, lng: -4.0, name: "Spain" }, // 西班牙
SE: { lat: 62.0, lng: 15.0, name: "Sweden" }, // 瑞典
CH: { lat: 47.0, lng: 8.0, name: "Switzerland" }, // 瑞士
UA: { lat: 49.0, lng: 32.0, name: "Ukraine" }, // 乌克兰
GB: { lat: 54.0, lng: -2.0, name: "United Kingdom" }, // 英国
VA: { lat: 41.9, lng: 12.45, name: "Vatican City" }, // 梵蒂冈
// 北美洲
AG: { lat: 17.05, lng: -61.8, name: "Antigua and Barbuda" }, // 安提瓜和巴布达
BS: { lat: 24.25, lng: -76.0, name: "Bahamas" }, // 巴哈马
BB: { lat: 13.1667, lng: -59.5333, name: "Barbados" }, // 巴巴多斯
BZ: { lat: 17.25, lng: -88.75, name: "Belize" }, // 伯利兹
CA: { lat: 60.0, lng: -95.0, name: "Canada" }, // 加拿大
CR: { lat: 10.0, lng: -84.0, name: "Costa Rica" }, // 哥斯达黎加
CU: { lat: 21.5, lng: -80.0, name: "Cuba" }, // 古巴
DM: { lat: 15.4167, lng: -61.3333, name: "Dominica" }, // 多米尼克
DO: { lat: 19.0, lng: -70.6667, name: "Dominican Republic" }, // 多米尼加共和国
SV: { lat: 13.8333, lng: -88.9167, name: "El Salvador" }, // 萨尔瓦多
GD: { lat: 12.1167, lng: -61.6667, name: "Grenada" }, // 格林纳达
GT: { lat: 15.5, lng: -90.25, name: "Guatemala" }, // 危地马拉
HT: { lat: 19.0, lng: -72.4167, name: "Haiti" }, // 海地
HN: { lat: 15.0, lng: -86.5, name: "Honduras" }, // 洪都拉斯
JM: { lat: 18.25, lng: -77.5, name: "Jamaica" }, // 牙买加
MX: { lat: 23.0, lng: -102.0, name: "Mexico" }, // 墨西哥
NI: { lat: 13.0, lng: -85.0, name: "Nicaragua" }, // 尼加拉瓜
PA: { lat: 9.0, lng: -80.0, name: "Panama" }, // 巴拿马
KN: { lat: 17.3333, lng: -62.75, name: "Saint Kitts and Nevis" }, // 圣基茨和尼维斯
LC: { lat: 13.8833, lng: -61.1333, name: "Saint Lucia" }, // 圣卢西亚
VC: { lat: 13.25, lng: -61.2, name: "Saint Vincent and the Grenadines" }, // 圣文森特和格林纳丁斯
TT: { lat: 11.0, lng: -61.0, name: "Trinidad and Tobago" }, // 特立尼达和多巴哥
US: { lat: 38.0, lng: -97.0, name: "United States" }, // 美国
// 北美洲
AG: { lat: 17.05, lng: -61.8, name: "Antigua and Barbuda" }, // 安提瓜和巴布达
BS: { lat: 24.25, lng: -76.0, name: "Bahamas" }, // 巴哈马
BB: { lat: 13.1667, lng: -59.5333, name: "Barbados" }, // 巴巴多斯
BZ: { lat: 17.25, lng: -88.75, name: "Belize" }, // 伯利兹
CA: { lat: 60.0, lng: -95.0, name: "Canada" }, // 加拿大
CR: { lat: 10.0, lng: -84.0, name: "Costa Rica" }, // 哥斯达黎加
CU: { lat: 21.5, lng: -80.0, name: "Cuba" }, // 古巴
DM: { lat: 15.4167, lng: -61.3333, name: "Dominica" }, // 多米尼克
DO: { lat: 19.0, lng: -70.6667, name: "Dominican Republic" }, // 多米尼加共和国
SV: { lat: 13.8333, lng: -88.9167, name: "El Salvador" }, // 萨尔瓦多
GD: { lat: 12.1167, lng: -61.6667, name: "Grenada" }, // 格林纳达
GT: { lat: 15.5, lng: -90.25, name: "Guatemala" }, // 危地马拉
HT: { lat: 19.0, lng: -72.4167, name: "Haiti" }, // 海地
HN: { lat: 15.0, lng: -86.5, name: "Honduras" }, // 洪都拉斯
JM: { lat: 18.25, lng: -77.5, name: "Jamaica" }, // 牙买加
MX: { lat: 23.0, lng: -102.0, name: "Mexico" }, // 墨西哥
NI: { lat: 13.0, lng: -85.0, name: "Nicaragua" }, // 尼加拉瓜
PA: { lat: 9.0, lng: -80.0, name: "Panama" }, // 巴拿马
KN: { lat: 17.3333, lng: -62.75, name: "Saint Kitts and Nevis" }, // 圣基茨和尼维斯
LC: { lat: 13.8833, lng: -61.1333, name: "Saint Lucia" }, // 圣卢西亚
VC: { lat: 13.25, lng: -61.2, name: "Saint Vincent and the Grenadines" }, // 圣文森特和格林纳丁斯
TT: { lat: 11.0, lng: -61.0, name: "Trinidad and Tobago" }, // 特立尼达和多巴哥
US: { lat: 38.0, lng: -97.0, name: "United States" }, // 美国
// 南美洲
AR: { lat: -34.0, lng: -64.0, name: "Argentina" }, // 阿根廷
BO: { lat: -17.0, lng: -65.0, name: "Bolivia" }, // 玻利维亚
BR: { lat: -10.0, lng: -55.0, name: "Brazil" }, // 巴西
CL: { lat: -30.0, lng: -71.0, name: "Chile" }, // 智利
CO: { lat: 4.0, lng: -72.0, name: "Colombia" }, // 哥伦比亚
EC: { lat: -2.0, lng: -77.5, name: "Ecuador" }, // 厄瓜多尔
GY: { lat: 5.0, lng: -59.0, name: "Guyana" }, // 圭亚那
PY: { lat: -23.0, lng: -58.0, name: "Paraguay" }, // 巴拉圭
PE: { lat: -10.0, lng: -76.0, name: "Peru" }, // 秘鲁
SR: { lat: 4.0, lng: -56.0, name: "Suriname" }, // 苏里南
UY: { lat: -33.0, lng: -56.0, name: "Uruguay" }, // 乌拉圭
VE: { lat: 8.0, lng: -66.0, name: "Venezuela" }, // 委内瑞拉
// 南美洲
AR: { lat: -34.0, lng: -64.0, name: "Argentina" }, // 阿根廷
BO: { lat: -17.0, lng: -65.0, name: "Bolivia" }, // 玻利维亚
BR: { lat: -10.0, lng: -55.0, name: "Brazil" }, // 巴西
CL: { lat: -30.0, lng: -71.0, name: "Chile" }, // 智利
CO: { lat: 4.0, lng: -72.0, name: "Colombia" }, // 哥伦比亚
EC: { lat: -2.0, lng: -77.5, name: "Ecuador" }, // 厄瓜多尔
GY: { lat: 5.0, lng: -59.0, name: "Guyana" }, // 圭亚那
PY: { lat: -23.0, lng: -58.0, name: "Paraguay" }, // 巴拉圭
PE: { lat: -10.0, lng: -76.0, name: "Peru" }, // 秘鲁
SR: { lat: 4.0, lng: -56.0, name: "Suriname" }, // 苏里南
UY: { lat: -33.0, lng: -56.0, name: "Uruguay" }, // 乌拉圭
VE: { lat: 8.0, lng: -66.0, name: "Venezuela" }, // 委内瑞拉
// 大洋洲
AU: { lat: -27.0, lng: 133.0, name: "Australia" }, // 澳大利亚
FJ: { lat: -18.0, lng: 175.0, name: "Fiji" }, // 斐济
KI: { lat: 1.4167, lng: 173.0, name: "Kiribati" }, // 基里巴斯
MH: { lat: 9.0, lng: 168.0, name: "Marshall Islands" }, // 马绍尔群岛
FM: { lat: 6.9167, lng: 158.25, name: "Micronesia" }, // 密克罗尼西亚
NR: { lat: -0.5333, lng: 166.9167, name: "Nauru" }, // 瑙鲁
NZ: { lat: -41.0, lng: 174.0, name: "New Zealand" }, // 新西兰
PW: { lat: 7.5, lng: 134.5, name: "Palau" }, // 帕劳
PG: { lat: -6.0, lng: 147.0, name: "Papua New Guinea" }, // 巴布亚新几内亚
WS: { lat: -13.5833, lng: -172.3333, name: "Samoa" }, // 萨摩亚
SB: { lat: -8.0, lng: 159.0, name: "Solomon Islands" }, // 所罗门群岛
TO: { lat: -20.0, lng: -175.0, name: "Tonga" }, // 汤加
TV: { lat: -8.0, lng: 178.0, name: "Tuvalu" }, // 图瓦卢
VU: { lat: -16.0, lng: 167.0, name: "Vanuatu" }, // 瓦努阿图
// 大洋洲
AU: { lat: -27.0, lng: 133.0, name: "Australia" }, // 澳大利亚
FJ: { lat: -18.0, lng: 175.0, name: "Fiji" }, // 斐济
KI: { lat: 1.4167, lng: 173.0, name: "Kiribati" }, // 基里巴斯
MH: { lat: 9.0, lng: 168.0, name: "Marshall Islands" }, // 马绍尔群岛
FM: { lat: 6.9167, lng: 158.25, name: "Micronesia" }, // 密克罗尼西亚
NR: { lat: -0.5333, lng: 166.9167, name: "Nauru" }, // 瑙鲁
NZ: { lat: -41.0, lng: 174.0, name: "New Zealand" }, // 新西兰
PW: { lat: 7.5, lng: 134.5, name: "Palau" }, // 帕劳
PG: { lat: -6.0, lng: 147.0, name: "Papua New Guinea" }, // 巴布亚新几内亚
WS: { lat: -13.5833, lng: -172.3333, name: "Samoa" }, // 萨摩亚
SB: { lat: -8.0, lng: 159.0, name: "Solomon Islands" }, // 所罗门群岛
TO: { lat: -20.0, lng: -175.0, name: "Tonga" }, // 汤加
TV: { lat: -8.0, lng: 178.0, name: "Tuvalu" }, // 图瓦卢
VU: { lat: -16.0, lng: 167.0, name: "Vanuatu" }, // 瓦努阿图
// 非洲
DZ: { lat: 28.0, lng: 3.0, name: "Algeria" }, // 阿尔及利亚
AO: { lat: -12.5, lng: 18.5, name: "Angola" }, // 安哥拉
BJ: { lat: 9.5, lng: 2.25, name: "Benin" }, // 贝宁
BW: { lat: -22.0, lng: 24.0, name: "Botswana" }, // 博茨瓦纳
BF: { lat: 13.0, lng: -2.0, name: "Burkina Faso" }, // 布基纳法索
BI: { lat: -3.5, lng: 30.0, name: "Burundi" }, // 布隆迪
CM: { lat: 6.0, lng: 12.0, name: "Cameroon" }, // 喀麦隆
CV: { lat: 16.0, lng: -24.0, name: "Cape Verde" }, // 佛得角
CF: { lat: 7.0, lng: 21.0, name: "Central African Republic" }, // 中非共和国
TD: { lat: 15.0, lng: 19.0, name: "Chad" }, // 乍得
KM: { lat: -12.1667, lng: 44.25, name: "Comoros" }, // 科摩罗
CG: { lat: -1.0, lng: 15.0, name: "Congo" }, // 刚果
CD: { lat: 0.0, lng: 25.0, name: "Democratic Republic of the Congo" }, // 刚果民主共和国
CI: { lat: 8.0, lng: -5.0, name: "Côte d'Ivoire" }, // 科特迪瓦
DJ: { lat: 11.5, lng: 43.0, name: "Djibouti" }, // 吉布提
EG: { lat: 27.0, lng: 30.0, name: "Egypt" }, // 埃及
GQ: { lat: 2.0, lng: 10.0, name: "Equatorial Guinea" }, // 赤道几内亚
ER: { lat: 15.0, lng: 39.0, name: "Eritrea" }, // 厄立特里亚
ET: { lat: 8.0, lng: 38.0, name: "Ethiopia" }, // 埃塞俄比亚
GA: { lat: -1.0, lng: 11.75, name: "Gabon" }, // 加蓬
GM: { lat: 13.4667, lng: -16.5667, name: "Gambia" }, // 冈比亚
GH: { lat: 8.0, lng: -2.0, name: "Ghana" }, // 加纳
GN: { lat: 11.0, lng: -10.0, name: "Guinea" }, // 几内亚
GW: { lat: 12.0, lng: -15.0, name: "Guinea-Bissau" }, // 几内亚比绍
KE: { lat: 1.0, lng: 38.0, name: "Kenya" }, // 肯尼亚
LS: { lat: -29.5, lng: 28.5, name: "Lesotho" }, // 莱索托
LR: { lat: 6.5, lng: -9.5, name: "Liberia" }, // 利比里亚
LY: { lat: 25.0, lng: 17.0, name: "Libya" }, // 利比亚
MG: { lat: -20.0, lng: 47.0, name: "Madagascar" }, // 马达加斯加
MW: { lat: -13.5, lng: 34.0, name: "Malawi" }, // 马拉维
ML: { lat: 17.0, lng: -4.0, name: "Mali" }, // 马里
MR: { lat: 20.0, lng: -12.0, name: "Mauritania" }, // 毛里塔尼亚
MU: { lat: -20.2833, lng: 57.55, name: "Mauritius" }, // 毛里求斯
YT: { lat: -12.8333, lng: 45.1667, name: "Mayotte" }, // 马约特
MA: { lat: 32.0, lng: -5.0, name: "Morocco" }, // 摩洛哥
MZ: { lat: -18.25, lng: 35.0, name: "Mozambique" }, // 莫桑比克
NA: { lat: -22.0, lng: 17.0, name: "Namibia" }, // 纳米比亚
NE: { lat: 16.0, lng: 8.0, name: "Niger" }, // 尼日尔
NG: { lat: 10.0, lng: 8.0, name: "Nigeria" }, // 尼日利亚
RW: { lat: -2.0, lng: 30.0, name: "Rwanda" }, // 卢旺达
ST: { lat: 1.0, lng: 7.0, name: "São Tomé and Principe" }, // 圣多美和普林西比
SN: { lat: 14.0, lng: -14.0, name: "Senegal" }, // 塞内加尔
SC: { lat: -4.5833, lng: 55.6667, name: "Seychelles" }, // 塞舌尔
SL: { lat: 8.5, lng: -11.5, name: "Sierra Leone" }, // 塞拉利昂
SO: { lat: 10.0, lng: 49.0, name: "Somalia" }, // 索马里
ZA: { lat: -29.0, lng: 24.0, name: "South Africa" }, // 南非
SD: { lat: 15.0, lng: 30.0, name: "Sudan" }, // 苏丹
SZ: { lat: -26.5, lng: 31.5, name: "Swaziland" }, // 斯威士兰
TZ: { lat: -6.0, lng: 35.0, name: "Tanzania" }, // 坦桑尼亚
TG: { lat: 8.0, lng: 1.1667, name: "Togo" }, // 多哥
TN: { lat: 34.0, lng: 9.0, name: "Tunisia" }, // 突尼斯
UG: { lat: 1.0, lng: 32.0, name: "Uganda" }, // 乌干达
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
}
// 非洲
DZ: { lat: 28.0, lng: 3.0, name: "Algeria" }, // 阿尔及利亚
AO: { lat: -12.5, lng: 18.5, name: "Angola" }, // 安哥拉
BJ: { lat: 9.5, lng: 2.25, name: "Benin" }, // 贝宁
BW: { lat: -22.0, lng: 24.0, name: "Botswana" }, // 博茨瓦纳
BF: { lat: 13.0, lng: -2.0, name: "Burkina Faso" }, // 布基纳法索
BI: { lat: -3.5, lng: 30.0, name: "Burundi" }, // 布隆迪
CM: { lat: 6.0, lng: 12.0, name: "Cameroon" }, // 喀麦隆
CV: { lat: 16.0, lng: -24.0, name: "Cape Verde" }, // 佛得角
CF: { lat: 7.0, lng: 21.0, name: "Central African Republic" }, // 中非共和国
TD: { lat: 15.0, lng: 19.0, name: "Chad" }, // 乍得
KM: { lat: -12.1667, lng: 44.25, name: "Comoros" }, // 科摩罗
CG: { lat: -1.0, lng: 15.0, name: "Congo" }, // 刚果
CD: { lat: 0.0, lng: 25.0, name: "Democratic Republic of the Congo" }, // 刚果民主共和国
CI: { lat: 8.0, lng: -5.0, name: "Côte d'Ivoire" }, // 科特迪瓦
DJ: { lat: 11.5, lng: 43.0, name: "Djibouti" }, // 吉布提
EG: { lat: 27.0, lng: 30.0, name: "Egypt" }, // 埃及
GQ: { lat: 2.0, lng: 10.0, name: "Equatorial Guinea" }, // 赤道几内亚
ER: { lat: 15.0, lng: 39.0, name: "Eritrea" }, // 厄立特里亚
ET: { lat: 8.0, lng: 38.0, name: "Ethiopia" }, // 埃塞俄比亚
GA: { lat: -1.0, lng: 11.75, name: "Gabon" }, // 加蓬
GM: { lat: 13.4667, lng: -16.5667, name: "Gambia" }, // 冈比亚
GH: { lat: 8.0, lng: -2.0, name: "Ghana" }, // 加纳
GN: { lat: 11.0, lng: -10.0, name: "Guinea" }, // 几内亚
GW: { lat: 12.0, lng: -15.0, name: "Guinea-Bissau" }, // 几内亚比绍
KE: { lat: 1.0, lng: 38.0, name: "Kenya" }, // 肯尼亚
LS: { lat: -29.5, lng: 28.5, name: "Lesotho" }, // 莱索托
LR: { lat: 6.5, lng: -9.5, name: "Liberia" }, // 利比里亚
LY: { lat: 25.0, lng: 17.0, name: "Libya" }, // 利比亚
MG: { lat: -20.0, lng: 47.0, name: "Madagascar" }, // 马达加斯加
MW: { lat: -13.5, lng: 34.0, name: "Malawi" }, // 马拉维
ML: { lat: 17.0, lng: -4.0, name: "Mali" }, // 马里
MR: { lat: 20.0, lng: -12.0, name: "Mauritania" }, // 毛里塔尼亚
MU: { lat: -20.2833, lng: 57.55, name: "Mauritius" }, // 毛里求斯
YT: { lat: -12.8333, lng: 45.1667, name: "Mayotte" }, // 马约特
MA: { lat: 32.0, lng: -5.0, name: "Morocco" }, // 摩洛哥
MZ: { lat: -18.25, lng: 35.0, name: "Mozambique" }, // 莫桑比克
NA: { lat: -22.0, lng: 17.0, name: "Namibia" }, // 纳米比亚
NE: { lat: 16.0, lng: 8.0, name: "Niger" }, // 尼日尔
NG: { lat: 10.0, lng: 8.0, name: "Nigeria" }, // 尼日利亚
RW: { lat: -2.0, lng: 30.0, name: "Rwanda" }, // 卢旺达
ST: { lat: 1.0, lng: 7.0, name: "São Tomé and Principe" }, // 圣多美和普林西比
SN: { lat: 14.0, lng: -14.0, name: "Senegal" }, // 塞内加尔
SC: { lat: -4.5833, lng: 55.6667, name: "Seychelles" }, // 塞舌尔
SL: { lat: 8.5, lng: -11.5, name: "Sierra Leone" }, // 塞拉利昂
SO: { lat: 10.0, lng: 49.0, name: "Somalia" }, // 索马里
ZA: { lat: -29.0, lng: 24.0, name: "South Africa" }, // 南非
SD: { lat: 15.0, lng: 30.0, name: "Sudan" }, // 苏丹
SZ: { lat: -26.5, lng: 31.5, name: "Swaziland" }, // 斯威士兰
TZ: { lat: -6.0, lng: 35.0, name: "Tanzania" }, // 坦桑尼亚
TG: { lat: 8.0, lng: 1.1667, name: "Togo" }, // 多哥
TN: { lat: 34.0, lng: 9.0, name: "Tunisia" }, // 突尼斯
UG: { lat: 1.0, lng: 32.0, name: "Uganda" }, // 乌干达
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
};
+97 -89
View File
@@ -1,99 +1,107 @@
export const InjectContext = (content: string) => {
const tempDiv = document.createElement("div")
tempDiv.innerHTML = content
const tempDiv = document.createElement("div");
tempDiv.innerHTML = content;
const INJECTION_MARK = "data-injected" // 自定义属性标识
const INJECTION_MARK = "data-injected"; // 自定义属性标识
// 清理已有的注入资源
const cleanInjectedResources = () => {
document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => node.remove())
}
// 清理已有的注入资源
const cleanInjectedResources = () => {
document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => {
node.remove();
});
};
const loadExternalScript = (scriptElement: HTMLScriptElement): Promise<void> => {
return new Promise((resolve, reject) => {
const script = document.createElement("script")
script.src = scriptElement.src
script.async = false // 保持顺序执行
script.setAttribute(INJECTION_MARK, "true") // 添加标识
script.onload = () => resolve()
script.onerror = () => reject(new Error(`Failed to load script: ${scriptElement.src}`))
document.head.appendChild(script)
})
}
const loadExternalScript = (
scriptElement: HTMLScriptElement,
): Promise<void> => {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = scriptElement.src;
script.async = false; // 保持顺序执行
script.setAttribute(INJECTION_MARK, "true"); // 添加标识
script.onload = () => resolve();
script.onerror = () =>
reject(new Error(`Failed to load script: ${scriptElement.src}`));
document.head.appendChild(script);
});
};
const executeInlineScript = (scriptContent: string): Promise<void> => {
return new Promise((resolve) => {
const script = document.createElement("script")
script.textContent = scriptContent
script.setAttribute(INJECTION_MARK, "true") // 添加标识
document.body.appendChild(script)
resolve()
})
}
const executeInlineScript = (scriptContent: string): Promise<void> => {
return new Promise((resolve) => {
const script = document.createElement("script");
script.textContent = scriptContent;
script.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(script);
resolve();
});
};
const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => {
return new Promise((resolve, reject) => {
if ((styleElement as any).href) {
// 处理 <link>
const link = document.createElement("link")
link.rel = "stylesheet"
link.href = (styleElement as any).href
link.setAttribute(INJECTION_MARK, "true") // 添加标识
link.onload = () => resolve()
link.onerror = () => reject(new Error(`Failed to load stylesheet: ${link.href}`))
document.head.appendChild(link)
} else {
const style = document.createElement("style")
style.textContent = styleElement.textContent
style.setAttribute(INJECTION_MARK, "true") // 添加标识
document.head.appendChild(style)
resolve()
}
})
}
const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => {
return new Promise((resolve, reject) => {
if ((styleElement as any).href) {
// 处理 <link>
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = (styleElement as any).href;
link.setAttribute(INJECTION_MARK, "true"); // 添加标识
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Failed to load stylesheet: ${link.href}`));
document.head.appendChild(link);
} else {
const style = document.createElement("style");
style.textContent = styleElement.textContent;
style.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(style);
resolve();
}
});
};
const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = {
SCRIPT: (element) => {
const scriptElement = element as HTMLScriptElement
if (scriptElement.src) {
// 加载外部脚本
return loadExternalScript(scriptElement)
} else {
// 执行内联脚本
return executeInlineScript(scriptElement.textContent || "")
}
},
STYLE: (element) => loadStyle(element as HTMLStyleElement),
META: (element) => {
const meta = element.cloneNode(true) as HTMLElement
meta.setAttribute(INJECTION_MARK, "true") // 添加标识
document.head.appendChild(meta) // 将 meta 标签插入到 <head>
return Promise.resolve()
},
DEFAULT: (element) => {
element.setAttribute(INJECTION_MARK, "true") // 添加标识
document.body.appendChild(element)
return Promise.resolve()
},
}
const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = {
SCRIPT: (element) => {
const scriptElement = element as HTMLScriptElement;
if (scriptElement.src) {
// 加载外部脚本
return loadExternalScript(scriptElement);
} else {
// 执行内联脚本
return executeInlineScript(scriptElement.textContent || "");
}
},
STYLE: (element) => loadStyle(element as HTMLStyleElement),
META: (element) => {
const meta = element.cloneNode(true) as HTMLElement;
meta.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(meta); // 将 meta 标签插入到 <head>
return Promise.resolve();
},
DEFAULT: (element) => {
element.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(element);
return Promise.resolve();
},
};
// 开始注入前清理已有资源
cleanInjectedResources()
// 开始注入前清理已有资源
cleanInjectedResources();
const executeSequentially = async () => {
for (const node of Array.from(tempDiv.childNodes)) {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement
const handler = handlers[element.tagName] || handlers.DEFAULT
await handler(element) // 按顺序等待当前脚本或资源完成处理
} else if (node.nodeType === Node.TEXT_NODE) {
document.body.appendChild(document.createTextNode(node.textContent || ""))
}
}
console.log("All resources have been injected and executed in sequence.")
}
const executeSequentially = async () => {
for (const node of Array.from(tempDiv.childNodes)) {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement;
const handler = handlers[element.tagName] || handlers.DEFAULT;
await handler(element); // 按顺序等待当前脚本或资源完成处理
} else if (node.nodeType === Node.TEXT_NODE) {
document.body.appendChild(
document.createTextNode(node.textContent || ""),
);
}
}
console.log("All resources have been injected and executed in sequence.");
};
return executeSequentially().catch((error) => {
console.error("Error during resource injection:", error)
})
}
return executeSequentially().catch((error) => {
console.error("Error during resource injection:", error);
});
};
+139 -133
View File
@@ -1,142 +1,148 @@
import type { SVGProps } from "react"
import type { SVGProps } from "react";
export function GetFontLogoClass(platform: string): string {
if (
[
"almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"elementary",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"kali-linux",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"raspberry-pi",
"redhat",
"rocky-linux",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin",
].indexOf(platform) > -1
) {
return platform
}
if (platform == "darwin") {
return "apple"
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "tux"
}
if (platform == "amazon") {
return "redhat"
}
if (platform == "arch") {
return "archlinux"
}
if (platform.toLowerCase().includes("opensuse")) {
return "opensuse"
}
return "tux"
if (
[
"almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"elementary",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"kali-linux",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"raspberry-pi",
"redhat",
"rocky-linux",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin",
].indexOf(platform) > -1
) {
return platform;
}
if (platform === "darwin") {
return "apple";
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "tux";
}
if (platform === "amazon") {
return "redhat";
}
if (platform === "arch") {
return "archlinux";
}
if (platform.toLowerCase().includes("opensuse")) {
return "opensuse";
}
return "tux";
}
export function GetOsName(platform: string): string {
if (
[
"almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"redhat",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin",
].indexOf(platform) > -1
) {
return platform.charAt(0).toUpperCase() + platform.slice(1)
}
if (platform == "darwin") {
return "macOS"
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "Linux"
}
if (platform == "amazon") {
return "Redhat"
}
if (platform == "arch") {
return "Archlinux"
}
if (platform.toLowerCase().includes("opensuse")) {
return "Opensuse"
}
return "Linux"
if (
[
"almalinux",
"alpine",
"aosc",
"apple",
"archlinux",
"archlabs",
"artix",
"budgie",
"centos",
"coreos",
"debian",
"deepin",
"devuan",
"docker",
"fedora",
"ferris",
"flathub",
"freebsd",
"gentoo",
"gnu-guix",
"illumos",
"linuxmint",
"mageia",
"mandriva",
"manjaro",
"nixos",
"openbsd",
"opensuse",
"pop-os",
"redhat",
"sabayon",
"slackware",
"snappy",
"solus",
"tux",
"ubuntu",
"void",
"zorin",
].indexOf(platform) > -1
) {
return platform.charAt(0).toUpperCase() + platform.slice(1);
}
if (platform === "darwin") {
return "macOS";
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "Linux";
}
if (platform === "amazon") {
return "Redhat";
}
if (platform === "arch") {
return "Archlinux";
}
if (platform.toLowerCase().includes("opensuse")) {
return "Opensuse";
}
return "Linux";
}
export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z"
></path>
</svg>
)
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z"
></path>
</svg>
);
}
+78 -43
View File
@@ -1,55 +1,90 @@
import { LoginUserResponse, MonitorResponse, ServerGroupResponse, ServiceResponse, SettingResponse } from "@/types/nezha-api"
import type {
LoginUserResponse,
MetricPeriod,
MetricType,
MonitorResponse,
ServerGroupResponse,
ServerMetricsResponse,
ServiceResponse,
SettingResponse,
} from "@/types/nezha-api";
let lastestRefreshTokenAt = 0
let lastestRefreshTokenAt = 0;
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
const response = await fetch("/api/v1/server-group")
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
return data
}
const response = await fetch("/api/v1/server-group");
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};
export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
const response = await fetch("/api/v1/profile")
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
const response = await fetch("/api/v1/profile");
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
// auto refresh token
if (document.cookie && (!lastestRefreshTokenAt || Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)) {
lastestRefreshTokenAt = Date.now()
fetch("/api/v1/refresh-token")
}
// auto refresh token
if (
document.cookie &&
(!lastestRefreshTokenAt ||
Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)
) {
lastestRefreshTokenAt = Date.now();
fetch("/api/v1/refresh-token");
}
return data
}
return data;
};
export const fetchMonitor = async (server_id: number): Promise<MonitorResponse> => {
const response = await fetch(`/api/v1/service/${server_id}`)
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
return data
}
export type MonitorPeriod = "1d" | "7d" | "30d";
export const fetchMonitor = async (
server_id: number,
period?: MonitorPeriod,
): Promise<MonitorResponse> => {
const query = period ? `?period=${period}` : "";
const response = await fetch(`/api/v1/server/${server_id}/service${query}`);
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};
export const fetchService = async (): Promise<ServiceResponse> => {
const response = await fetch("/api/v1/service")
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
return data
}
const response = await fetch("/api/v1/service");
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};
export const fetchSetting = async (): Promise<SettingResponse> => {
const response = await fetch("/api/v1/setting")
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
return data
}
const response = await fetch("/api/v1/setting");
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};
export const fetchServerMetrics = async (
server_id: number,
metric: MetricType,
period?: MetricPeriod,
): Promise<ServerMetricsResponse> => {
const query = period
? `?metric=${metric}&period=${period}`
: `?metric=${metric}`;
const response = await fetch(`/api/v1/server/${server_id}/metrics${query}`);
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};
+281 -259
View File
@@ -1,322 +1,344 @@
import { NezhaServer } from "@/types/nezha-api"
import { type ClassValue, clsx } from "clsx"
import dayjs from "dayjs"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs";
import { twMerge } from "tailwind-merge";
import type { NezhaServer } from "@/types/nezha-api";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
const lastActiveTime = serverInfo.last_active.startsWith("000") ? 0 : parseISOTimestamp(serverInfo.last_active)
return {
...serverInfo,
cpu: serverInfo.state.cpu || 0,
gpu: serverInfo.state.gpu || [],
process: serverInfo.state.process_count || 0,
up: serverInfo.state.net_out_speed / 1024 / 1024 || 0,
down: serverInfo.state.net_in_speed / 1024 / 1024 || 0,
last_active_time_string: lastActiveTime ? dayjs(lastActiveTime).format("YYYY-MM-DD HH:mm:ss") : "",
online: now - lastActiveTime <= 30000,
uptime: serverInfo.state.uptime || 0,
version: serverInfo.host.version || null,
tcp: serverInfo.state.tcp_conn_count || 0,
udp: serverInfo.state.udp_conn_count || 0,
mem: (serverInfo.state.mem_used / serverInfo.host.mem_total) * 100 || 0,
swap: (serverInfo.state.swap_used / serverInfo.host.swap_total) * 100 || 0,
disk: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0,
stg: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0,
country_code: serverInfo.country_code,
platform: serverInfo.host.platform || "",
net_out_transfer: serverInfo.state.net_out_transfer || 0,
net_in_transfer: serverInfo.state.net_in_transfer || 0,
arch: serverInfo.host.arch || "",
mem_total: serverInfo.host.mem_total || 0,
swap_total: serverInfo.host.swap_total || 0,
disk_total: serverInfo.host.disk_total || 0,
boot_time: serverInfo.host.boot_time || 0,
boot_time_string: serverInfo.host.boot_time ? dayjs(serverInfo.host.boot_time * 1000).format("YYYY-MM-DD HH:mm:ss") : "",
platform_version: serverInfo.host.platform_version || "",
cpu_info: serverInfo.host.cpu || [],
gpu_info: serverInfo.host.gpu || [],
load_1: serverInfo.state.load_1?.toFixed(2) || 0.0,
load_5: serverInfo.state.load_5?.toFixed(2) || 0.0,
load_15: serverInfo.state.load_15?.toFixed(2) || 0.0,
public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""),
}
const lastActiveTime = serverInfo.last_active.startsWith("000")
? 0
: parseISOTimestamp(serverInfo.last_active);
return {
...serverInfo,
cpu: serverInfo.state.cpu || 0,
gpu: serverInfo.state.gpu || [],
process: serverInfo.state.process_count || 0,
up: serverInfo.state.net_out_speed / 1024 / 1024 || 0,
down: serverInfo.state.net_in_speed / 1024 / 1024 || 0,
last_active_time_string: lastActiveTime
? dayjs(lastActiveTime).format("YYYY-MM-DD HH:mm:ss")
: "",
online: now - lastActiveTime <= 30000,
uptime: serverInfo.state.uptime || 0,
version: serverInfo.host.version || null,
tcp: serverInfo.state.tcp_conn_count || 0,
udp: serverInfo.state.udp_conn_count || 0,
mem: (serverInfo.state.mem_used / serverInfo.host.mem_total) * 100 || 0,
swap: (serverInfo.state.swap_used / serverInfo.host.swap_total) * 100 || 0,
disk: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0,
stg: (serverInfo.state.disk_used / serverInfo.host.disk_total) * 100 || 0,
country_code: serverInfo.country_code,
platform: serverInfo.host.platform || "",
net_out_transfer: serverInfo.state.net_out_transfer || 0,
net_in_transfer: serverInfo.state.net_in_transfer || 0,
arch: serverInfo.host.arch || "",
mem_total: serverInfo.host.mem_total || 0,
swap_total: serverInfo.host.swap_total || 0,
disk_total: serverInfo.host.disk_total || 0,
boot_time: serverInfo.host.boot_time || 0,
boot_time_string: serverInfo.host.boot_time
? dayjs(serverInfo.host.boot_time * 1000).format("YYYY-MM-DD HH:mm:ss")
: "",
platform_version: serverInfo.host.platform_version || "",
cpu_info: serverInfo.host.cpu || [],
gpu_info: serverInfo.host.gpu || [],
load_1: serverInfo.state.load_1?.toFixed(2) || 0.0,
load_5: serverInfo.state.load_5?.toFixed(2) || 0.0,
load_15: serverInfo.state.load_15?.toFixed(2) || 0.0,
public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""),
};
}
export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDate, endDate }: BillingData): {
days: number
cycleLabel: string
remainingPercentage: number
export function getDaysBetweenDatesWithAutoRenewal({
autoRenewal,
cycle,
startDate,
endDate,
}: BillingData): {
days: number;
cycleLabel: string;
remainingPercentage: number;
} {
let months = 1
// 套餐资费
let cycleLabel = cycle
let months = 1;
// 套餐资费
let cycleLabel = cycle;
switch (cycle.toLowerCase()) {
case "月":
case "m":
case "mo":
case "month":
case "monthly":
cycleLabel = "月"
months = 1
break
case "年":
case "y":
case "yr":
case "year":
case "annual":
cycleLabel = "年"
months = 12
break
case "季":
case "q":
case "qr":
case "quarterly":
cycleLabel = "季"
months = 3
break
case "半":
case "半年":
case "h":
case "half":
case "semi-annually":
cycleLabel = "半年"
months = 6
break
default:
cycleLabel = cycle
break
}
switch (cycle.toLowerCase()) {
case "月":
case "m":
case "mo":
case "month":
case "monthly":
cycleLabel = "月";
months = 1;
break;
case "年":
case "y":
case "yr":
case "year":
case "annual":
cycleLabel = "年";
months = 12;
break;
case "季":
case "q":
case "qr":
case "quarterly":
cycleLabel = "季";
months = 3;
break;
case "半":
case "半年":
case "h":
case "half":
case "semi-annually":
cycleLabel = "半年";
months = 6;
break;
default:
cycleLabel = cycle;
break;
}
const nowTime = new Date().getTime()
const endTime = dayjs(endDate).valueOf()
const nowTime = Date.now();
const endTime = dayjs(endDate).valueOf();
if (autoRenewal !== "1") {
return {
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel,
remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day") > 1
? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day"),
}
}
if (autoRenewal !== "1") {
return {
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel,
remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
dayjs(endDate).diff(startDate, "day") >
1
? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
dayjs(endDate).diff(startDate, "day"),
};
}
if (nowTime < endTime) {
return {
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel,
remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months) > 1
? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months),
}
}
if (nowTime < endTime) {
return {
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel,
remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
(30 * months) >
1
? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
(30 * months),
};
}
const nextTime = getNextCycleTime(endTime, months, nowTime)
const diff = dayjs(nextTime).diff(dayjs(), "day") + 1
const remainingPercentage = diff / (30 * months) > 1 ? 1 : diff / (30 * months)
const nextTime = getNextCycleTime(endTime, months, nowTime);
const diff = dayjs(nextTime).diff(dayjs(), "day") + 1;
const remainingPercentage =
diff / (30 * months) > 1 ? 1 : diff / (30 * months);
return {
days: diff,
cycleLabel: cycleLabel,
remainingPercentage: remainingPercentage,
}
return {
days: diff,
cycleLabel: cycleLabel,
remainingPercentage: remainingPercentage,
};
}
// Thanks to hi2shark for the code
// https://github.com/hi2shark/nazhua/blob/main/src/utils/date.js#L86
export function getNextCycleTime(startDate: number, months: number, specifiedDate: number): number {
const start = dayjs(startDate)
const checkDate = dayjs(specifiedDate)
export function getNextCycleTime(
startDate: number,
months: number,
specifiedDate: number,
): number {
const start = dayjs(startDate);
const checkDate = dayjs(specifiedDate);
if (!start.isValid() || months <= 0) {
throw new Error("参数无效:请检查起始日期、周期月份数和指定日期。")
}
if (!start.isValid() || months <= 0) {
throw new Error("参数无效:请检查起始日期、周期月份数和指定日期。");
}
let nextDate = start
let nextDate = start;
// 循环增加周期直到大于当前日期
let whileStatus = true
while (whileStatus) {
nextDate = nextDate.add(months, "month")
whileStatus = nextDate.valueOf() <= checkDate.valueOf()
}
// 循环增加周期直到大于当前日期
let whileStatus = true;
while (whileStatus) {
nextDate = nextDate.add(months, "month");
whileStatus = nextDate.valueOf() <= checkDate.valueOf();
}
return nextDate.valueOf() // 返回时间毫秒数
return nextDate.valueOf(); // 返回时间毫秒数
}
export function getDaysBetweenDates(date1: string, date2: string): number {
const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
const firstDate = new Date(date1)
const secondDate = new Date(date2)
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
const firstDate = new Date(date1);
const secondDate = new Date(date2);
// 计算两个日期之间的天数差异
return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay)
// 计算两个日期之间的天数差异
return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay);
}
export const fetcher = (url: string) =>
fetch(url)
.then((res) => {
if (!res.ok) {
throw new Error(res.statusText)
}
return res.json()
})
.then((data) => data.data)
.catch((err) => {
console.error(err)
throw err
})
fetch(url)
.then((res) => {
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json();
})
.then((data) => data.data)
.catch((err) => {
console.error(err);
throw err;
});
export const nezhaFetcher = async (url: string) => {
const res = await fetch(url)
const res = await fetch(url);
if (!res.ok) {
const error = new Error("An error occurred while fetching the data.")
// @ts-expect-error - res.json() returns a Promise<any>
error.info = await res.json()
// @ts-expect-error - res.status is a number
error.status = res.status
throw error
}
if (!res.ok) {
const error = new Error("An error occurred while fetching the data.");
// @ts-expect-error - res.json() returns a Promise<any>
error.info = await res.json();
// @ts-expect-error - res.status is a number
error.status = res.status;
throw error;
}
return res.json()
}
return res.json();
};
export function parseISOTimestamp(isoString: string): number {
return new Date(isoString).getTime()
return new Date(isoString).getTime();
}
export function formatRelativeTime(timestamp: number): string {
const now = Date.now()
const diff = now - timestamp
const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
const now = Date.now();
const diff = now - timestamp;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (hours > 24) {
const days = Math.floor(hours / 24)
return `${days}d`
} else if (hours > 0) {
return `${hours}h`
} else if (minutes > 0) {
return `${minutes}m`
} else if (seconds >= 0) {
return `${seconds}s`
}
return "0s"
if (hours > 24) {
const days = Math.floor(hours / 24);
return `${days}d`;
} else if (hours > 0) {
return `${hours}h`;
} else if (minutes > 0) {
return `${minutes}m`;
} else if (seconds >= 0) {
return `${seconds}s`;
}
return "0s";
}
export function formatTime(timestamp: number): string {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hours = date.getHours().toString().padStart(2, "0")
const minutes = date.getMinutes().toString().padStart(2, "0")
const seconds = date.getSeconds().toString().padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
interface BillingData {
startDate: string
endDate: string
autoRenewal: string
cycle: string
amount: string
startDate: string;
endDate: string;
autoRenewal: string;
cycle: string;
amount: string;
}
interface PlanData {
bandwidth: string
trafficVol: string
trafficType: string
IPv4: string
IPv6: string
networkRoute: string
extra: string
bandwidth: string;
trafficVol: string;
trafficType: string;
IPv4: string;
IPv6: string;
networkRoute: string;
extra: string;
}
export interface PublicNoteData {
billingDataMod?: BillingData
planDataMod?: PlanData
billingDataMod?: BillingData;
planDataMod?: PlanData;
}
export function parsePublicNote(publicNote: string): PublicNoteData | null {
try {
if (!publicNote) {
return null
}
const data = JSON.parse(publicNote)
if (!data.billingDataMod && !data.planDataMod) {
return null
}
if (data.billingDataMod && !data.planDataMod) {
return {
billingDataMod: {
startDate: data.billingDataMod.startDate || "",
endDate: data.billingDataMod.endDate,
autoRenewal: data.billingDataMod.autoRenewal || "",
cycle: data.billingDataMod.cycle || "",
amount: data.billingDataMod.amount || "",
},
}
}
if (!data.billingDataMod && data.planDataMod) {
return {
planDataMod: {
bandwidth: data.planDataMod.bandwidth || "",
trafficVol: data.planDataMod.trafficVol || "",
trafficType: data.planDataMod.trafficType || "",
IPv4: data.planDataMod.IPv4 || "",
IPv6: data.planDataMod.IPv6 || "",
networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "",
},
}
}
try {
if (!publicNote) {
return null;
}
const data = JSON.parse(publicNote);
if (!data.billingDataMod && !data.planDataMod) {
return null;
}
if (data.billingDataMod && !data.planDataMod) {
return {
billingDataMod: {
startDate: data.billingDataMod.startDate || "",
endDate: data.billingDataMod.endDate,
autoRenewal: data.billingDataMod.autoRenewal || "",
cycle: data.billingDataMod.cycle || "",
amount: data.billingDataMod.amount || "",
},
};
}
if (!data.billingDataMod && data.planDataMod) {
return {
planDataMod: {
bandwidth: data.planDataMod.bandwidth || "",
trafficVol: data.planDataMod.trafficVol || "",
trafficType: data.planDataMod.trafficType || "",
IPv4: data.planDataMod.IPv4 || "",
IPv6: data.planDataMod.IPv6 || "",
networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "",
},
};
}
return {
billingDataMod: {
startDate: data.billingDataMod.startDate || "",
endDate: data.billingDataMod.endDate,
autoRenewal: data.billingDataMod.autoRenewal || "",
cycle: data.billingDataMod.cycle || "",
amount: data.billingDataMod.amount || "",
},
planDataMod: {
bandwidth: data.planDataMod.bandwidth || "",
trafficVol: data.planDataMod.trafficVol || "",
trafficType: data.planDataMod.trafficType || "",
IPv4: data.planDataMod.IPv4 || "",
IPv6: data.planDataMod.IPv6 || "",
networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "",
},
}
} catch (error) {
console.error("Error parsing public note:", error)
return null
}
return {
billingDataMod: {
startDate: data.billingDataMod.startDate || "",
endDate: data.billingDataMod.endDate,
autoRenewal: data.billingDataMod.autoRenewal || "",
cycle: data.billingDataMod.cycle || "",
amount: data.billingDataMod.amount || "",
},
planDataMod: {
bandwidth: data.planDataMod.bandwidth || "",
trafficVol: data.planDataMod.trafficVol || "",
trafficType: data.planDataMod.trafficType || "",
IPv4: data.planDataMod.IPv4 || "",
IPv6: data.planDataMod.IPv6 || "",
networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "",
},
};
} catch (error) {
console.error("Error parsing public note:", error);
return null;
}
}
// Function to handle public_note with sessionStorage
export function handlePublicNote(serverId: number, publicNote: string): string {
const storageKey = `server_${serverId}_public_note`
const storedNote = sessionStorage.getItem(storageKey)
const storageKey = `server_${serverId}_public_note`;
const storedNote = sessionStorage.getItem(storageKey);
if (!publicNote && storedNote) {
return storedNote
}
if (!publicNote && storedNote) {
return storedNote;
}
if (publicNote) {
sessionStorage.setItem(storageKey, publicNote)
return publicNote
}
if (publicNote) {
sessionStorage.setItem(storageKey, publicNote);
return publicNote;
}
return ""
return "";
}
+124 -118
View File
@@ -1,120 +1,126 @@
{
"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"
"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",
"packetLoss": "Paketverlust",
"clearSelections": "Löschen",
"peakCut": "Peak cut",
"period1d": "1 Tag",
"period7d": "7 Tage",
"period30d": "30 Tage"
},
"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"
}
+146 -133
View File
@@ -1,135 +1,148 @@
{
"nezha": "Nezha Monitoring",
"overview": "Overview",
"dashboard": "Dashboard",
"login": "Login",
"online": "Online",
"offline": "Offline",
"whereTheTimeIs": "Where the time is",
"refreshing": "Refreshing",
"info": {
"websocketConnecting": "WebSocket connecting",
"websocketConnected": "WebSocket connected",
"websocketDisconnected": "WebSocket disconnected",
"processing": "Processing..."
},
"serverOverview": {
"totalServers": "Total Servers",
"onlineServers": "Online Servers",
"offlineServers": "Offline Servers",
"totalBandwidth": "Total Bandwidth",
"speed": "Speed",
"network": "Network"
},
"map": {
"Distributions": "Servers are distributed in",
"Regions": "Regions",
"Servers": "servers"
},
"serverCard": {
"mem": "MEM",
"stg": "STG",
"days": "Days",
"hours": "Hours",
"upload": "Upload",
"download": "Download",
"system": "System",
"uptime": "Uptime",
"totalUpload": "Upload",
"totalDownload": "Download"
},
"cycleTransfer": {
"used": "used",
"total": "total",
"nextUpdate": "next update"
},
"serviceTracker": {
"noService": "No service data",
"uptime": "Uptime",
"delay": "Delay",
"daysAgo": "days ago",
"today": "Today",
"loading": "Loading..."
},
"serverDetail": {
"status": "Status",
"online": "Online",
"days": "Days",
"hours": "Hours",
"offline": "Offline",
"unknown": "Unknown",
"uptime": "Uptime",
"version": "Version",
"arch": "Arch",
"mem": "Mem",
"disk": "Disk",
"region": "Region",
"system": "System",
"upload": "Upload",
"download": "Download",
"lastActive": "Last active time",
"temperature": "Temperature",
"bootTime": "Boot time"
},
"serverDetailChart": {
"process": "Process",
"disk": "Disk",
"mem": "Mem",
"swap": "Swap",
"upload": "Upload",
"download": "Download"
},
"footer": {
"themeBy": "Theme by "
},
"language": {
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"en-US": "English",
"de-DE": "Deutsch",
"es-ES": "Español",
"ru-RU": "Русский",
"ta-IN": "தமிழ்"
},
"theme": {
"light": "Light",
"dark": "Dark",
"system": "System"
},
"error": {
"pageNotFound": "Page not found",
"backToHome": "Back to home"
},
"tabSwitch": {
"Detail": "Detail",
"Network": "Network"
},
"monitor": {
"noData": "No server monitor data, please add a service monitor first",
"avgDelay": "Latency",
"monitorCount": "Services"
},
"pwa": {
"offlineReady": "App ready to work offline",
"newContent": "New content available",
"reload": "Update"
},
"billingInfo": {
"remaining": "Remaining",
"error": "error",
"indefinite": "Indefinite",
"expired": "Expired",
"days": "days",
"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"
"nezha": "Nezha Monitoring",
"overview": "Overview",
"dashboard": "Dashboard",
"login": "Login",
"online": "Online",
"offline": "Offline",
"whereTheTimeIs": "Where the time is",
"refreshing": "Refreshing",
"info": {
"websocketConnecting": "WebSocket connecting",
"websocketConnected": "WebSocket connected",
"websocketDisconnected": "WebSocket disconnected",
"processing": "Processing..."
},
"serverOverview": {
"totalServers": "Total Servers",
"onlineServers": "Online Servers",
"offlineServers": "Offline Servers",
"totalBandwidth": "Total Bandwidth",
"speed": "Speed",
"network": "Network"
},
"map": {
"Distributions": "Servers are distributed in",
"Regions": "Regions",
"Servers": "servers"
},
"serverCard": {
"mem": "MEM",
"stg": "STG",
"days": "Days",
"hours": "Hours",
"upload": "Upload",
"download": "Download",
"system": "System",
"uptime": "Uptime",
"totalUpload": "Upload",
"totalDownload": "Download"
},
"cycleTransfer": {
"used": "used",
"total": "total",
"nextUpdate": "next update"
},
"serviceTracker": {
"noService": "No service data",
"uptime": "Uptime",
"delay": "Delay",
"daysAgo": "days ago",
"today": "Today",
"loading": "Loading..."
},
"serverDetail": {
"status": "Status",
"online": "Online",
"days": "Days",
"hours": "Hours",
"offline": "Offline",
"unknown": "Unknown",
"uptime": "Uptime",
"version": "Version",
"arch": "Arch",
"mem": "Mem",
"disk": "Disk",
"region": "Region",
"system": "System",
"upload": "Upload",
"download": "Download",
"lastActive": "Last active time",
"temperature": "Temperature",
"bootTime": "Boot time"
},
"serverDetailChart": {
"process": "Process",
"disk": "Disk",
"mem": "Mem",
"swap": "Swap",
"upload": "Upload",
"download": "Download",
"realtime": "Realtime",
"period1d": "1 Day",
"period7d": "7 Days",
"period30d": "30 Days",
"tsdbRequired": "Enable TSDB to use historical data",
"loginRequired": "Please login to view"
},
"footer": {
"themeBy": "Theme by "
},
"language": {
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"en-US": "English",
"de-DE": "Deutsch",
"es-ES": "Español",
"ru-RU": "Русский",
"ta-IN": "தமிழ்"
},
"theme": {
"light": "Light",
"dark": "Dark",
"system": "System"
},
"error": {
"pageNotFound": "Page not found",
"backToHome": "Back to home"
},
"tabSwitch": {
"Detail": "Detail",
"Network": "Network"
},
"monitor": {
"noData": "No server monitor data, please add a service monitor first",
"avgDelay": "Latency",
"monitorCount": "Services",
"packetLoss": "Packet Loss",
"clearSelections": "Clear",
"peakCut": "Peak cut",
"loginRequired": "Please login to view",
"period1d": "1 Day",
"period7d": "7 Days",
"period30d": "30 Days"
},
"pwa": {
"offlineReady": "App ready to work offline",
"newContent": "New content available",
"reload": "Update"
},
"billingInfo": {
"remaining": "Remaining",
"error": "error",
"indefinite": "Indefinite",
"expired": "Expired",
"days": "days",
"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"
}
+139 -133
View File
@@ -1,135 +1,141 @@
{
"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",
"de-DE": "Deutsch",
"es-ES": "Español",
"ru-RU": "Русский",
"ta-IN": "தமிழ்"
},
"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": "Monitor 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": "Alm."
},
"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. "
}
"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",
"de-DE": "Deutsch",
"es-ES": "Español",
"ru-RU": "Русский",
"ta-IN": "தமிழ்"
},
"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": "Monitor 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": "Alm."
},
"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",
"packetLoss": "Pérdida de paquetes",
"clearSelections": "Limpiar",
"peakCut": "Peak cut",
"period1d": "1 Día",
"period7d": "7 Días",
"period30d": "30 Días"
},
"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. "
}
}
+137
View File
@@ -0,0 +1,137 @@
{
"nezha": "Monitoreo Nezha",
"theme": {
"light": "Claro",
"dark": "Escuro",
"system": "Sistema"
},
"overview": "Visión xeral",
"dashboard": "Panel de control",
"login": "Inciar sesión",
"online": "Activo",
"offline": "Desconectado",
"whereTheTimeIs": "Onde está a hora",
"refreshing": "Recargando",
"info": {
"websocketConnecting": "Conectando o WebSocket",
"websocketConnected": "WebSocket conectado",
"websocketDisconnected": "WebSocket desconectado",
"processing": "Procesando..."
},
"serverOverview": {
"totalServers": "Servidores totais",
"onlineServers": "Servidores en liña",
"offlineServers": "Servidores desconectados",
"totalBandwidth": "Ancho de banda total",
"speed": "Velocidade",
"network": "Rede"
},
"map": {
"Distributions": "Os servidores están distribuidos en",
"Regions": "Rexións",
"Servers": "servidores"
},
"serverCard": {
"mem": "MEM",
"stg": "STG",
"days": "Dias",
"hours": "Horas",
"upload": "Subir",
"download": "Descargar",
"system": "Sistema",
"uptime": "Tempo activo",
"totalUpload": "Subir",
"totalDownload": "Descargar"
},
"cycleTransfer": {
"used": "empregado",
"total": "total",
"nextUpdate": "próxima actualización"
},
"serviceTracker": {
"noService": "Non hai información do servizo",
"uptime": "Tempo de actividade",
"delay": "Atraso",
"daysAgo": "dias atrás",
"today": "Hoxe",
"loading": "Cargando..."
},
"serverDetail": {
"status": "Estado",
"online": "Activo",
"days": "Días",
"hours": "Horas",
"offline": "Desconectado",
"unknown": "Descoñecido",
"uptime": "Tempo activo",
"version": "Versión",
"arch": "Arch",
"mem": "Mem",
"disk": "Disco",
"region": "Rexión",
"system": "Sistema",
"upload": "Subida",
"download": "Descarga",
"lastActive": "Última vez activo",
"temperature": "Temperatura",
"bootTime": "Tempo de reinicio"
},
"serverDetailChart": {
"process": "Proceso",
"disk": "Disco",
"mem": "Mem",
"swap": "Swap",
"upload": "Subida",
"download": "Descarga"
},
"footer": {
"themeBy": "Tema por "
},
"language": {
"zh-CN": "简体中文 (Chinés simplificado)",
"zh-TW": "繁體中文 (Chinés tradicional)",
"en-US": "English (Inglés)",
"de-DE": "Deutsch (Alemán)",
"es-ES": "Español",
"ru-RU": "Русский (Ruso)",
"ta-IN": "தமிழ் (Tamil)"
},
"error": {
"pageNotFound": "Páxina non atopada",
"backToHome": "Voltar áo inicio"
},
"tabSwitch": {
"Detail": "Detalle",
"Network": "Rede"
},
"monitor": {
"noData": "Non hai datos de monitor de servidor. Engada primeiro un monitor de servizo",
"avgDelay": "Latencia",
"monitorCount": "Servizos",
"packetLoss": "Pérdida de datos",
"clearSelections": "Limpar"
},
"pwa": {
"offlineReady": "Aplicación lista para traballar desconectada (offline)",
"newContent": "Novo contido dispoñíble",
"reload": "Actualizar"
},
"billingInfo": {
"remaining": "Restante",
"error": "erro",
"indefinite": "Indefinido",
"expired": "Expirado",
"days": "días",
"price": "Precio",
"free": "Gratis",
"usage-baseed": "Baseado no uso"
},
"TypeCommand": "Escribe un comando ou procura...",
"NoResults": "Non se atoparon resultados.",
"Servers": "Servidores",
"Shortcuts": "Atallos",
"ToggleLightMode": "Cambiar ao modo claro",
"ToggleDarkMode": "Cambiar ao modo escuro",
"ToggleSystemMode": "Cambiar ao determinado polo sistema",
"Home": "Páxina principal"
}
+25 -25
View File
@@ -1,27 +1,27 @@
{
"nezha": "Monitor Nezha",
"overview": "Ringkasan",
"dashboard": "Dasbor",
"login": "Masuk",
"online": "Online",
"offline": "Offline",
"whereTheTimeIs": "Dimana waktunya",
"refreshing": "Menyegarkan",
"info": {
"websocketConnecting": "WebSocket menghubungkan",
"processing": "Memproses...",
"websocketDisconnected": "WebSocket terputus",
"websocketConnected": "WebSocket terhubung"
},
"serverOverview": {
"totalServers": "Jumlah Server",
"onlineServers": "Online Server",
"offlineServers": "Offline Server",
"speed": "Kecepatan",
"totalBandwidth": "Jumlah Bandwidth"
},
"theme": {
"light": "Terang",
"dark": "Gelap"
}
"nezha": "Monitor Nezha",
"overview": "Ringkasan",
"dashboard": "Dasbor",
"login": "Masuk",
"online": "Online",
"offline": "Offline",
"whereTheTimeIs": "Dimana waktunya",
"refreshing": "Menyegarkan",
"info": {
"websocketConnecting": "WebSocket menghubungkan",
"processing": "Memproses...",
"websocketDisconnected": "WebSocket terputus",
"websocketConnected": "WebSocket terhubung"
},
"serverOverview": {
"totalServers": "Jumlah Server",
"onlineServers": "Online Server",
"offlineServers": "Offline Server",
"speed": "Kecepatan",
"totalBandwidth": "Jumlah Bandwidth"
},
"theme": {
"light": "Terang",
"dark": "Gelap"
}
}
+1
View File
@@ -0,0 +1 @@
{}
+143 -133
View File
@@ -1,135 +1,145 @@
{
"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"
"map": {
"Servers": "сервера",
"Distributions": "Серверы распределены в",
"Regions": "Регионы"
},
"serverDetailChart": {
"disk": "Диск",
"download": "Скачивание",
"swap": "Swap",
"upload": "Загрузка",
"mem": "Mem",
"process": "Процесс",
"realtime": "Сейчас",
"period1d": "1 День",
"period7d": "7 Дней",
"period30d": "30 Дней"
},
"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": "Сервисы",
"packetLoss": "Потеря пакетов",
"clearSelections": "Очистить",
"peakCut": "Peak cut",
"period1d": "1 день",
"period7d": "7 дней",
"period30d": "30 дней"
},
"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"
}
+124 -118
View File
@@ -1,120 +1,126 @@
{
"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": "ஏற்றுகிறது ..."
}
"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": "சேவைகள்",
"packetLoss": "தொகுப்பு இழப்பு",
"clearSelections": "அழி",
"peakCut": "Peak cut",
"period1d": "1 நாள்",
"period7d": "7 நாட்கள்",
"period30d": "30 நாட்கள்"
},
"pwa": {
"offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது",
"newContent": "புதிய உள்ளடக்கம் கிடைக்கிறது",
"reload": "புதுப்பிப்பு"
},
"billingInfo": {
"remaining": "மீதமுள்ள",
"error": "பிழை",
"indefinite": "காலவரையற்றது",
"expired": "காலாவதியான",
"days": "நாட்கள்",
"price": "விலை",
"free": "இலவசம்",
"usage-baseed": "பயன்பாடு அடிப்படையிலானது"
},
"serviceTracker": {
"noService": "பணி தரவு இல்லை",
"uptime": "நேரம்",
"daysAgo": "சில நாட்களுக்கு முன்பு",
"today": "இன்று",
"loading": "ஏற்றுகிறது ..."
}
}
+73
View File
@@ -0,0 +1,73 @@
{
"nezha": "Nezha Моніторинг",
"theme": {
"light": "Світло",
"dark": "Темний",
"system": "Як в Системі"
},
"NoResults": "Немає результатів.",
"Servers": "Сервери",
"map": {
"Servers": "сервери",
"Distributions": "Сервери розподілені в",
"Regions": "Регіони"
},
"serverCard": {
"mem": "МЕМ",
"stg": "STG",
"days": "Днів",
"hours": "Годин",
"upload": "Завантаження",
"download": "Завантажити",
"system": "Система",
"uptime": "Аптайм",
"totalUpload": "Завантажено",
"totalDownload": "Скачано"
},
"serverDetail": {
"mem": "Мем",
"status": "Статус",
"online": "Online",
"days": "Днів",
"hours": "Годин",
"offline": "Offline",
"unknown": "Невідомо"
},
"serverDetailChart": {
"mem": "Мем"
},
"overview": "Огляд",
"dashboard": "Інформаційна панель",
"online": "Онлайн",
"offline": "Оффлайн",
"whereTheTimeIs": "Де час",
"refreshing": "Оновлення",
"info": {
"websocketConnecting": "WebSocket з'єднання",
"websocketConnected": "WebSocket з'єднано",
"websocketDisconnected": "WebSocket відключено",
"processing": "Обробка..."
},
"serverOverview": {
"totalServers": "Всього Серверів",
"onlineServers": "Сервери в мережі",
"offlineServers": "Сервери не в мережі",
"totalBandwidth": "Загальна пропускна здатність",
"speed": "Швидкість",
"network": "Мережа"
},
"cycleTransfer": {
"used": "використано",
"total": "всього",
"nextUpdate": "наступне оновлення"
},
"serviceTracker": {
"noService": "Немає даних про сервіс",
"uptime": "Аптайм",
"delay": "Затримка",
"daysAgo": "Днів тому",
"today": "Сьогодні",
"loading": "Завантаження..."
},
"login": "Логін"
}
+147 -134
View File
@@ -1,136 +1,149 @@
{
"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": "English",
"en-US": "English",
"de-DE": "Deutsch",
"es-ES": "Español",
"ta-IN": "தமிழ்",
"ru-RU": "Русский"
},
"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": "首页"
"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": "下载",
"realtime": "实时",
"period1d": "1 天",
"period7d": "7 天",
"period30d": "30 天",
"tsdbRequired": "需要开启 TSDB 才能启用历史记录功能",
"loginRequired": "请登录后查看"
},
"footer": {
"themeBy": "主题-"
},
"language": {
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"en": "English",
"en-US": "English",
"de-DE": "Deutsch",
"es-ES": "Español",
"ta-IN": "தமிழ்",
"ru-RU": "Русский"
},
"theme": {
"light": "亮色",
"dark": "暗色",
"system": "跟随系统"
},
"error": {
"pageNotFound": "页面不存在",
"backToHome": "回到主页"
},
"tabSwitch": {
"Detail": "详情",
"Network": "网络"
},
"monitor": {
"noData": "没有服务监控数据,请在管理后台服务页添加监控任务",
"avgDelay": "延迟",
"monitorCount": "个监控服务",
"packetLoss": "丢包率",
"clearSelections": "清除",
"peakCut": "削峰",
"loginRequired": "请登录后查看",
"period1d": "1 天",
"period7d": "7 天",
"period30d": "30 天"
},
"pwa": {
"offlineReady": "应用可以离线使用了",
"newContent": "发现新版本",
"reload": "更新"
},
"billingInfo": {
"remaining": "剩余天数",
"error": "计算错误",
"indefinite": "永久",
"expired": "已过期",
"days": "天",
"price": "价格",
"free": "免费",
"usage-baseed": "按量计费"
},
"TypeCommand": "输入命令或搜索",
"NoResults": "结果为空",
"Servers": "服务器",
"Shortcuts": "快捷键",
"ToggleLightMode": "切换亮色模式",
"ToggleDarkMode": "切换暗色模式",
"ToggleSystemMode": "切换系统模式",
"Home": "首页"
}
+142 -136
View File
@@ -1,138 +1,144 @@
{
"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",
"de-DE": "德文",
"es-ES": "西班牙文",
"ru-RU": "俄文",
"ta-IN": "தமிழ்"
},
"theme": {
"light": "亮色",
"dark": "暗色",
"system": "跟隨系統"
},
"error": {
"pageNotFound": "頁面不存在",
"backToHome": "回到主頁"
},
"tabSwitch": {
"detail": "詳細資訊",
"network": "網路",
"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": "首頁",
"pwa": {
"offlineReady": "可離線使用之應用程式",
"newContent": "有新内容可用",
"reload": "更新"
}
"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",
"de-DE": "德文",
"es-ES": "西班牙文",
"ru-RU": "俄文",
"ta-IN": "தமிழ்"
},
"theme": {
"light": "亮色",
"dark": "暗色",
"system": "跟隨系統"
},
"error": {
"pageNotFound": "頁面不存在",
"backToHome": "回到主頁"
},
"tabSwitch": {
"detail": "詳細資訊",
"network": "網路",
"Detail": "詳細資訊",
"Network": "網路"
},
"monitor": {
"noData": "沒有服務監控數據,請在管理後台服務新增監控任務",
"status": "狀態",
"avgDelay": "延遲",
"monitorCount": "個監控",
"packetLoss": "丟包率",
"clearSelections": "清除",
"peakCut": "削峰",
"period1d": "1 天",
"period7d": "7 天",
"period30d": "30 天"
},
"billingInfo": {
"remaining": "剩餘天數",
"error": "獲取失敗",
"indefinite": "無限期",
"expired": "已過期",
"days": "",
"price": "價格",
"free": "免費",
"usage-baseed": "按量計費"
},
"TypeCommand": "輸入命令或搜尋",
"NoResults": "沒有結果",
"Servers": "伺服器",
"Shortcuts": "快捷鍵",
"ToggleLightMode": "切換亮色模式",
"ToggleDarkMode": "切換暗色模式",
"ToggleSystemMode": "切換系統模式",
"Home": "首頁",
"pwa": {
"offlineReady": "可離線使用之應用程式",
"newContent": "有新内容可用",
"reload": "更新"
}
}
+53 -44
View File
@@ -1,47 +1,56 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import ReactDOM from "react-dom/client"
import { Toaster } from "sonner"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import ReactDOM from "react-dom/client";
import { Toaster } from "sonner";
import App from "./App"
import { ThemeColorManager } from "./components/ThemeColorManager"
import { ThemeProvider } from "./components/ThemeProvider"
import { MotionProvider } from "./components/motion/motion-provider"
import { SortProvider } from "./context/sort-provider"
import { StatusProvider } from "./context/status-provider"
import { TooltipProvider } from "./context/tooltip-provider"
import { WebSocketProvider } from "./context/websocket-provider"
import "./i18n"
import "./index.css"
import App from "./App";
import { MotionProvider } from "./components/motion/motion-provider";
import { ThemeColorManager } from "./components/ThemeColorManager";
import { ThemeProvider } from "./components/ThemeProvider";
import { CommandProvider } from "./context/command-provider";
import { SortProvider } from "./context/sort-provider";
import { StatusProvider } from "./context/status-provider";
import { TooltipProvider } from "./context/tooltip-provider";
import { WebSocketProvider } from "./context/websocket-provider";
import "./i18n";
import "./index.css";
const queryClient = new QueryClient()
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")!).render(
<MotionProvider>
<ThemeProvider storageKey="vite-ui-theme">
<ThemeColorManager />
<QueryClientProvider client={queryClient}>
<WebSocketProvider url="/api/v1/ws/server">
<StatusProvider>
<SortProvider>
<TooltipProvider>
<App />
<Toaster
duration={1000}
toastOptions={{
classNames: {
default: "w-fit rounded-full px-2.5 py-1.5 bg-neutral-100 border border-neutral-200 backdrop-blur-xl shadow-none",
},
}}
position="top-center"
className={"flex items-center justify-center"}
/>
<ReactQueryDevtools />
</TooltipProvider>
</SortProvider>
</StatusProvider>
</WebSocketProvider>
</QueryClientProvider>
</ThemeProvider>
</MotionProvider>,
)
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error("Root element not found");
}
ReactDOM.createRoot(rootElement).render(
<MotionProvider>
<ThemeProvider storageKey="vite-ui-theme">
<ThemeColorManager />
<QueryClientProvider client={queryClient}>
<WebSocketProvider url="/api/v1/ws/server">
<CommandProvider>
<StatusProvider>
<SortProvider>
<TooltipProvider>
<App />
<Toaster
duration={1000}
toastOptions={{
classNames: {
default:
"w-fit rounded-full px-2.5 py-1.5 bg-neutral-100 border border-neutral-200 backdrop-blur-xl shadow-none",
},
}}
position="top-center"
className={"flex items-center justify-center"}
/>
<ReactQueryDevtools />
</TooltipProvider>
</SortProvider>
</StatusProvider>
</CommandProvider>
</WebSocketProvider>
</QueryClientProvider>
</ThemeProvider>
</MotionProvider>,
);
+14 -12
View File
@@ -1,19 +1,21 @@
import { useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next";
interface ErrorPageProps {
code?: string | number
message?: string
code?: string | number;
message?: string;
}
export default function ErrorPage({ code = "500", message }: ErrorPageProps) {
const { t } = useTranslation()
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">{code}</h1>
<p className="text-xl text-muted-foreground">{message || t("error.somethingWentWrong")}</p>
</div>
</div>
)
return (
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">{code}</h1>
<p className="text-xl text-muted-foreground">
{message || t("error.somethingWentWrong")}
</p>
</div>
</div>
);
}
+18 -16
View File
@@ -1,20 +1,22 @@
import { Button } from "@/components/ui/button"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
export default function NotFound() {
const navigate = useNavigate()
const { t } = useTranslation()
const navigate = useNavigate();
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">404</h1>
<p className="text-xl text-muted-foreground">{t("error.pageNotFound")}</p>
<Button onClick={() => navigate("/")} className="mt-2">
{t("error.backToHome")}
</Button>
</div>
</div>
)
return (
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">404</h1>
<p className="text-xl text-muted-foreground">
{t("error.pageNotFound")}
</p>
<Button onClick={() => navigate("/")} className="mt-2">
{t("error.backToHome")}
</Button>
</div>
</div>
);
}
+467 -376
View File
@@ -1,422 +1,513 @@
// src/pages/Server.tsx (最终交互优化版)
import {
ArrowDownIcon,
ArrowsUpDownIcon,
ArrowUpIcon,
ChartBarSquareIcon,
MapIcon,
ViewColumnsIcon,
} from "@heroicons/react/20/solid";
import { useQuery } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import GlobalMap from "@/components/GlobalMap";
import GroupSwitch from "@/components/GroupSwitch";
import { Loader } from "@/components/loading/Loader";
import ServerCard from "@/components/ServerCard";
import ServerCardInline from "@/components/ServerCardInline";
import ServerOverview from "@/components/ServerOverview";
import { ServiceTracker } from "@/components/ServiceTracker";
import { Loader } from "@/components/loading/Loader";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SORT_ORDERS, SORT_TYPES } from "@/context/sort-context";
import { useSort } from "@/hooks/use-sort";
import { useStatus } from "@/hooks/use-status";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { fetchServerGroup } from "@/lib/nezha-api";
import { cn, formatNezhaInfo } from "@/lib/utils";
import { NezhaWebsocketResponse, ServerGroup } from "@/types/nezha-api";
import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import type { NezhaWebsocketResponse, ServerGroup } from "@/types/nezha-api";
import { DomainStatus } from "@/components/DomainStatus";
import { getDomains } from "@/api/domain";
export default function Servers() {
const { t } = useTranslation();
const { sortType, sortOrder, setSortOrder, setSortType } = useSort();
const { data: groupData } = useQuery({
queryKey: ["server-group"],
queryFn: () => fetchServerGroup(),
});
const { lastMessage, connected } = useWebSocketContext();
const { status } = useStatus();
const [showServices, setShowServices] = useState<string>("0");
const [showMap, setShowMap] = useState<string>("0");
const [inline, setInline] = useState<string>("0");
const containerRef = useRef<HTMLDivElement>(null);
const [settingsOpen, setSettingsOpen] = useState<boolean>(false);
const [currentGroup, setCurrentGroup] = useState<string>("All");
const { t } = useTranslation();
const { sortType, sortOrder, setSortOrder, setSortType } = useSort();
const { data: groupData } = useQuery({
queryKey: ["server-group"],
queryFn: () => fetchServerGroup(),
});
const { lastMessage, connected } = useWebSocketContext();
const { status } = useStatus();
const [showServices, setShowServices] = useState<string>("0");
const [showMap, setShowMap] = useState<string>("0");
const [inline, setInline] = useState<string>("0");
const containerRef = useRef<HTMLDivElement>(null);
const [settingsOpen, setSettingsOpen] = useState<boolean>(false);
const [currentGroup, setCurrentGroup] = useState<string>("All");
const [activeView, setActiveView] = useState<'servers' | 'domains'>('servers');
const { data: domains } = useQuery({
queryKey: ['domains'],
queryFn: getDomains
});
const [activeView, setActiveView] = useState<'servers' | 'domains'>('servers');
const { data: domains } = useQuery({
queryKey: ['domains'],
queryFn: getDomains
});
// 当用户点击 "在线" 或 "离线" 或 "总服务器数" 时,status 会改变,我们就自动切回服务器视图
useEffect(() => {
// 只有在 status 改变时才触发,避免无限循环
const currentStatus = status || 'all';
if(currentStatus !== 'all' || activeView === 'domains') {
setActiveView('servers');
}
}, [status]);
// 当用户点击 "在线" 或 "离线" 或 "总服务器数" 时,status 会改变,我们就自动切回服务器视图
useEffect(() => {
// 只有在 status 改变时才触发,避免无限循环
const currentStatus = status || 'all';
if(currentStatus !== 'all' || activeView === 'domains') {
setActiveView('servers');
}
}, [status]);
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const restoreScrollPosition = () => {
const savedPosition = sessionStorage.getItem("scrollPosition");
if (savedPosition && containerRef.current) {
containerRef.current.scrollTop = Number(savedPosition);
}
};
const restoreScrollPosition = useCallback(() => {
const savedPosition = sessionStorage.getItem("scrollPosition");
if (savedPosition && containerRef.current) {
containerRef.current.scrollTop = Number(savedPosition);
}
}, []);
const handleTagChange = (newGroup: string) => {
setCurrentGroup(newGroup);
sessionStorage.setItem("selectedGroup", newGroup);
sessionStorage.setItem("scrollPosition", String(containerRef.current?.scrollTop || 0));
};
const handleTagChange = (newGroup: string) => {
setCurrentGroup(newGroup);
sessionStorage.setItem("selectedGroup", newGroup);
sessionStorage.setItem(
"scrollPosition",
String(containerRef.current?.scrollTop || 0),
);
};
useEffect(() => {
const showServicesState = localStorage.getItem("showServices");
if ((window as any).ForceShowServices) {
setShowServices("1");
} else if (showServicesState !== null) {
setShowServices(showServicesState);
}
}, []);
useEffect(() => {
const showServicesState = localStorage.getItem("showServices");
if (window.ForceShowServices) {
setShowServices("1");
} else if (showServicesState !== null) {
setShowServices(showServicesState);
}
}, []);
useEffect(() => {
const checkInlineSettings = () => {
const isMobile = window.innerWidth < 768;
useEffect(() => {
const checkInlineSettings = () => {
const isMobile = window.innerWidth < 768;
if (!isMobile) {
const inlineState = localStorage.getItem("inline");
if ((window as any).ForceCardInline) {
setInline("1");
} else if (inlineState !== null) {
setInline(inlineState);
}
}
};
if (!isMobile) {
const inlineState = localStorage.getItem("inline");
if (window.ForceCardInline) {
setInline("1");
} else if (inlineState !== null) {
setInline(inlineState);
}
}
};
checkInlineSettings();
window.addEventListener("resize", checkInlineSettings);
return () => {
window.removeEventListener("resize", checkInlineSettings);
};
}, []);
checkInlineSettings();
useEffect(() => {
const showMapState = localStorage.getItem("showMap");
if ((window as any).ForceShowMap) {
setShowMap("1");
} else if (showMapState !== null) {
setShowMap(showMapState);
}
}, []);
window.addEventListener("resize", checkInlineSettings);
useEffect(() => {
const savedGroup = sessionStorage.getItem("selectedGroup") || "All";
setCurrentGroup(savedGroup);
restoreScrollPosition();
}, []);
return () => {
window.removeEventListener("resize", checkInlineSettings);
};
}, []);
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null;
useEffect(() => {
const showMapState = localStorage.getItem("showMap");
if (window.ForceShowMap) {
setShowMap("1");
} else if (showMapState !== null) {
setShowMap(showMapState);
}
}, []);
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) || []),
];
useEffect(() => {
const savedGroup = sessionStorage.getItem("selectedGroup") || "All";
setCurrentGroup(savedGroup);
if (!connected && !lastMessage) {
return (
<div className="flex flex-col items-center min-h-96 justify-center ">
<div className="font-semibold flex items-center gap-2 text-sm">
<Loader visible={true} />
{t("info.websocketConnecting")}
</div>
</div>
);
}
restoreScrollPosition();
}, [restoreScrollPosition]);
if (!nezhaWsData) {
return (
<div className="flex flex-col items-center justify-center ">
<p className="font-semibold text-sm">{t("info.processing")} </p>
</div>
);
}
const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
let filteredServers =
nezhaWsData?.servers?.filter((server) => {
if (currentGroup === "All") return true;
const group = groupData?.data?.find(
(g: ServerGroup) => g.group.name === currentGroup && Array.isArray(g.servers) && g.servers.includes(server.id),
);
return !!group;
}) || [];
const 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) || []),
];
const totalServers = filteredServers.length || 0;
const onlineServers = filteredServers.filter((server) => formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0;
const offlineServers = filteredServers.filter((server) => !formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0;
const up =
filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_transfer ?? 0) : total),
0,
) || 0;
const down =
filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_transfer ?? 0) : total),
0,
) || 0;
if (!connected && !lastMessage) {
return (
<div className="flex flex-col items-center min-h-96 justify-center ">
<div className="font-semibold flex items-center gap-2 text-sm">
<Loader visible={true} />
{t("info.websocketConnecting")}
</div>
</div>
);
}
const upSpeed =
filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_speed ?? 0) : total),
0,
) || 0;
const downSpeed =
filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_speed ?? 0) : total),
0,
) || 0;
if (!nezhaWsData) {
return (
<div className="flex flex-col items-center justify-center ">
<p className="font-semibold text-sm">{t("info.processing")}</p>
</div>
);
}
filteredServers =
status === "all"
? filteredServers
: filteredServers.filter((server) => [status].includes(formatNezhaInfo(nezhaWsData.now, server).online ? "online" : "offline"));
let filteredServers =
nezhaWsData?.servers?.filter((server) => {
if (currentGroup === "All") return true;
const group = groupData?.data?.find(
(g: ServerGroup) =>
g.group.name === currentGroup &&
Array.isArray(g.servers) &&
g.servers.includes(server.id),
);
return !!group;
}) || [];
filteredServers = filteredServers.sort((a, b) => {
const serverAInfo = formatNezhaInfo(nezhaWsData.now, a);
const serverBInfo = formatNezhaInfo(nezhaWsData.now, b);
const totalServers = filteredServers.length || 0;
const onlineServers =
filteredServers.filter(
(server) => formatNezhaInfo(nezhaWsData.now, server).online,
)?.length || 0;
const offlineServers =
filteredServers.filter(
(server) => !formatNezhaInfo(nezhaWsData.now, server).online,
)?.length || 0;
const up =
filteredServers.reduce(
(total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_out_transfer ?? 0)
: total,
0,
) || 0;
const down =
filteredServers.reduce(
(total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_in_transfer ?? 0)
: total,
0,
) || 0;
if (sortType !== "name") {
if (!serverAInfo.online && serverBInfo.online) return 1;
if (serverAInfo.online && !serverBInfo.online) return -1;
if (!serverAInfo.online && !serverBInfo.online) {
return 0;
}
}
const upSpeed =
filteredServers.reduce(
(total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_out_speed ?? 0)
: total,
0,
) || 0;
const downSpeed =
filteredServers.reduce(
(total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_in_speed ?? 0)
: total,
0,
) || 0;
let comparison = 0;
filteredServers =
status === "all"
? filteredServers
: filteredServers.filter((server) =>
[status].includes(
formatNezhaInfo(nezhaWsData.now, server).online
? "online"
: "offline",
),
);
switch (sortType) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "uptime":
comparison = (a.state?.uptime ?? 0) - (b.state?.uptime ?? 0);
break;
case "system":
comparison = a.host.platform.localeCompare(b.host.platform);
break;
case "cpu":
comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0);
break;
case "mem":
comparison = (formatNezhaInfo(nezhaWsData.now, a).mem ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).mem ?? 0);
break;
case "disk":
comparison = (formatNezhaInfo(nezhaWsData.now, a).disk ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).disk ?? 0);
break;
case "up":
comparison = (a.state?.net_out_speed ?? 0) - (b.state?.net_out_speed ?? 0);
break;
case "down":
comparison = (a.state?.net_in_speed ?? 0) - (b.state?.net_in_speed ?? 0);
break;
case "up total":
comparison = (a.state?.net_out_transfer ?? 0) - (b.state?.net_out_transfer ?? 0);
break;
case "down total":
comparison = (a.state?.net_in_transfer ?? 0) - (b.state?.net_in_transfer ?? 0);
break;
default:
comparison = 0;
}
filteredServers = filteredServers.sort((a, b) => {
const serverAInfo = formatNezhaInfo(nezhaWsData.now, a);
const serverBInfo = formatNezhaInfo(nezhaWsData.now, b);
return sortOrder === "asc" ? comparison : -comparison;
});
if (sortType !== "name") {
// 仅在非 "name" 排序时,先按在线状态排序
if (!serverAInfo.online && serverBInfo.online) return 1;
if (serverAInfo.online && !serverBInfo.online) return -1;
if (!serverAInfo.online && !serverBInfo.online) {
// 如果两者都离线,可以继续按照其他条件排序,或者保持原序
// 这里选择保持原序
return 0;
}
}
const totalDomainsCount = domains?.length || 0;
let comparison = 0;
return (
<div className="mx-auto w-full max-w-5xl px-0">
<ServerOverview
total={totalServers}
online={onlineServers}
offline={offlineServers}
up={up}
down={down}
upSpeed={upSpeed}
downSpeed={downSpeed}
totalDomains={totalDomainsCount}
onViewChange={setActiveView}
activeView={activeView}
/>
switch (sortType) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "uptime":
comparison = (a.state?.uptime ?? 0) - (b.state?.uptime ?? 0);
break;
case "system":
comparison = a.host.platform.localeCompare(b.host.platform);
break;
case "cpu":
comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0);
break;
case "mem":
comparison =
(formatNezhaInfo(nezhaWsData.now, a).mem ?? 0) -
(formatNezhaInfo(nezhaWsData.now, b).mem ?? 0);
break;
case "disk":
comparison =
(formatNezhaInfo(nezhaWsData.now, a).disk ?? 0) -
(formatNezhaInfo(nezhaWsData.now, b).disk ?? 0);
break;
case "up":
comparison =
(a.state?.net_out_speed ?? 0) - (b.state?.net_out_speed ?? 0);
break;
case "down":
comparison =
(a.state?.net_in_speed ?? 0) - (b.state?.net_in_speed ?? 0);
break;
case "up total":
comparison =
(a.state?.net_out_transfer ?? 0) - (b.state?.net_out_transfer ?? 0);
break;
case "down total":
comparison =
(a.state?.net_in_transfer ?? 0) - (b.state?.net_in_transfer ?? 0);
break;
default:
comparison = 0;
}
{/* ======================================================= */}
{/* vvvvvvvvvvv 这是最终的条件渲染逻辑 vvvvvvvvvvv */}
{/* 只有在 'domains' 视图下,才隐藏服务器列表 */}
{activeView !== 'domains' && (
<>
<div className="flex mt-6 items-center justify-between gap-2 server-overview-controls">
<section className="flex items-center gap-2 w-full overflow-hidden">
<button
onClick={() => {
setShowMap(showMap === "0" ? "1" : "0");
localStorage.setItem("showMap", showMap === "0" ? "1" : "0");
}}
className={cn(
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
{
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": showMap === "1",
"text-white": showMap === "1",
},
{
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
},
)}
>
<MapIcon
className={cn("size-[13px]", {
"text-white": showMap === "1",
})}
/>
</button>
<button
onClick={() => {
setShowServices(showServices === "0" ? "1" : "0");
localStorage.setItem("showServices", showServices === "0" ? "1" : "0");
}}
className={cn(
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
{
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": showServices === "1",
"text-white": showServices === "1",
},
{
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
},
)}
>
<ChartBarSquareIcon
className={cn("size-[13px]", {
"text-white": showServices === "1",
})}
/>
</button>
<button
onClick={() => {
setInline(inline === "0" ? "1" : "0");
localStorage.setItem("inline", inline === "0" ? "1" : "0");
}}
className={cn(
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
{
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": inline === "1",
"text-white": inline === "1",
},
{
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
},
)}
>
<ViewColumnsIcon
className={cn("size-[13px]", {
"text-white": inline === "1",
})}
/>
</button>
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={handleTagChange} />
</section>
<Popover onOpenChange={setSettingsOpen}>
<PopoverTrigger asChild>
<button
className={cn(
"rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] dark:bg-stone-800 bg-white p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
{
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] dark:bg-stone-700 bg-stone-200": settingsOpen,
},
{
"dark:bg-stone-800/70 bg-stone-100/70 ": customBackgroundImage,
},
)}
>
<p className="text-[10px] font-bold whitespace-nowrap">{sortType === "default" ? "Sort" : sortType.toUpperCase()}</p>
{sortOrder === "asc" && sortType !== "default" ? (
<ArrowUpIcon className="size-[13px]" />
) : sortOrder === "desc" && sortType !== "default" ? (
<ArrowDownIcon className="size-[13px]" />
) : (
<ArrowsUpDownIcon className="size-[13px]" />
)}
</button>
</PopoverTrigger>
<PopoverContent className="p-4 w-[240px] rounded-lg">
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">Sort by</Label>
<Select value={sortType} onValueChange={setSortType}>
<SelectTrigger className="w-full text-xs h-8">
<SelectValue placeholder="Choose type" />
</SelectTrigger>
<SelectContent>
{SORT_TYPES.map((type) => (
<SelectItem key={type} value={type} className="text-xs">
{type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">Sort order</Label>
<Select value={sortOrder} onValueChange={setSortOrder} disabled={sortType === "default"}>
<SelectTrigger className="w-full text-xs h-8">
<SelectValue placeholder="Choose order" />
</SelectTrigger>
<SelectContent>
{SORT_ORDERS.map((order) => (
<SelectItem key={order} value={order} className="text-xs">
{order.charAt(0).toUpperCase() + order.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</PopoverContent>
</Popover>
</div>
{showMap === "1" && <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />}
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
{inline === "1" && (
<section ref={containerRef} className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6 server-inline-list">
{filteredServers.map((serverInfo) => (
<ServerCardInline now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
))}
</section>
)}
{inline === "0" && (
<section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list">
{filteredServers.map((serverInfo) => (
<ServerCard now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
))}
</section>
)}
</>
)}
return sortOrder === "asc" ? comparison : -comparison;
});
{/* 只有在 status 为 'all' (即点击总服务器数时) 或者 activeView 为 'domains' 时,才显示域名列表 */}
{(status === 'all' || activeView === 'domains') && (
<div className="mt-6">
<DomainStatus />
</div>
)}
const totalDomainsCount = domains?.length || 0;
{/* ^^^^^^^^^^^ 这是最终的条件渲染逻辑 ^^^^^^^^^^^ */}
{/* ======================================================= */}
</div>
);
}
return (
<div className="mx-auto w-full max-w-5xl px-0">
<ServerOverview
total={totalServers}
online={onlineServers}
offline={offlineServers}
up={up}
down={down}
upSpeed={upSpeed}
downSpeed={downSpeed}
totalDomains={totalDomainsCount}
onViewChange={setActiveView}
activeView={activeView}
/>
{activeView !== 'domains' && (
<>
<div className="flex mt-6 items-center justify-between gap-2 server-overview-controls">
<section className="flex items-center gap-2 w-full overflow-hidden">
<button
onClick={() => {
setShowMap(showMap === "0" ? "1" : "0");
localStorage.setItem("showMap", showMap === "0" ? "1" : "0");
}}
className={cn(
"inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100",
{
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
showMap === "1",
},
{
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
},
)}
>
<MapIcon className={cn("size-[13px]")} />
</button>
<button
onClick={() => {
setShowServices(showServices === "0" ? "1" : "0");
localStorage.setItem(
"showServices",
showServices === "0" ? "1" : "0",
);
}}
className={cn(
"inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100",
{
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
showServices === "1",
},
{
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
},
)}
>
<ChartBarSquareIcon className={cn("size-[13px]")} />
</button>
<button
onClick={() => {
setInline(inline === "0" ? "1" : "0");
localStorage.setItem("inline", inline === "0" ? "1" : "0");
}}
className={cn(
"inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100",
{
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
inline === "1",
},
{
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
},
)}
>
<ViewColumnsIcon
className={cn("size-[13px]", {
"text-white": inline === "1",
})}
/>
</button>
<GroupSwitch
tabs={groupTabs}
currentTab={currentGroup}
setCurrentTab={handleTagChange}
/>
</section>
<Popover onOpenChange={setSettingsOpen}>
<PopoverTrigger asChild>
<button
className={cn(
"rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:0_1px_0_rgb(0_0_0/20%)] dark:bg-stone-800 bg-white p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
{
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] dark:bg-stone-700 bg-stone-200":
settingsOpen,
},
{
"dark:bg-stone-800/70 bg-stone-100/70 ":
customBackgroundImage,
},
)}
>
<p className="text-[10px] font-bold whitespace-nowrap">
{sortType === "default" ? "Sort" : sortType.toUpperCase()}
</p>
{sortOrder === "asc" && sortType !== "default" ? (
<ArrowUpIcon className="size-[13px]" />
) : sortOrder === "desc" && sortType !== "default" ? (
<ArrowDownIcon className="size-[13px]" />
) : (
<ArrowsUpDownIcon className="size-[13px]" />
)}
</button>
</PopoverTrigger>
<PopoverContent className="p-4 w-[240px] rounded-lg">
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">
Sort by
</Label>
<Select value={sortType} onValueChange={setSortType}>
<SelectTrigger className="w-full text-xs h-8">
<SelectValue placeholder="Choose type" />
</SelectTrigger>
<SelectContent>
{SORT_TYPES.map((type) => (
<SelectItem key={type} value={type} className="text-xs">
{type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">
Sort order
</Label>
<Select
value={sortOrder}
onValueChange={setSortOrder}
disabled={sortType === "default"}
>
<SelectTrigger className="w-full text-xs h-8">
<SelectValue placeholder="Choose order" />
</SelectTrigger>
<SelectContent>
{SORT_ORDERS.map((order) => (
<SelectItem key={order} value={order} className="text-xs">
{order.charAt(0).toUpperCase() + order.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</PopoverContent>
</Popover>
</div>
{showMap === "1" && (
<GlobalMap
now={nezhaWsData.now}
serverList={nezhaWsData?.servers || []}
/>
)}
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
{inline === "1" && (
<section
ref={containerRef}
className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6 server-inline-list"
>
{filteredServers.map((serverInfo) => (
<ServerCardInline
now={nezhaWsData.now}
key={serverInfo.id}
serverInfo={serverInfo}
/>
))}
</section>
)}
{inline === "0" && (
<section
ref={containerRef}
className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list"
>
{filteredServers.map((serverInfo) => (
<ServerCard
now={nezhaWsData.now}
key={serverInfo.id}
serverInfo={serverInfo}
/>
))}
</section>
)}
</>
)}
{/* 只有在 status 为 'all' (即点击总服务器数时) 或者 activeView 为 'domains' 时,才显示域名列表 */}
{(status === 'all' || activeView === 'domains') && (
<div className="mt-6">
<DomainStatus />
</div>
)}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More