mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-02-05 13:10:09 +00:00
feat: server page
This commit is contained in:
@@ -7,9 +7,12 @@ const Footer: React.FC = () => {
|
||||
<section className="flex flex-col">
|
||||
<section className="mt-1 flex items-center gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||
©2020-{new Date().getFullYear()}{" "}
|
||||
<a href={"https://nezha.wiki"} target="_blank">
|
||||
<a href={"https://github.com/naiba/nezha"} target="_blank">
|
||||
Nezha
|
||||
</a>
|
||||
<a href={"https://github.com/hamster1963/nezha-dash-react"} target="_blank">
|
||||
Nezha-Dash
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
</footer>
|
||||
|
||||
@@ -21,13 +21,13 @@ function Header() {
|
||||
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
|
||||
/>
|
||||
</div>
|
||||
{"NezhaDash"}
|
||||
{"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">
|
||||
哪吒监控面板
|
||||
哪吒监控
|
||||
</p>
|
||||
</section>
|
||||
<section className="flex items-center gap-2">
|
||||
|
||||
128
src/components/ServerCard.tsx
Normal file
128
src/components/ServerCard.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import ServerFlag from "@/components/ServerFlag";
|
||||
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||
import { NezhaAPI } from "@/types/nezha-api";
|
||||
import { Card } from "./ui/card";
|
||||
|
||||
export default function ServerCard({
|
||||
serverInfo,
|
||||
}: {
|
||||
serverInfo: NezhaAPI;
|
||||
}) {
|
||||
|
||||
const { name, country_code, online, cpu, up, down, mem, stg } =
|
||||
formatNezhaInfo(serverInfo);
|
||||
|
||||
const showFlag = true
|
||||
|
||||
|
||||
return online ? (
|
||||
<section >
|
||||
<Card
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row",
|
||||
)}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2 lg:w-40")}
|
||||
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">
|
||||
<p
|
||||
className={cn(
|
||||
"break-all font-bold tracking-tight",
|
||||
showFlag ? "text-xs " : "text-sm",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex flex-col gap-2">
|
||||
<section
|
||||
className={cn("grid grid-cols-5 items-center gap-3")}
|
||||
>
|
||||
<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">{"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">{"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">{"Upload"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up >= 1024
|
||||
? `${(up / 1024).toFixed(2)}G/s`
|
||||
: `${up.toFixed(2)}M/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{"Download"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down >= 1024
|
||||
? `${(down / 1024).toFixed(2)}G/s`
|
||||
: `${down.toFixed(2)}M/s`}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
) : (
|
||||
<Card
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row",
|
||||
)}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2 lg: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">
|
||||
<p
|
||||
className={cn(
|
||||
"break-all font-bold tracking-tight",
|
||||
showFlag ? "text-xs" : "text-sm",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
48
src/components/ServerFlag.tsx
Normal file
48
src/components/ServerFlag.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ServerFlag({
|
||||
country_code,
|
||||
className,
|
||||
}: {
|
||||
country_code: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
};
|
||||
|
||||
checkEmojiSupport();
|
||||
}, []);
|
||||
|
||||
if (!country_code) return null;
|
||||
|
||||
if (supportsEmojiFlags && country_code.toLowerCase() === "tw") {
|
||||
country_code = "cn";
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
||||
{ !supportsEmojiFlags ? (
|
||||
<span className={`fi fi-${country_code}`} />
|
||||
) : (
|
||||
getUnicodeFlagIcon(country_code)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
110
src/components/ServerOverview.tsx
Normal file
110
src/components/ServerOverview.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { cn, formatBytes } from "@/lib/utils";
|
||||
|
||||
type ServerOverviewProps = {
|
||||
online: number;
|
||||
offline: number;
|
||||
total: number;
|
||||
up: number;
|
||||
down: number;
|
||||
}
|
||||
|
||||
|
||||
export default function ServerOverview({ online, offline, total, up, down }: ServerOverviewProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Card
|
||||
className={cn("hover:border-blue-500 transition-all")}
|
||||
>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{"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
|
||||
className={cn(
|
||||
" hover:ring-green-500 ring-1 ring-transparent transition-all",
|
||||
)}
|
||||
>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{"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
|
||||
className={cn(
|
||||
" hover:ring-red-500 ring-1 ring-transparent transition-all",
|
||||
|
||||
)}
|
||||
>
|
||||
<CardContent className="px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{"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
|
||||
className={cn(
|
||||
" hover:ring-purple-500 ring-1 ring-transparent transition-all",
|
||||
|
||||
)}
|
||||
>
|
||||
<CardContent className="relative px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{"Totalbandwidth"}
|
||||
</p>
|
||||
|
||||
<section className="flex flex-col sm:flex-row pt-[8px] sm:items-center items-start gap-1">
|
||||
<p className="text-[12px] text-nowrap font-semibold">
|
||||
↑{formatBytes(up)}
|
||||
</p>
|
||||
<p className="text-[12px] text-nowrap font-semibold">
|
||||
↓{formatBytes(down)}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
23
src/components/ServerUsageBar.tsx
Normal file
23
src/components/ServerUsageBar.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
type ServerUsageBarProps = {
|
||||
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"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
85
src/components/ui/card.tsx
Normal file
85
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from "react";
|
||||
|
||||
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 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 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";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
28
src/components/ui/progress.tsx
Normal file
28
src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
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
|
||||
|
||||
export { Progress }
|
||||
Reference in New Issue
Block a user