feat: server page

This commit is contained in:
hamster1963
2024-11-23 19:28:55 +08:00
parent d1558b71c4
commit 963b6a54a6
21 changed files with 784 additions and 96 deletions

View File

@@ -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>

View File

@@ -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">

View 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>
);
}

View 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>
);
}

View 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>
</>
);
}

View 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"}
/>
);
}

View 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,
};

View 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 }