From 910f74b96d2a1e2051d7c2b21e6476d27e5f250c Mon Sep 17 00:00:00 2001 From: Montia37 Date: Fri, 15 Aug 2025 23:26:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E9=99=90=E5=88=B6=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=8D=A1=E7=89=87=E5=92=8C=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- komari-theme.json | 2 +- src/components/sections/NodeCard.tsx | 58 +++++++++++--- src/components/sections/NodeListHeader.tsx | 6 +- src/components/sections/NodeListItem.tsx | 53 +++++++++--- src/components/ui/circle-progress.tsx | 66 +++++++++++++++ src/hooks/useNodeCommons.ts | 28 +++++++ src/pages/instance/Instance.tsx | 93 ++++++++++++++-------- src/pages/instance/index.tsx | 74 ++++++++--------- src/utils/formatHelper.ts | 20 +++++ 10 files changed, 303 insertions(+), 99 deletions(-) create mode 100644 src/components/ui/circle-progress.tsx diff --git a/README.md b/README.md index fa2f9f9..d5d6a87 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ### 配置背景图片 -> 为获得最佳视觉效果,建议搭配背景图片使用。 +> 为获得最佳视觉效果,建议搭配背景图片使用 #### Komari v1.0.5 及以上版本 diff --git a/komari-theme.json b/komari-theme.json index 43e0d66..d1ba943 100644 --- a/komari-theme.json +++ b/komari-theme.json @@ -2,7 +2,7 @@ "name": "Komari Theme PurCart", "short": "PurCarte", "description": "A frosted glass theme for Komari", - "version": "1.0.2", + "version": "1.0.3", "author": "Montia & Gemini", "url": "https://github.com/Montia37/Komari-theme-purcarte", "preview": "preview.png", diff --git a/src/components/sections/NodeCard.tsx b/src/components/sections/NodeCard.tsx index daaf2b2..11851d4 100644 --- a/src/components/sections/NodeCard.tsx +++ b/src/components/sections/NodeCard.tsx @@ -1,5 +1,10 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { formatBytes, formatUptime, getOSImage } from "@/utils"; +import { + formatBytes, + formatUptime, + getOSImage, + formatTrafficLimit, +} from "@/utils"; import type { NodeWithStatus } from "@/types/node"; import { Link } from "react-router-dom"; import { CpuIcon, MemoryStickIcon, HardDriveIcon } from "lucide-react"; @@ -7,6 +12,7 @@ import Flag from "./Flag"; import { Tag } from "../ui/tag"; import { useNodeCommons } from "@/hooks/useNodeCommons"; import { ProgressBar } from "../ui/progress-bar"; +import { CircleProgress } from "../ui/circle-progress"; interface NodeCardProps { node: NodeWithStatus; @@ -23,6 +29,7 @@ export const NodeCard = ({ node }: NodeCardProps) => { diskUsage, load, expired_at, + trafficPercentage, } = useNodeCommons(node); const getProgressBarClass = (percentage: number) => { @@ -129,7 +136,7 @@ export const NodeCard = ({ node }: NodeCardProps) => {
- 网络 + 网络:
↑ {stats ? formatBytes(stats.network.up, true) : "N/A"} @@ -137,13 +144,38 @@ export const NodeCard = ({ node }: NodeCardProps) => {
-
- 流量 -
- ↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"} - - ↓ {stats ? formatBytes(stats.network.totalDown) : "N/A"} - +
+ 流量 +
+
+ {node.traffic_limit !== 0 && isOnline && stats && ( + + )} +
+
+
+ + ↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"} + + + ↓ {stats ? formatBytes(stats.network.totalDown) : "N/A"} + +
+ {node.traffic_limit !== 0 && isOnline && stats && ( +
+ {formatTrafficLimit( + node.traffic_limit, + node.traffic_limit_type + )} +
+ )} +
@@ -151,13 +183,13 @@ export const NodeCard = ({ node }: NodeCardProps) => { {load}
-
- 到期 +
+ 到期:
{expired_at}
-
- 在线 +
+ 在线: {isOnline && stats ? formatUptime(stats.uptime) : "离线"} diff --git a/src/components/sections/NodeListHeader.tsx b/src/components/sections/NodeListHeader.tsx index 79f947a..340aa7f 100644 --- a/src/components/sections/NodeListHeader.tsx +++ b/src/components/sections/NodeListHeader.tsx @@ -1,12 +1,12 @@ export const NodeListHeader = () => { return ( -
-
节点名称
+
+
节点名称
CPU
内存
SWAP
硬盘
-
网络
+
网络
流量
负载
diff --git a/src/components/sections/NodeListItem.tsx b/src/components/sections/NodeListItem.tsx index f0fbac3..dacdf11 100644 --- a/src/components/sections/NodeListItem.tsx +++ b/src/components/sections/NodeListItem.tsx @@ -1,10 +1,11 @@ -import { formatBytes, formatUptime } from "@/utils"; +import { formatBytes, formatTrafficLimit, formatUptime } from "@/utils"; import type { NodeWithStatus } from "@/types/node"; import { Link } from "react-router-dom"; import { CpuIcon, MemoryStickIcon, HardDriveIcon } from "lucide-react"; import Flag from "./Flag"; import { Tag } from "../ui/tag"; import { useNodeCommons } from "@/hooks/useNodeCommons"; +import { CircleProgress } from "../ui/circle-progress"; interface NodeListItemProps { node: NodeWithStatus; @@ -21,22 +22,23 @@ export const NodeListItem = ({ node }: NodeListItemProps) => { diskUsage, load, expired_at, + trafficPercentage, } = useNodeCommons(node); return (
-
+
{node.name}
-
+
到期:
{expired_at}
@@ -86,17 +88,42 @@ export const NodeListItem = ({ node }: NodeListItemProps) => { {isOnline ? `${diskUsage.toFixed(1)}%` : "N/A"}
-
- - ↑ {stats ? formatBytes(stats.network.up, true) : "N/A"}↓{" "} - {stats ? formatBytes(stats.network.down, true) : "N/A"} - +
+
↑ {stats ? formatBytes(stats.network.up, true) : "N/A"}
+
↓ {stats ? formatBytes(stats.network.down, true) : "N/A"}
- - ↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"}↓{" "} - {stats ? formatBytes(stats.network.totalDown) : "N/A"} - +
+ {node.traffic_limit !== 0 && isOnline && stats && ( +
+ +
+ )} +
+
+ + ↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"} + + + ↓ {stats ? formatBytes(stats.network.totalDown) : "N/A"} + +
+ {node.traffic_limit !== 0 && isOnline && stats && ( +
+ {formatTrafficLimit( + node.traffic_limit, + node.traffic_limit_type + )} +
+ )} +
+
{load} diff --git a/src/components/ui/circle-progress.tsx b/src/components/ui/circle-progress.tsx new file mode 100644 index 0000000..9ad097b --- /dev/null +++ b/src/components/ui/circle-progress.tsx @@ -0,0 +1,66 @@ +import React from "react"; + +interface CircleProgressProps { + value: number; + maxValue?: number; + size?: number; + strokeWidth?: number; + className?: string; + showPercentage?: boolean; + color?: string; +} + +export const CircleProgress: React.FC = ({ + value, + maxValue = 100, + size = 40, + strokeWidth = 4, + className = "", + showPercentage = true, + color = "", +}) => { + const percentage = Math.min(100, Math.max(0, (value / maxValue) * 100)); + const radius = size / 2 - strokeWidth / 2; + const circumference = radius * 2 * Math.PI; + const strokeDashoffset = circumference - (percentage / 100) * circumference; + + const getColor = () => { + if (color) return color; + if (percentage > 90) return "stroke-red-600"; + if (percentage > 50) return "stroke-yellow-400"; + return "stroke-green-500"; + }; + + return ( +
+ + {/* Background circle */} + + {/* Progress circle */} + + + {showPercentage && ( +
+ {percentage.toFixed(0)}% +
+ )} +
+ ); +}; diff --git a/src/hooks/useNodeCommons.ts b/src/hooks/useNodeCommons.ts index cc1a4d1..cb25c15 100644 --- a/src/hooks/useNodeCommons.ts +++ b/src/hooks/useNodeCommons.ts @@ -68,6 +68,33 @@ export const useNodeCommons = (node: NodeWithStatus) => { : []), ]; + // 计算流量使用百分比 + const trafficPercentage = useMemo(() => { + if (!node.traffic_limit || !stats || !isOnline) return 0; + + // 根据流量限制类型确定使用的流量值 + let usedTraffic = 0; + switch (node.traffic_limit_type) { + case "up": + usedTraffic = stats.network.totalUp; + break; + case "down": + usedTraffic = stats.network.totalDown; + break; + case "sum": + usedTraffic = stats.network.totalUp + stats.network.totalDown; + break; + case "min": + usedTraffic = Math.min(stats.network.totalUp, stats.network.totalDown); + break; + default: // max 或者未设置 + usedTraffic = Math.max(stats.network.totalUp, stats.network.totalDown); + break; + } + + return (usedTraffic / node.traffic_limit) * 100; + }, [node.traffic_limit, node.traffic_limit_type, stats, isOnline]); + return { stats, isOnline, @@ -78,5 +105,6 @@ export const useNodeCommons = (node: NodeWithStatus) => { diskUsage, load, expired_at, + trafficPercentage, }; }; diff --git a/src/pages/instance/Instance.tsx b/src/pages/instance/Instance.tsx index 4849ebf..b489c13 100644 --- a/src/pages/instance/Instance.tsx +++ b/src/pages/instance/Instance.tsx @@ -1,32 +1,13 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { NodeWithStatus } from "@/types/node"; import { useMemo, memo } from "react"; -import { formatBytes, formatUptime } from "@/utils"; +import { formatBytes, formatUptime, formatTrafficLimit } from "@/utils"; +import { CircleProgress } from "@/components/ui/circle-progress"; interface InstanceProps { node: NodeWithStatus; } -const formatTrafficLimit = ( - limit?: number, - type?: "sum" | "max" | "min" | "up" | "down" -) => { - if (!limit) return "未设置"; - - const limitText = formatBytes(limit); - - const typeText = - { - sum: "总和", - max: "最大值", - min: "最小值", - up: "上传", - down: "下载", - }[type || "max"] || ""; - - return `${limitText} (${typeText})`; -}; - const Instance = memo(({ node }: InstanceProps) => { const { stats, isOnline } = useMemo(() => { return { @@ -35,6 +16,33 @@ const Instance = memo(({ node }: InstanceProps) => { }; }, [node]); + // 计算流量使用百分比 + const trafficPercentage = useMemo(() => { + if (!node.traffic_limit || !stats || !isOnline) return 0; + + // 根据流量限制类型确定使用的流量值 + let usedTraffic = 0; + switch (node.traffic_limit_type) { + case "up": + usedTraffic = stats.network.totalUp; + break; + case "down": + usedTraffic = stats.network.totalDown; + break; + case "sum": + usedTraffic = stats.network.totalUp + stats.network.totalDown; + break; + case "min": + usedTraffic = Math.min(stats.network.totalUp, stats.network.totalDown); + break; + default: // max 或者未设置 + usedTraffic = Math.max(stats.network.totalUp, stats.network.totalDown); + break; + } + + return (usedTraffic / node.traffic_limit) * 100; + }, [node.traffic_limit, node.traffic_limit_type, stats, isOnline]); + return ( @@ -95,7 +103,7 @@ const Instance = memo(({ node }: InstanceProps) => {

运行时间

{formatUptime(stats?.uptime || 0)}

-
+

实时网络

{stats && isOnline @@ -106,20 +114,41 @@ const Instance = memo(({ node }: InstanceProps) => { : "N/A"}

-
+

总流量

-

- {stats && isOnline - ? `↑ ${formatBytes(stats.network.totalUp)} ↓ ${formatBytes( - stats.network.totalDown - )}` - : "N/A"} +

+ {node.traffic_limit && isOnline && ( + + )} +

+

+ {stats && isOnline + ? `↑ ${formatBytes(stats.network.totalUp)} ↓ ${formatBytes( + stats.network.totalDown + )}` + : "N/A"} +

+

+ {formatTrafficLimit( + node.traffic_limit, + node.traffic_limit_type + )} +

+

-
-

流量限制

+
+

负载

- {formatTrafficLimit(node.traffic_limit, node.traffic_limit_type)} + {stats && isOnline + ? `${stats.load.load1.toFixed(2)} | ${stats.load.load5.toFixed( + 2 + )} | ${stats.load.load15.toFixed(2)}` + : "N/A"}

diff --git a/src/pages/instance/index.tsx b/src/pages/instance/index.tsx index ad107c4..2c50ca3 100644 --- a/src/pages/instance/index.tsx +++ b/src/pages/instance/index.tsx @@ -127,46 +127,48 @@ const InstancePage = () => { {enableInstanceDetail && } -
-
- - {enablePingChart && ( +
+
+
+ {enablePingChart && ( + + )} +
+ {chartType === "load" ? ( +
+ {loadTimeRanges.map((range) => ( + + ))} +
+ ) : ( +
+ {pingTimeRanges.map((range) => ( + + ))} +
)}
- {chartType === "load" ? ( -
- {loadTimeRanges.map((range) => ( - - ))} -
- ) : ( -
- {pingTimeRanges.map((range) => ( - - ))} -
- )}
{ + if (!limit) return "未设置"; + + const limitText = formatBytes(limit); + + const typeText = + { + sum: "总和", + max: "最大值", + min: "最小值", + up: "上传", + down: "下载", + }[type || "max"] || ""; + + return `总 ${limitText} (${typeText})`; +};