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}
-
-
到期
+
-
-
在线
+
+
在线:
{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 (
+
+
+ {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})`;
+};