mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-20 12:29:22 +08:00
feat: 新增流量限制功能,优化节点卡片和列表显示
This commit is contained in:
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
@@ -95,7 +103,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
||||
<p className="text-muted-foreground text-sm">运行时间</p>
|
||||
<p className="text-sm">{formatUptime(stats?.uptime || 0)}</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">实时网络</p>
|
||||
<p className="text-sm">
|
||||
{stats && isOnline
|
||||
@@ -106,20 +114,41 @@ const Instance = memo(({ node }: InstanceProps) => {
|
||||
: "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">总流量</p>
|
||||
<p className="text-sm">
|
||||
{stats && isOnline
|
||||
? `↑ ${formatBytes(stats.network.totalUp)} ↓ ${formatBytes(
|
||||
stats.network.totalDown
|
||||
)}`
|
||||
: "N/A"}
|
||||
<p className="flex items-center gap-2">
|
||||
{node.traffic_limit && isOnline && (
|
||||
<CircleProgress
|
||||
value={trafficPercentage}
|
||||
maxValue={100}
|
||||
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>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-muted-foreground text-sm">流量限制</p>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">负载</p>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
|
@@ -127,46 +127,48 @@ const InstancePage = () => {
|
||||
|
||||
{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 space-x-2">
|
||||
<Button
|
||||
variant={chartType === "load" ? "secondary" : "ghost"}
|
||||
onClick={() => setChartType("load")}>
|
||||
负载
|
||||
</Button>
|
||||
{enablePingChart && (
|
||||
<div className="flex justify-center w-full">
|
||||
<div className="bg-card border rounded-lg py-3 px-4">
|
||||
<div className="flex justify-center space-x-2">
|
||||
<Button
|
||||
variant={chartType === "ping" ? "secondary" : "ghost"}
|
||||
onClick={() => setChartType("ping")}>
|
||||
延迟
|
||||
variant={chartType === "load" ? "secondary" : "ghost"}
|
||||
onClick={() => setChartType("load")}>
|
||||
负载
|
||||
</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>
|
||||
{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>
|
||||
|
||||
<Suspense
|
||||
|
Reference in New Issue
Block a user