feat: detail overview i18n

This commit is contained in:
hamster1963 2024-11-24 21:44:12 +08:00
parent 8812cd1d3f
commit ee03928a56
9 changed files with 157 additions and 36 deletions

View File

@ -1,7 +1,9 @@
// src/components/Footer.tsx // src/components/Footer.tsx
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
const Footer: React.FC = () => { const Footer: React.FC = () => {
const { t } = useTranslation();
return ( return (
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4"> <footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4">
<section className="flex flex-col"> <section className="flex flex-col">
@ -13,7 +15,7 @@ const Footer: React.FC = () => {
</a> </a>
</p> </p>
<p> <p>
Theme by{" "} {t("footer.themeBy")}
<a <a
href={"https://github.com/hamster1963/nezha-dash-react"} href={"https://github.com/hamster1963/nezha-dash-react"}
target="_blank" target="_blank"

View File

@ -5,10 +5,11 @@ import { fetchLoginUser } from "@/lib/nezha-api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { LanguageSwitcher } from "./LanguageSwitcher"; import { LanguageSwitcher } from "./LanguageSwitcher";
import { useTranslation } from "react-i18next";
function Header() { function Header() {
const { t } = useTranslation();
return ( return (
<div className="mx-auto w-full max-w-5xl"> <div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between"> <section className="flex items-center justify-between">
@ -28,7 +29,7 @@ function Header() {
className="mx-2 hidden h-4 w-[1px] md:block" className="mx-2 hidden h-4 w-[1px] md:block"
/> />
<p className="hidden text-sm font-medium opacity-40 md:block"> <p className="hidden text-sm font-medium opacity-40 md:block">
{t("nezha")}
</p> </p>
</section> </section>
<section className="flex items-center gap-2"> <section className="flex items-center gap-2">
@ -98,7 +99,7 @@ function Overview() {
<section className={"mt-10 flex flex-col md:mt-16"}> <section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">👋 {t("overview")}</p> <p className="text-base font-semibold">👋 {t("overview")}</p>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<p className="text-sm font-medium opacity-50">where the time is</p> <p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
{mouted ? ( {mouted ? (
<p className="text-sm font-medium">{timeString}</p> <p className="text-sm font-medium">{timeString}</p>
) : ( ) : (

View File

@ -5,8 +5,10 @@ import { cn, formatNezhaInfo } from "@/lib/utils";
import { NezhaAPI } from "@/types/nezha-api"; import { NezhaAPI } from "@/types/nezha-api";
import { Card } from "./ui/card"; import { Card } from "./ui/card";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
export default function ServerCard({ serverInfo }: { serverInfo: NezhaAPI }) { export default function ServerCard({ serverInfo }: { serverInfo: NezhaAPI }) {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { name, country_code, online, cpu, up, down, mem, stg } = const { name, country_code, online, cpu, up, down, mem, stg } =
formatNezhaInfo(serverInfo); formatNezhaInfo(serverInfo);
@ -55,21 +57,27 @@ export default function ServerCard({ serverInfo }: { serverInfo: NezhaAPI }) {
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"MEM"}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}% {mem.toFixed(2)}%
</div> </div>
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"STG"}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}% {stg.toFixed(2)}%
</div> </div>
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"Upload"}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{up >= 1024 {up >= 1024
? `${(up / 1024).toFixed(2)}G/s` ? `${(up / 1024).toFixed(2)}G/s`
@ -77,7 +85,9 @@ export default function ServerCard({ serverInfo }: { serverInfo: NezhaAPI }) {
</div> </div>
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"Download"}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{down >= 1024 {down >= 1024
? `${(down / 1024).toFixed(2)}G/s` ? `${(down / 1024).toFixed(2)}G/s`

View File

@ -7,18 +7,18 @@ import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils"; import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils";
import { NezhaAPIResponse } from "@/types/nezha-api"; import { NezhaAPIResponse } from "@/types/nezha-api";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
export default function ServerDetailOverview() { export default function ServerDetailOverview() {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const { lastMessage, readyState } = useWebSocketContext(); const { lastMessage, readyState } = useWebSocketContext();
// 检查连接状态
if (readyState !== 1) { if (readyState !== 1) {
return <ServerDetailLoading />; return <ServerDetailLoading />;
} }
// 解析消息
const nezhaWsData = lastMessage const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaAPIResponse) ? (JSON.parse(lastMessage.data) as NezhaAPIResponse)
: null; : null;
@ -48,7 +48,9 @@ export default function ServerDetailOverview() {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Status"}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.status")}
</p>
<Badge <Badge
className={cn( className={cn(
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", "text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
@ -58,7 +60,7 @@ export default function ServerDetailOverview() {
}, },
)} )}
> >
{online ? "Online" : "Offline"} {online ? t("serverDetail.online") : t("serverDetail.offline")}
</Badge> </Badge>
</section> </section>
</CardContent> </CardContent>
@ -66,7 +68,9 @@ export default function ServerDetailOverview() {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Uptime"}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.uptime")}
</p>
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
{online ? (uptime / 86400).toFixed(0) : "N/A"} {"Days"}{" "} {online ? (uptime / 86400).toFixed(0) : "N/A"} {"Days"}{" "}
@ -77,23 +81,33 @@ export default function ServerDetailOverview() {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Version"}</p> <p className="text-xs text-muted-foreground">
<div className="text-xs">{version || "Unknown"} </div> {t("serverDetail.version")}
</p>
<div className="text-xs">
{version || t("serverDetail.unknown")}{" "}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Arch"}</p> <p className="text-xs text-muted-foreground">
<div className="text-xs">{server.host.arch || "Unknown"} </div> {t("serverDetail.arch")}
</p>
<div className="text-xs">
{server.host.arch || t("serverDetail.unknown")}{" "}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Mem"}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.mem")}
</p>
<div className="text-xs"> <div className="text-xs">
{formatBytes(server.host.mem_total)} {formatBytes(server.host.mem_total)}
</div> </div>
@ -103,7 +117,9 @@ export default function ServerDetailOverview() {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Disk"}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.disk")}
</p>
<div className="text-xs"> <div className="text-xs">
{formatBytes(server.host.disk_total)} {formatBytes(server.host.disk_total)}
</div> </div>
@ -113,10 +129,13 @@ export default function ServerDetailOverview() {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Region"}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.region")}
</p>
<section className="flex items-start gap-1"> <section className="flex items-start gap-1">
<div className="text-xs text-start"> <div className="text-xs text-start">
{server.host.country_code?.toUpperCase() || "Unknown"} {server.host.country_code?.toUpperCase() ||
t("serverDetail.unknown")}
</div> </div>
{server.host.country_code && ( {server.host.country_code && (
<ServerFlag <ServerFlag
@ -133,15 +152,17 @@ export default function ServerDetailOverview() {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"System"}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.system")}
</p>
{server.host.platform ? ( {server.host.platform ? (
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
{server.host.platform || "Unknown"} -{" "} {server.host.platform || t("serverDetail.unknown")} -{" "}
{server.host.platform_version}{" "} {server.host.platform_version}{" "}
</div> </div>
) : ( ) : (
<div className="text-xs">Unknown</div> <div className="text-xs"> {t("serverDetail.unknown")}</div>
)} )}
</section> </section>
</CardContent> </CardContent>
@ -153,7 +174,7 @@ export default function ServerDetailOverview() {
{server.host.cpu ? ( {server.host.cpu ? (
<div className="text-xs"> {server.host.cpu}</div> <div className="text-xs"> {server.host.cpu}</div>
) : ( ) : (
<div className="text-xs">Unknown</div> <div className="text-xs"> {t("serverDetail.unknown")}</div>
)} )}
</section> </section>
</CardContent> </CardContent>

View File

@ -1,5 +1,6 @@
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { cn, formatBytes } from "@/lib/utils"; import { cn, formatBytes } from "@/lib/utils";
import { useTranslation } from "react-i18next";
type ServerOverviewProps = { type ServerOverviewProps = {
online: number; online: number;
@ -16,6 +17,8 @@ export default function ServerOverview({
up, up,
down, down,
}: ServerOverviewProps) { }: ServerOverviewProps) {
const { t } = useTranslation();
return ( return (
<> <>
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4"> <section className="grid grid-cols-2 gap-4 lg:grid-cols-4">
@ -23,7 +26,7 @@ export default function ServerOverview({
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">
{"Total servers"} {t("serverOverview.totalServers")}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
@ -42,7 +45,7 @@ export default function ServerOverview({
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">
{"Online servers"} {t("serverOverview.onlineServers")}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
@ -63,7 +66,7 @@ export default function ServerOverview({
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">
{"Offline servers"} {t("serverOverview.offlineServers")}
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
@ -83,7 +86,7 @@ export default function ServerOverview({
<CardContent className="relative px-6 py-3"> <CardContent className="relative px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">
{"Total bandwidth"} {t("serverOverview.totalBandwidth")}
</p> </p>
<section className="flex flex-col sm:flex-row pt-[8px] sm:items-center items-start gap-1"> <section className="flex flex-col sm:flex-row pt-[8px] sm:items-center items-start gap-1">

View File

@ -1,5 +1,35 @@
{ {
"nezha": "Nezha Monitoring",
"overview": "Overview", "overview": "Overview",
"whereTheTimeIs": "Where the time is",
"serverOverview": {
"totalServers": "Total Servers",
"onlineServers": "Online Servers",
"offlineServers": "Offline Servers",
"totalBandwidth": "Total Bandwidth"
},
"serverCard": {
"mem": "MEM",
"stg": "STG",
"upload": "Upload",
"download": "Download"
},
"serverDetail": {
"status": "Status",
"online": "Online",
"offline": "Offline",
"unknown": "Unknown",
"uptime": "Uptime",
"version": "Version",
"arch": "Arch",
"mem": "Mem",
"disk": "Disk",
"region": "Region",
"system": "System"
},
"footer": {
"themeBy": "Theme by "
},
"language": { "language": {
"zh-CN": "简体中文", "zh-CN": "简体中文",
"zh-TW": "繁體中文", "zh-TW": "繁體中文",

View File

@ -1,5 +1,35 @@
{ {
"nezha": "哪吒监控",
"overview": "概览", "overview": "概览",
"whereTheTimeIs": "当前时间",
"serverOverview": {
"totalServers": "服务器总数",
"onlineServers": "在线服务器",
"offlineServers": "离线服务器",
"totalBandwidth": "总流量"
},
"serverCard": {
"mem": "内存",
"stg": "存储",
"upload": "上传",
"download": "下载"
},
"serverDetail": {
"status": "状态",
"online": "在线",
"offline": "离线",
"unknown": "未知",
"uptime": "运行时间",
"version": "版本",
"arch": "架构",
"mem": "内存",
"disk": "磁盘",
"region": "区域",
"system": "系统"
},
"footer": {
"themeBy": "主题-"
},
"language": { "language": {
"zh-CN": "简体中文", "zh-CN": "简体中文",
"zh-TW": "繁體中文", "zh-TW": "繁體中文",

View File

@ -1,5 +1,35 @@
{ {
"nezha": "哪吒監控",
"overview": "概覽", "overview": "概覽",
"whereTheTimeIs": "目前時間",
"serverOverview": {
"totalServers": "總服務器",
"onlineServers": "線上服務器",
"offlineServers": "離線服務器",
"totalBandwidth": "總帶寬"
},
"serverCard": {
"mem": "內存",
"stg": "存儲",
"upload": "上傳",
"download": "下載"
},
"serverDetail": {
"status": "狀態",
"online": "線上",
"offline": "離線",
"unknown": "未知",
"uptime": "運行時間",
"version": "版本",
"arch": "架構",
"mem": "內存",
"disk": "磁盤",
"region": "地區",
"system": "系統"
},
"footer": {
"themeBy": "主題-"
},
"language": { "language": {
"zh-CN": "简体中文", "zh-CN": "简体中文",
"zh-TW": "繁體中文", "zh-TW": "繁體中文",
@ -8,6 +38,6 @@
"theme": { "theme": {
"light": "亮色", "light": "亮色",
"dark": "暗色", "dark": "暗色",
"system": "跟系統" "system": "跟系統"
} }
} }

View File

@ -17,10 +17,8 @@ export default function Servers() {
}); });
const { lastMessage, readyState } = useWebSocketContext(); const { lastMessage, readyState } = useWebSocketContext();
// 添加分组状态
const [currentGroup, setCurrentGroup] = useState<string>("All"); const [currentGroup, setCurrentGroup] = useState<string>("All");
// 获取所有分组名称
const groupTabs = [ const groupTabs = [
"All", "All",
...(groupData?.data?.map((item: ServerGroup) => item.group.name) || []), ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || []),
@ -32,7 +30,6 @@ export default function Servers() {
} }
}, [readyState]); }, [readyState]);
// 检查连接状态
if (readyState !== 1) { if (readyState !== 1) {
return ( return (
<div className="flex flex-col items-center justify-center "> <div className="flex flex-col items-center justify-center ">
@ -41,7 +38,6 @@ export default function Servers() {
); );
} }
// 解析消息
const nezhaWsData = lastMessage const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaAPIResponse) ? (JSON.parse(lastMessage.data) as NezhaAPIResponse)
: null; : null;
@ -54,7 +50,6 @@ export default function Servers() {
); );
} }
// 计算所有服务器的统计数据(用于 Overview
const totalServers = nezhaWsData?.servers?.length || 0; const totalServers = nezhaWsData?.servers?.length || 0;
const onlineServers = const onlineServers =
nezhaWsData?.servers?.filter((server) => formatNezhaInfo(server).online) nezhaWsData?.servers?.filter((server) => formatNezhaInfo(server).online)
@ -73,7 +68,6 @@ export default function Servers() {
0, 0,
) || 0; ) || 0;
// 根据当前选中的分组筛选服务器(用于显示列表)
const filteredServers = const filteredServers =
nezhaWsData?.servers?.filter((server) => { nezhaWsData?.servers?.filter((server) => {
if (currentGroup === "All") return true; if (currentGroup === "All") return true;