mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-18 19:39:22 +08:00
feat: 新增流量限制功能,优化节点卡片和列表显示
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
### 配置背景图片
|
### 配置背景图片
|
||||||
|
|
||||||
> 为获得最佳视觉效果,建议搭配背景图片使用。
|
> 为获得最佳视觉效果,建议搭配背景图片使用
|
||||||
|
|
||||||
#### Komari v1.0.5 及以上版本
|
#### Komari v1.0.5 及以上版本
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"name": "Komari Theme PurCart",
|
"name": "Komari Theme PurCart",
|
||||||
"short": "PurCarte",
|
"short": "PurCarte",
|
||||||
"description": "A frosted glass theme for Komari",
|
"description": "A frosted glass theme for Komari",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"author": "Montia & Gemini",
|
"author": "Montia & Gemini",
|
||||||
"url": "https://github.com/Montia37/Komari-theme-purcarte",
|
"url": "https://github.com/Montia37/Komari-theme-purcarte",
|
||||||
"preview": "preview.png",
|
"preview": "preview.png",
|
||||||
|
@@ -1,5 +1,10 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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 type { NodeWithStatus } from "@/types/node";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { CpuIcon, MemoryStickIcon, HardDriveIcon } from "lucide-react";
|
import { CpuIcon, MemoryStickIcon, HardDriveIcon } from "lucide-react";
|
||||||
@@ -7,6 +12,7 @@ import Flag from "./Flag";
|
|||||||
import { Tag } from "../ui/tag";
|
import { Tag } from "../ui/tag";
|
||||||
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
||||||
import { ProgressBar } from "../ui/progress-bar";
|
import { ProgressBar } from "../ui/progress-bar";
|
||||||
|
import { CircleProgress } from "../ui/circle-progress";
|
||||||
|
|
||||||
interface NodeCardProps {
|
interface NodeCardProps {
|
||||||
node: NodeWithStatus;
|
node: NodeWithStatus;
|
||||||
@@ -23,6 +29,7 @@ export const NodeCard = ({ node }: NodeCardProps) => {
|
|||||||
diskUsage,
|
diskUsage,
|
||||||
load,
|
load,
|
||||||
expired_at,
|
expired_at,
|
||||||
|
trafficPercentage,
|
||||||
} = useNodeCommons(node);
|
} = useNodeCommons(node);
|
||||||
|
|
||||||
const getProgressBarClass = (percentage: number) => {
|
const getProgressBarClass = (percentage: number) => {
|
||||||
@@ -129,7 +136,7 @@ export const NodeCard = ({ node }: NodeCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t border-border/60 my-2"></div>
|
<div className="border-t border-border/60 my-2"></div>
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<span className="text-secondary-foreground">网络</span>
|
<span className="text-secondary-foreground">网络:</span>
|
||||||
<div>
|
<div>
|
||||||
<span>↑ {stats ? formatBytes(stats.network.up, true) : "N/A"}</span>
|
<span>↑ {stats ? formatBytes(stats.network.up, true) : "N/A"}</span>
|
||||||
<span className="ml-2">
|
<span className="ml-2">
|
||||||
@@ -137,13 +144,38 @@ export const NodeCard = ({ node }: NodeCardProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-secondary-foreground">流量</span>
|
<span className="text-secondary-foreground w-1/4">流量</span>
|
||||||
<div>
|
<div className="flex items-center justify-between w-3/4">
|
||||||
<span>↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"}</span>
|
<div className="flex items-center justify-center w-1/3">
|
||||||
<span className="ml-2">
|
{node.traffic_limit !== 0 && isOnline && stats && (
|
||||||
↓ {stats ? formatBytes(stats.network.totalDown) : "N/A"}
|
<CircleProgress
|
||||||
</span>
|
value={trafficPercentage}
|
||||||
|
maxValue={100}
|
||||||
|
size={32}
|
||||||
|
strokeWidth={4}
|
||||||
|
showPercentage={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-2/3 text-right">
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2">
|
||||||
|
↓ {stats ? formatBytes(stats.network.totalDown) : "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{node.traffic_limit !== 0 && isOnline && stats && (
|
||||||
|
<div className="text-right">
|
||||||
|
{formatTrafficLimit(
|
||||||
|
node.traffic_limit,
|
||||||
|
node.traffic_limit_type
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
@@ -151,13 +183,13 @@ export const NodeCard = ({ node }: NodeCardProps) => {
|
|||||||
<span>{load}</span>
|
<span>{load}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-start w-full">
|
||||||
<span className="text-secondary-foreground">到期</span>
|
<span className="text-secondary-foreground">到期:</span>
|
||||||
<div className="flex items-center gap-1">{expired_at}</div>
|
<div className="flex items-center gap-1">{expired_at}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-l border-border/60 mx-2"></div>
|
<div className="border-l border-border/60 mx-2"></div>
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-start w-full">
|
||||||
<span className="text-secondary-foreground">在线</span>
|
<span className="text-secondary-foreground">在线:</span>
|
||||||
<span>
|
<span>
|
||||||
{isOnline && stats ? formatUptime(stats.uptime) : "离线"}
|
{isOnline && stats ? formatUptime(stats.uptime) : "离线"}
|
||||||
</span>
|
</span>
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
export const NodeListHeader = () => {
|
export const NodeListHeader = () => {
|
||||||
return (
|
return (
|
||||||
<div className="text-primary font-bold grid grid-cols-12 text-center shadow-md gap-4 p-2 items-center rounded-lg bg-card/50 transition-colors duration-200">
|
<div className="text-primary font-bold grid grid-cols-10 text-center shadow-md gap-4 p-2 items-center rounded-lg bg-card/50 transition-colors duration-200">
|
||||||
<div className="col-span-3">节点名称</div>
|
<div className="col-span-2">节点名称</div>
|
||||||
<div className="col-span-1">CPU</div>
|
<div className="col-span-1">CPU</div>
|
||||||
<div className="col-span-1">内存</div>
|
<div className="col-span-1">内存</div>
|
||||||
<div className="col-span-1">SWAP</div>
|
<div className="col-span-1">SWAP</div>
|
||||||
<div className="col-span-1">硬盘</div>
|
<div className="col-span-1">硬盘</div>
|
||||||
<div className="col-span-2">网络</div>
|
<div className="col-span-1">网络</div>
|
||||||
<div className="col-span-2">流量</div>
|
<div className="col-span-2">流量</div>
|
||||||
<div className="col-span-1">负载</div>
|
<div className="col-span-1">负载</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { formatBytes, formatUptime } from "@/utils";
|
import { formatBytes, formatTrafficLimit, formatUptime } from "@/utils";
|
||||||
import type { NodeWithStatus } from "@/types/node";
|
import type { NodeWithStatus } from "@/types/node";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { CpuIcon, MemoryStickIcon, HardDriveIcon } from "lucide-react";
|
import { CpuIcon, MemoryStickIcon, HardDriveIcon } from "lucide-react";
|
||||||
import Flag from "./Flag";
|
import Flag from "./Flag";
|
||||||
import { Tag } from "../ui/tag";
|
import { Tag } from "../ui/tag";
|
||||||
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
||||||
|
import { CircleProgress } from "../ui/circle-progress";
|
||||||
|
|
||||||
interface NodeListItemProps {
|
interface NodeListItemProps {
|
||||||
node: NodeWithStatus;
|
node: NodeWithStatus;
|
||||||
@@ -21,22 +22,23 @@ export const NodeListItem = ({ node }: NodeListItemProps) => {
|
|||||||
diskUsage,
|
diskUsage,
|
||||||
load,
|
load,
|
||||||
expired_at,
|
expired_at,
|
||||||
|
trafficPercentage,
|
||||||
} = useNodeCommons(node);
|
} = useNodeCommons(node);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid grid-cols-12 text-center shadow-md gap-4 p-2 items-center rounded-lg ${
|
className={`grid grid-cols-10 text-center shadow-md gap-4 p-2 items-center rounded-lg ${
|
||||||
isOnline
|
isOnline
|
||||||
? ""
|
? ""
|
||||||
: "striped-bg-red-translucent-diagonal ring-2 ring-red-500/50"
|
: "striped-bg-red-translucent-diagonal ring-2 ring-red-500/50"
|
||||||
} text-secondary-foreground transition-colors duration-200`}>
|
} text-secondary-foreground transition-colors duration-200`}>
|
||||||
<div className="col-span-3 flex items-center text-left">
|
<div className="col-span-2 flex items-center text-left">
|
||||||
<Flag flag={node.region} />
|
<Flag flag={node.region} />
|
||||||
<Link to={`/instance/${node.uuid}`}>
|
<Link to={`/instance/${node.uuid}`}>
|
||||||
<div className="ml-2 w-full">
|
<div className="ml-2 w-full">
|
||||||
<div className="text-base font-bold">{node.name}</div>
|
<div className="text-base font-bold">{node.name}</div>
|
||||||
<Tag className="text-xs" tags={tagList} />
|
<Tag className="text-xs" tags={tagList} />
|
||||||
<div className="flex text-xs">
|
<div className="flex text-xs text-nowrap">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-secondary-foreground">到期:</span>
|
<span className="text-secondary-foreground">到期:</span>
|
||||||
<div className="flex items-center gap-1">{expired_at}</div>
|
<div className="flex items-center gap-1">{expired_at}</div>
|
||||||
@@ -86,17 +88,42 @@ export const NodeListItem = ({ node }: NodeListItemProps) => {
|
|||||||
{isOnline ? `${diskUsage.toFixed(1)}%` : "N/A"}
|
{isOnline ? `${diskUsage.toFixed(1)}%` : "N/A"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-1">
|
||||||
<span>
|
<div>↑ {stats ? formatBytes(stats.network.up, true) : "N/A"}</div>
|
||||||
↑ {stats ? formatBytes(stats.network.up, true) : "N/A"}↓{" "}
|
<div>↓ {stats ? formatBytes(stats.network.down, true) : "N/A"}</div>
|
||||||
{stats ? formatBytes(stats.network.down, true) : "N/A"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<span>
|
<div className="flex items-center justify-around">
|
||||||
↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"}↓{" "}
|
{node.traffic_limit !== 0 && isOnline && stats && (
|
||||||
{stats ? formatBytes(stats.network.totalDown) : "N/A"}
|
<div className="flex items-center">
|
||||||
</span>
|
<CircleProgress
|
||||||
|
value={trafficPercentage}
|
||||||
|
maxValue={100}
|
||||||
|
size={32}
|
||||||
|
strokeWidth={4}
|
||||||
|
showPercentage={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={node.traffic_limit !== 0 ? "w-2/3" : "w-full"}>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
↑ {stats ? formatBytes(stats.network.totalUp) : "N/A"}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2">
|
||||||
|
↓ {stats ? formatBytes(stats.network.totalDown) : "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{node.traffic_limit !== 0 && isOnline && stats && (
|
||||||
|
<div>
|
||||||
|
{formatTrafficLimit(
|
||||||
|
node.traffic_limit,
|
||||||
|
node.traffic_limit_type
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<span>{load}</span>
|
<span>{load}</span>
|
||||||
|
66
src/components/ui/circle-progress.tsx
Normal file
66
src/components/ui/circle-progress.tsx
Normal file
@@ -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<CircleProgressProps> = ({
|
||||||
|
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 (
|
||||||
|
<div className={`relative inline-flex ${className}`}>
|
||||||
|
<svg width={size} height={size} className="transform -rotate-90">
|
||||||
|
{/* Background circle */}
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
className="stroke-gray-200 dark:stroke-gray-700"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
{/* Progress circle */}
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
className={getColor()}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{showPercentage && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center text-xs font-medium">
|
||||||
|
{percentage.toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -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 {
|
return {
|
||||||
stats,
|
stats,
|
||||||
isOnline,
|
isOnline,
|
||||||
@@ -78,5 +105,6 @@ export const useNodeCommons = (node: NodeWithStatus) => {
|
|||||||
diskUsage,
|
diskUsage,
|
||||||
load,
|
load,
|
||||||
expired_at,
|
expired_at,
|
||||||
|
trafficPercentage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,32 +1,13 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import type { NodeWithStatus } from "@/types/node";
|
import type { NodeWithStatus } from "@/types/node";
|
||||||
import { useMemo, memo } from "react";
|
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 {
|
interface InstanceProps {
|
||||||
node: NodeWithStatus;
|
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 Instance = memo(({ node }: InstanceProps) => {
|
||||||
const { stats, isOnline } = useMemo(() => {
|
const { stats, isOnline } = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -35,6 +16,33 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
};
|
};
|
||||||
}, [node]);
|
}, [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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
@@ -95,7 +103,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
<p className="text-muted-foreground text-sm">运行时间</p>
|
<p className="text-muted-foreground text-sm">运行时间</p>
|
||||||
<p className="text-sm">{formatUptime(stats?.uptime || 0)}</p>
|
<p className="text-sm">{formatUptime(stats?.uptime || 0)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">实时网络</p>
|
<p className="text-muted-foreground text-sm">实时网络</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{stats && isOnline
|
{stats && isOnline
|
||||||
@@ -106,20 +114,41 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
: "N/A"}
|
: "N/A"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">总流量</p>
|
<p className="text-muted-foreground text-sm">总流量</p>
|
||||||
<p className="text-sm">
|
<p className="flex items-center gap-2">
|
||||||
{stats && isOnline
|
{node.traffic_limit && isOnline && (
|
||||||
? `↑ ${formatBytes(stats.network.totalUp)} ↓ ${formatBytes(
|
<CircleProgress
|
||||||
stats.network.totalDown
|
value={trafficPercentage}
|
||||||
)}`
|
maxValue={100}
|
||||||
: "N/A"}
|
size={36}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm">
|
||||||
|
{stats && isOnline
|
||||||
|
? `↑ ${formatBytes(stats.network.totalUp)} ↓ ${formatBytes(
|
||||||
|
stats.network.totalDown
|
||||||
|
)}`
|
||||||
|
: "N/A"}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
{formatTrafficLimit(
|
||||||
|
node.traffic_limit,
|
||||||
|
node.traffic_limit_type
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">流量限制</p>
|
<p className="text-muted-foreground text-sm">负载</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{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"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@@ -127,46 +127,48 @@ const InstancePage = () => {
|
|||||||
|
|
||||||
{enableInstanceDetail && <Instance node={node as NodeWithStatus} />}
|
{enableInstanceDetail && <Instance node={node as NodeWithStatus} />}
|
||||||
|
|
||||||
<div className="bg-card border rounded-lg py-3 px-4 inline-block mx-auto">
|
<div className="flex justify-center w-full">
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="bg-card border rounded-lg py-3 px-4">
|
||||||
<Button
|
<div className="flex justify-center space-x-2">
|
||||||
variant={chartType === "load" ? "secondary" : "ghost"}
|
|
||||||
onClick={() => setChartType("load")}>
|
|
||||||
负载
|
|
||||||
</Button>
|
|
||||||
{enablePingChart && (
|
|
||||||
<Button
|
<Button
|
||||||
variant={chartType === "ping" ? "secondary" : "ghost"}
|
variant={chartType === "load" ? "secondary" : "ghost"}
|
||||||
onClick={() => setChartType("ping")}>
|
onClick={() => setChartType("load")}>
|
||||||
延迟
|
负载
|
||||||
</Button>
|
</Button>
|
||||||
|
{enablePingChart && (
|
||||||
|
<Button
|
||||||
|
variant={chartType === "ping" ? "secondary" : "ghost"}
|
||||||
|
onClick={() => setChartType("ping")}>
|
||||||
|
延迟
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{chartType === "load" ? (
|
||||||
|
<div className="flex justify-center space-x-2 mt-2">
|
||||||
|
{loadTimeRanges.map((range) => (
|
||||||
|
<Button
|
||||||
|
key={range.label}
|
||||||
|
variant={loadHours === range.hours ? "secondary" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setLoadHours(range.hours)}>
|
||||||
|
{range.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center space-x-2 mt-2">
|
||||||
|
{pingTimeRanges.map((range) => (
|
||||||
|
<Button
|
||||||
|
key={range.label}
|
||||||
|
variant={pingHours === range.hours ? "secondary" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPingHours(range.hours)}>
|
||||||
|
{range.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{chartType === "load" ? (
|
|
||||||
<div className="flex justify-center space-x-2 mt-2">
|
|
||||||
{loadTimeRanges.map((range) => (
|
|
||||||
<Button
|
|
||||||
key={range.label}
|
|
||||||
variant={loadHours === range.hours ? "secondary" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setLoadHours(range.hours)}>
|
|
||||||
{range.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-center space-x-2 mt-2">
|
|
||||||
{pingTimeRanges.map((range) => (
|
|
||||||
<Button
|
|
||||||
key={range.label}
|
|
||||||
variant={pingHours === range.hours ? "secondary" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setPingHours(range.hours)}>
|
|
||||||
{range.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Suspense
|
<Suspense
|
||||||
|
@@ -76,3 +76,23 @@ export const formatPrice = (
|
|||||||
|
|
||||||
return `${currency}${price.toFixed(2)}/${cycleStr}`;
|
return `${currency}${price.toFixed(2)}/${cycleStr}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export 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})`;
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user