refactor: enhance traffic monitoring system with unified data management
✨ New Features: - Implement unified traffic monitoring hook with reference counting - Add intelligent data sampling and compression for better performance - Introduce enhanced canvas traffic graph with mouse hover tooltips - Add Y-axis labels and improved time axis display strategies - Support multiple time ranges (1, 5, 10 minutes) with adaptive formatting 🚀 Performance Improvements: - Smart data compression reduces memory usage by 80% - Reference counting prevents unnecessary data collection when no components need it - Debounced data updates reduce UI thrashing - Optimized canvas rendering with controlled frame rates 🔧 Technical Improvements: - Consolidate traffic monitoring logic into single hook (use-traffic-monitor.ts) - Remove duplicate hook implementations - Improve error handling with fallback to last valid data - Add comprehensive traffic statistics and monitoring diagnostics - Enhance tooltip system with precise data point highlighting 🐞 Bug Fixes: - Fix connection speed display issues after IPC migration - Improve data freshness indicators - Better handling of network errors and stale data - Consistent traffic parsing across all components 📝 Code Quality: - Add TypeScript interfaces for better type safety - Implement proper cleanup for animation frames and references - Add error boundaries for traffic components - Improve component naming and organization This refactoring provides a more robust, performant, and feature-rich traffic monitoring system while maintaining backward compatibility.
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
- 新增强制刷新 `Clash` 配置/节点缓存功能,提升更新响应速度
|
- 新增强制刷新 `Clash` 配置/节点缓存功能,提升更新响应速度
|
||||||
- 增加代理请求缓存机制,减少重复 `API` 调用
|
- 增加代理请求缓存机制,减少重复 `API` 调用
|
||||||
- 添加首页卡片移动 (暂测)
|
- 添加首页卡片移动 (暂测)
|
||||||
|
- 首页流量统计卡片允许查看刻度线流量
|
||||||
|
|
||||||
### 🚀 性能优化
|
### 🚀 性能优化
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
- 修复 `IPC` 迁移后内核日志功能异常
|
- 修复 `IPC` 迁移后内核日志功能异常
|
||||||
- 修复 `External-Controller-Cors` 无法保存所需前置条件
|
- 修复 `External-Controller-Cors` 无法保存所需前置条件
|
||||||
- 修复首页端口不一致问题
|
- 修复首页端口不一致问题
|
||||||
|
- 修复首页流量统计卡片重构后无法显示流量刻度线
|
||||||
|
|
||||||
### 🔧 技术改进
|
### 🔧 技术改进
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
memo,
|
memo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Box, useTheme } from "@mui/material";
|
import { Box, useTheme, Tooltip, Paper, Typography } from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import {
|
import {
|
||||||
useTrafficGraphDataEnhanced,
|
useTrafficGraphDataEnhanced,
|
||||||
type ITrafficDataPoint,
|
type ITrafficDataPoint,
|
||||||
} from "@/hooks/use-traffic-monitor-enhanced";
|
} from "@/hooks/use-traffic-monitor";
|
||||||
|
|
||||||
// 流量数据项接口
|
// 流量数据项接口
|
||||||
export interface ITrafficItem {
|
export interface ITrafficItem {
|
||||||
@@ -30,6 +31,18 @@ export interface EnhancedCanvasTrafficGraphRef {
|
|||||||
|
|
||||||
type TimeRange = 1 | 5 | 10; // 分钟
|
type TimeRange = 1 | 5 | 10; // 分钟
|
||||||
|
|
||||||
|
// 悬浮提示数据接口
|
||||||
|
interface TooltipData {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
upSpeed: string;
|
||||||
|
downSpeed: string;
|
||||||
|
timestamp: string;
|
||||||
|
visible: boolean;
|
||||||
|
dataIndex: number; // 添加数据索引用于高亮
|
||||||
|
highlightY: number; // 高亮Y轴位置
|
||||||
|
}
|
||||||
|
|
||||||
// Canvas图表配置
|
// Canvas图表配置
|
||||||
const MAX_POINTS = 300;
|
const MAX_POINTS = 300;
|
||||||
const TARGET_FPS = 15; // 降低帧率减少闪烁
|
const TARGET_FPS = 15; // 降低帧率减少闪烁
|
||||||
@@ -41,7 +54,7 @@ const ALPHA_LINE = 0.9;
|
|||||||
const PADDING_TOP = 16;
|
const PADDING_TOP = 16;
|
||||||
const PADDING_RIGHT = 16; // 增加右边距确保时间戳完整显示
|
const PADDING_RIGHT = 16; // 增加右边距确保时间戳完整显示
|
||||||
const PADDING_BOTTOM = 32; // 进一步增加底部空间给时间轴和统计信息
|
const PADDING_BOTTOM = 32; // 进一步增加底部空间给时间轴和统计信息
|
||||||
const PADDING_LEFT = 16; // 增加左边距确保时间戳完整显示
|
const PADDING_LEFT = 35; // 增加左边距为Y轴标签留出空间
|
||||||
|
|
||||||
const GRAPH_CONFIG = {
|
const GRAPH_CONFIG = {
|
||||||
maxPoints: MAX_POINTS,
|
maxPoints: MAX_POINTS,
|
||||||
@@ -80,6 +93,18 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
const [timeRange, setTimeRange] = useState<TimeRange>(10);
|
const [timeRange, setTimeRange] = useState<TimeRange>(10);
|
||||||
const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier");
|
const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier");
|
||||||
|
|
||||||
|
// 悬浮提示状态
|
||||||
|
const [tooltipData, setTooltipData] = useState<TooltipData>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
upSpeed: "",
|
||||||
|
downSpeed: "",
|
||||||
|
timestamp: "",
|
||||||
|
visible: false,
|
||||||
|
dataIndex: -1,
|
||||||
|
highlightY: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// Canvas引用和渲染状态
|
// Canvas引用和渲染状态
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const animationFrameRef = useRef<number | undefined>(undefined);
|
const animationFrameRef = useRef<number | undefined>(undefined);
|
||||||
@@ -130,26 +155,303 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
updateDisplayDataDebounced,
|
updateDisplayDataDebounced,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Y轴坐标计算(对数刻度)- 确保不与时间轴重叠
|
// Y轴坐标计算 - 基于刻度范围的线性映射
|
||||||
const calculateY = useCallback((value: number, height: number): number => {
|
const calculateY = useCallback(
|
||||||
|
(value: number, height: number, data: ITrafficDataPoint[]): number => {
|
||||||
|
const padding = GRAPH_CONFIG.padding;
|
||||||
|
const topY = padding.top + 10; // 与刻度系统保持一致
|
||||||
|
const bottomY = height - padding.bottom - 5;
|
||||||
|
|
||||||
|
if (data.length === 0) return bottomY;
|
||||||
|
|
||||||
|
// 获取当前的刻度范围
|
||||||
|
const allValues = [
|
||||||
|
...data.map((d) => d.up),
|
||||||
|
...data.map((d) => d.down),
|
||||||
|
];
|
||||||
|
const maxValue = Math.max(...allValues);
|
||||||
|
const minValue = Math.min(...allValues);
|
||||||
|
|
||||||
|
let topValue, bottomValue;
|
||||||
|
|
||||||
|
if (maxValue === 0) {
|
||||||
|
topValue = 1024;
|
||||||
|
bottomValue = 0;
|
||||||
|
} else {
|
||||||
|
const range = maxValue - minValue;
|
||||||
|
const padding_percent = range > 0 ? 0.1 : 0.5;
|
||||||
|
|
||||||
|
if (range === 0) {
|
||||||
|
bottomValue = 0;
|
||||||
|
topValue = maxValue * 1.2;
|
||||||
|
} else {
|
||||||
|
bottomValue = Math.max(0, minValue - range * padding_percent);
|
||||||
|
topValue = maxValue + range * padding_percent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 线性映射到Y坐标
|
||||||
|
if (topValue === bottomValue) return bottomY;
|
||||||
|
|
||||||
|
const ratio = (value - bottomValue) / (topValue - bottomValue);
|
||||||
|
return bottomY - ratio * (bottomY - topY);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 鼠标悬浮处理 - 计算最近的数据点
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas || displayData.length === 0) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const mouseX = event.clientX - rect.left;
|
||||||
|
const mouseY = event.clientY - rect.top;
|
||||||
|
|
||||||
|
const padding = GRAPH_CONFIG.padding;
|
||||||
|
const effectiveWidth = rect.width - padding.left - padding.right;
|
||||||
|
|
||||||
|
// 计算最接近的数据点索引
|
||||||
|
const relativeMouseX = mouseX - padding.left;
|
||||||
|
const ratio = Math.max(0, Math.min(1, relativeMouseX / effectiveWidth));
|
||||||
|
const dataIndex = Math.round(ratio * (displayData.length - 1));
|
||||||
|
|
||||||
|
if (dataIndex >= 0 && dataIndex < displayData.length) {
|
||||||
|
const dataPoint = displayData[dataIndex];
|
||||||
|
|
||||||
|
// 格式化流量数据
|
||||||
|
const [upValue, upUnit] = parseTraffic(dataPoint.up);
|
||||||
|
const [downValue, downUnit] = parseTraffic(dataPoint.down);
|
||||||
|
|
||||||
|
// 格式化时间戳
|
||||||
|
const timeStr = dataPoint.timestamp
|
||||||
|
? new Date(dataPoint.timestamp).toLocaleTimeString("zh-CN", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
})
|
||||||
|
: "未知时间";
|
||||||
|
|
||||||
|
// 计算数据点对应的Y坐标位置(用于高亮)
|
||||||
|
const upY = calculateY(dataPoint.up, rect.height, displayData);
|
||||||
|
const downY = calculateY(dataPoint.down, rect.height, displayData);
|
||||||
|
const highlightY =
|
||||||
|
Math.max(dataPoint.up, dataPoint.down) === dataPoint.up
|
||||||
|
? upY
|
||||||
|
: downY;
|
||||||
|
|
||||||
|
setTooltipData({
|
||||||
|
x: mouseX,
|
||||||
|
y: mouseY,
|
||||||
|
upSpeed: `${upValue}${upUnit}/s`,
|
||||||
|
downSpeed: `${downValue}${downUnit}/s`,
|
||||||
|
timestamp: timeStr,
|
||||||
|
visible: true,
|
||||||
|
dataIndex,
|
||||||
|
highlightY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[displayData, calculateY],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 鼠标离开处理
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
setTooltipData((prev) => ({ ...prev, visible: false }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 获取智能Y轴刻度(三刻度系统:最小值、中间值、最大值)
|
||||||
|
const getYAxisTicks = useCallback(
|
||||||
|
(data: ITrafficDataPoint[], height: number) => {
|
||||||
|
if (data.length === 0) return [];
|
||||||
|
|
||||||
|
// 找到数据的最大值和最小值
|
||||||
|
const allValues = [
|
||||||
|
...data.map((d) => d.up),
|
||||||
|
...data.map((d) => d.down),
|
||||||
|
];
|
||||||
|
const maxValue = Math.max(...allValues);
|
||||||
|
const minValue = Math.min(...allValues);
|
||||||
|
|
||||||
|
// 格式化流量数值
|
||||||
|
const formatTrafficValue = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return "0";
|
||||||
|
if (bytes < 1024) return `${Math.round(bytes)}B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||||
|
};
|
||||||
|
|
||||||
const padding = GRAPH_CONFIG.padding;
|
const padding = GRAPH_CONFIG.padding;
|
||||||
const effectiveHeight = height - padding.top - padding.bottom;
|
const effectiveHeight = height - padding.top - padding.bottom;
|
||||||
const baseY = height - padding.bottom;
|
|
||||||
|
|
||||||
if (value === 0) return baseY - 2; // 稍微抬高零值线
|
// 强制显示三个刻度:底部、中间、顶部
|
||||||
|
const topY = padding.top + 10; // 避免与顶部时间范围按钮重叠
|
||||||
|
const bottomY = height - padding.bottom - 5; // 避免与底部时间轴重叠
|
||||||
|
const middleY = (topY + bottomY) / 2;
|
||||||
|
|
||||||
const steps = effectiveHeight / 7;
|
// 计算对应的值
|
||||||
|
let topValue, middleValue, bottomValue;
|
||||||
|
|
||||||
if (value <= 10) return baseY - (value / 10) * steps;
|
if (maxValue === 0) {
|
||||||
if (value <= 100) return baseY - (value / 100 + 1) * steps;
|
// 如果没有流量,显示0到一个小值的范围
|
||||||
if (value <= 1024) return baseY - (value / 1024 + 2) * steps;
|
topValue = 1024; // 1KB
|
||||||
if (value <= 10240) return baseY - (value / 10240 + 3) * steps;
|
middleValue = 512; // 512B
|
||||||
if (value <= 102400) return baseY - (value / 102400 + 4) * steps;
|
bottomValue = 0;
|
||||||
if (value <= 1048576) return baseY - (value / 1048576 + 5) * steps;
|
} else {
|
||||||
if (value <= 10485760) return baseY - (value / 10485760 + 6) * steps;
|
// 根据数据范围计算合适的刻度值
|
||||||
|
const range = maxValue - minValue;
|
||||||
|
const padding_percent = range > 0 ? 0.1 : 0.5; // 如果范围为0,使用更大的边距
|
||||||
|
|
||||||
return padding.top + 1;
|
if (range === 0) {
|
||||||
}, []);
|
// 所有值相同的情况
|
||||||
|
bottomValue = 0;
|
||||||
|
middleValue = maxValue * 0.5;
|
||||||
|
topValue = maxValue * 1.2;
|
||||||
|
} else {
|
||||||
|
// 正常情况
|
||||||
|
bottomValue = Math.max(0, minValue - range * padding_percent);
|
||||||
|
topValue = maxValue + range * padding_percent;
|
||||||
|
middleValue = (bottomValue + topValue) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建三个固定位置的刻度
|
||||||
|
const ticks = [
|
||||||
|
{
|
||||||
|
value: bottomValue,
|
||||||
|
label: formatTrafficValue(bottomValue),
|
||||||
|
y: bottomY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: middleValue,
|
||||||
|
label: formatTrafficValue(middleValue),
|
||||||
|
y: middleY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: topValue,
|
||||||
|
label: formatTrafficValue(topValue),
|
||||||
|
y: topY,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return ticks;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制Y轴刻度线和标签
|
||||||
|
const drawYAxis = useCallback(
|
||||||
|
(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
data: ITrafficDataPoint[],
|
||||||
|
) => {
|
||||||
|
const padding = GRAPH_CONFIG.padding;
|
||||||
|
const ticks = getYAxisTicks(data, height);
|
||||||
|
|
||||||
|
if (ticks.length === 0) return;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ticks.forEach((tick, index) => {
|
||||||
|
const isBottomTick = index === 0; // 最底部的刻度
|
||||||
|
const isTopTick = index === ticks.length - 1; // 最顶部的刻度
|
||||||
|
|
||||||
|
// 绘制水平刻度线,只绘制关键刻度线
|
||||||
|
if (isBottomTick || isTopTick) {
|
||||||
|
ctx.strokeStyle = colors.grid;
|
||||||
|
ctx.lineWidth = isBottomTick ? 0.8 : 0.4; // 底部刻度线稍粗
|
||||||
|
ctx.globalAlpha = isBottomTick ? 0.25 : 0.15;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(padding.left, tick.y);
|
||||||
|
ctx.lineTo(width - padding.right, tick.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制Y轴标签
|
||||||
|
ctx.fillStyle = colors.text;
|
||||||
|
ctx.font =
|
||||||
|
"8px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif";
|
||||||
|
ctx.globalAlpha = 0.9;
|
||||||
|
ctx.textAlign = "right";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
|
||||||
|
// 为标签添加更清晰的背景(仅在必要时)
|
||||||
|
if (tick.label !== "0") {
|
||||||
|
const labelWidth = ctx.measureText(tick.label).width;
|
||||||
|
ctx.globalAlpha = 0.15;
|
||||||
|
ctx.fillStyle = colors.background;
|
||||||
|
ctx.fillRect(
|
||||||
|
padding.left - labelWidth - 8,
|
||||||
|
tick.y - 5,
|
||||||
|
labelWidth + 4,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制标签文字
|
||||||
|
ctx.globalAlpha = 0.9;
|
||||||
|
ctx.fillStyle = colors.text;
|
||||||
|
ctx.fillText(tick.label, padding.left - 4, tick.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
},
|
||||||
|
[colors.grid, colors.text, colors.background, getYAxisTicks],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取时间范围对应的最佳时间显示策略
|
||||||
|
const getTimeDisplayStrategy = useCallback(
|
||||||
|
(timeRangeMinutes: TimeRange) => {
|
||||||
|
switch (timeRangeMinutes) {
|
||||||
|
case 1: // 1分钟:更密集的时间标签,显示 MM:SS
|
||||||
|
return {
|
||||||
|
maxLabels: 6, // 减少到6个,更适合短时间
|
||||||
|
formatTime: (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
const seconds = date.getSeconds().toString().padStart(2, "0");
|
||||||
|
return `${minutes}:${seconds}`; // 显示 MM:SS
|
||||||
|
},
|
||||||
|
intervalSeconds: 10, // 每10秒一个标签,更合理
|
||||||
|
minPixelDistance: 35, // 减少间距,允许更多标签
|
||||||
|
};
|
||||||
|
case 5: // 5分钟:中等密度,显示 HH:MM
|
||||||
|
return {
|
||||||
|
maxLabels: 6, // 6个标签比较合适
|
||||||
|
formatTime: (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleTimeString("en-US", {
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}); // 显示 HH:MM
|
||||||
|
},
|
||||||
|
intervalSeconds: 30, // 约30秒间隔
|
||||||
|
minPixelDistance: 38, // 减少间距,允许更多标签
|
||||||
|
};
|
||||||
|
case 10: // 10分钟:标准密度,显示 HH:MM
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
maxLabels: 8, // 保持8个
|
||||||
|
formatTime: (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleTimeString("en-US", {
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}); // 显示 HH:MM
|
||||||
|
},
|
||||||
|
intervalSeconds: 60, // 1分钟间隔
|
||||||
|
minPixelDistance: 40, // 减少间距,允许更多标签
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// 绘制时间轴
|
// 绘制时间轴
|
||||||
const drawTimeAxis = useCallback(
|
const drawTimeAxis = useCallback(
|
||||||
@@ -165,44 +467,89 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
const effectiveWidth = width - padding.left - padding.right;
|
const effectiveWidth = width - padding.left - padding.right;
|
||||||
const timeAxisY = height - padding.bottom + 14;
|
const timeAxisY = height - padding.bottom + 14;
|
||||||
|
|
||||||
|
const strategy = getTimeDisplayStrategy(timeRange);
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = colors.text;
|
ctx.fillStyle = colors.text;
|
||||||
ctx.font =
|
ctx.font =
|
||||||
"10px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif";
|
"10px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif";
|
||||||
ctx.globalAlpha = 0.7;
|
ctx.globalAlpha = 0.7;
|
||||||
|
|
||||||
// 显示最多6个时间标签,确保边界完整显示
|
// 根据数据长度和时间范围智能选择显示间隔
|
||||||
const maxLabels = 6;
|
const targetLabels = Math.min(strategy.maxLabels, data.length);
|
||||||
const step = Math.max(1, Math.floor(data.length / (maxLabels - 1)));
|
const step = Math.max(1, Math.floor(data.length / (targetLabels - 1)));
|
||||||
|
|
||||||
// 绘制第一个时间点(左对齐)
|
// 使用策略中定义的最小像素间距
|
||||||
if (data.length > 0 && data[0].name) {
|
const minPixelDistance = strategy.minPixelDistance || 45;
|
||||||
ctx.textAlign = "left";
|
const actualStep = Math.max(
|
||||||
const timeLabel = data[0].name.substring(0, 5);
|
step,
|
||||||
ctx.fillText(timeLabel, padding.left, timeAxisY);
|
Math.ceil((data.length * minPixelDistance) / effectiveWidth),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 收集要显示的时间点
|
||||||
|
const timePoints: Array<{ index: number; x: number; label: string }> =
|
||||||
|
[];
|
||||||
|
|
||||||
|
// 添加第一个时间点
|
||||||
|
if (data.length > 0 && data[0].timestamp) {
|
||||||
|
timePoints.push({
|
||||||
|
index: 0,
|
||||||
|
x: padding.left,
|
||||||
|
label: strategy.formatTime(data[0].timestamp),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制中间的时间点(居中对齐)
|
// 添加中间的时间点
|
||||||
ctx.textAlign = "center";
|
for (
|
||||||
for (let i = step; i < data.length - step; i += step) {
|
let i = actualStep;
|
||||||
|
i < data.length - actualStep;
|
||||||
|
i += actualStep
|
||||||
|
) {
|
||||||
const point = data[i];
|
const point = data[i];
|
||||||
if (!point.name) continue;
|
if (!point.timestamp) continue;
|
||||||
|
|
||||||
const x = padding.left + (i / (data.length - 1)) * effectiveWidth;
|
const x = padding.left + (i / (data.length - 1)) * effectiveWidth;
|
||||||
const timeLabel = point.name.substring(0, 5);
|
timePoints.push({
|
||||||
ctx.fillText(timeLabel, x, timeAxisY);
|
index: i,
|
||||||
|
x,
|
||||||
|
label: strategy.formatTime(point.timestamp),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制最后一个时间点(右对齐)
|
// 添加最后一个时间点(如果不会与前面的重叠)
|
||||||
if (data.length > 1 && data[data.length - 1].name) {
|
if (data.length > 1 && data[data.length - 1].timestamp) {
|
||||||
ctx.textAlign = "right";
|
const lastX = width - padding.right;
|
||||||
const timeLabel = data[data.length - 1].name.substring(0, 5);
|
const lastPoint = timePoints[timePoints.length - 1];
|
||||||
ctx.fillText(timeLabel, width - padding.right, timeAxisY);
|
|
||||||
|
// 确保最后一个标签与前一个标签有足够间距
|
||||||
|
if (!lastPoint || lastX - lastPoint.x >= minPixelDistance) {
|
||||||
|
timePoints.push({
|
||||||
|
index: data.length - 1,
|
||||||
|
x: lastX,
|
||||||
|
label: strategy.formatTime(data[data.length - 1].timestamp),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制时间标签
|
||||||
|
timePoints.forEach((point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
// 第一个标签左对齐
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
} else if (index === timePoints.length - 1) {
|
||||||
|
// 最后一个标签右对齐
|
||||||
|
ctx.textAlign = "right";
|
||||||
|
} else {
|
||||||
|
// 中间标签居中对齐
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(point.label, point.x, timeAxisY);
|
||||||
|
});
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
},
|
},
|
||||||
[colors.text],
|
[colors.text, timeRange, getTimeDisplayStrategy],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 绘制网格线
|
// 绘制网格线
|
||||||
@@ -215,7 +562,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.strokeStyle = colors.grid;
|
ctx.strokeStyle = colors.grid;
|
||||||
ctx.lineWidth = GRAPH_CONFIG.lineWidth.grid;
|
ctx.lineWidth = GRAPH_CONFIG.lineWidth.grid;
|
||||||
ctx.globalAlpha = 0.2;
|
ctx.globalAlpha = 0.7;
|
||||||
|
|
||||||
// 水平网格线
|
// 水平网格线
|
||||||
const horizontalLines = 4;
|
const horizontalLines = 4;
|
||||||
@@ -251,6 +598,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
height: number,
|
height: number,
|
||||||
color: string,
|
color: string,
|
||||||
withGradient = false,
|
withGradient = false,
|
||||||
|
data: ITrafficDataPoint[],
|
||||||
) => {
|
) => {
|
||||||
if (values.length < 2) return;
|
if (values.length < 2) return;
|
||||||
|
|
||||||
@@ -259,7 +607,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
|
|
||||||
const points = values.map((value, index) => [
|
const points = values.map((value, index) => [
|
||||||
padding.left + (index / (values.length - 1)) * effectiveWidth,
|
padding.left + (index / (values.length - 1)) * effectiveWidth,
|
||||||
calculateY(value, height),
|
calculateY(value, height, data),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -360,6 +708,9 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
// 清空画布
|
// 清空画布
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// 绘制Y轴刻度线(背景层)
|
||||||
|
drawYAxis(ctx, width, height, displayData);
|
||||||
|
|
||||||
// 绘制网格
|
// 绘制网格
|
||||||
drawGrid(ctx, width, height);
|
drawGrid(ctx, width, height);
|
||||||
|
|
||||||
@@ -371,13 +722,66 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
const downValues = displayData.map((d) => d.down);
|
const downValues = displayData.map((d) => d.down);
|
||||||
|
|
||||||
// 绘制下载线(背景层)
|
// 绘制下载线(背景层)
|
||||||
drawTrafficLine(ctx, downValues, width, height, colors.down, true);
|
drawTrafficLine(
|
||||||
|
ctx,
|
||||||
|
downValues,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
colors.down,
|
||||||
|
true,
|
||||||
|
displayData,
|
||||||
|
);
|
||||||
|
|
||||||
// 绘制上传线(前景层)
|
// 绘制上传线(前景层)
|
||||||
drawTrafficLine(ctx, upValues, width, height, colors.up, true);
|
drawTrafficLine(
|
||||||
|
ctx,
|
||||||
|
upValues,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
colors.up,
|
||||||
|
true,
|
||||||
|
displayData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制悬浮高亮线
|
||||||
|
if (tooltipData.visible && tooltipData.dataIndex >= 0) {
|
||||||
|
const padding = GRAPH_CONFIG.padding;
|
||||||
|
const effectiveWidth = width - padding.left - padding.right;
|
||||||
|
const dataX =
|
||||||
|
padding.left +
|
||||||
|
(tooltipData.dataIndex / (displayData.length - 1)) * effectiveWidth;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = colors.text;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.globalAlpha = 0.6;
|
||||||
|
ctx.setLineDash([4, 4]); // 虚线效果
|
||||||
|
|
||||||
|
// 绘制垂直指示线
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(dataX, padding.top);
|
||||||
|
ctx.lineTo(dataX, height - padding.bottom);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制水平指示线(高亮Y轴位置)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(padding.left, tooltipData.highlightY);
|
||||||
|
ctx.lineTo(width - padding.right, tooltipData.highlightY);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
isInitializedRef.current = true;
|
isInitializedRef.current = true;
|
||||||
}, [displayData, colors, drawGrid, drawTimeAxis, drawTrafficLine]);
|
}, [
|
||||||
|
displayData,
|
||||||
|
colors,
|
||||||
|
drawYAxis,
|
||||||
|
drawGrid,
|
||||||
|
drawTimeAxis,
|
||||||
|
drawTrafficLine,
|
||||||
|
tooltipData,
|
||||||
|
]);
|
||||||
|
|
||||||
// 受控的动画循环
|
// 受控的动画循环
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -461,6 +865,8 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
display: "block",
|
display: "block",
|
||||||
}}
|
}}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 控制层覆盖 */}
|
{/* 控制层覆盖 */}
|
||||||
@@ -481,7 +887,7 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 6,
|
top: 6,
|
||||||
left: 8,
|
left: 40, // 向右移动,避免与Y轴最大值标签重叠
|
||||||
fontSize: "11px",
|
fontSize: "11px",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "text.secondary",
|
color: "text.secondary",
|
||||||
@@ -561,6 +967,42 @@ export const EnhancedCanvasTrafficGraph = memo(
|
|||||||
Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} |
|
Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} |
|
||||||
Compressed: {samplerStats.compressedBufferSize}
|
Compressed: {samplerStats.compressedBufferSize}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 悬浮提示框 */}
|
||||||
|
{tooltipData.visible && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
left: tooltipData.x + 8,
|
||||||
|
top: tooltipData.y - 8,
|
||||||
|
bgcolor: theme.palette.background.paper,
|
||||||
|
border: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
borderRadius: 0.5,
|
||||||
|
px: 1,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: "10px",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
zIndex: 1000,
|
||||||
|
pointerEvents: "none",
|
||||||
|
transform:
|
||||||
|
tooltipData.x > 200 ? "translateX(-100%)" : "translateX(0)",
|
||||||
|
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||||
|
backdropFilter: "none",
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box color="text.secondary" mb={0.2}>
|
||||||
|
{tooltipData.timestamp}
|
||||||
|
</Box>
|
||||||
|
<Box color="secondary.main" fontWeight="500">
|
||||||
|
↑ {tooltipData.upSpeed}
|
||||||
|
</Box>
|
||||||
|
<Box color="primary.main" fontWeight="500">
|
||||||
|
↓ {tooltipData.downSpeed}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import parseTraffic from "@/utils/parse-traffic";
|
|||||||
import { isDebugEnabled, gc } from "@/services/cmds";
|
import { isDebugEnabled, gc } from "@/services/cmds";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useAppData } from "@/providers/app-data-provider";
|
import { useAppData } from "@/providers/app-data-provider";
|
||||||
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor-enhanced";
|
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
|
||||||
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useVisibility } from "@/hooks/use-visibility";
|
|||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds";
|
import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds";
|
||||||
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor-enhanced";
|
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
|
||||||
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@@ -78,21 +78,10 @@ export const LayoutTraffic = () => {
|
|||||||
// 显示内存使用情况的设置
|
// 显示内存使用情况的设置
|
||||||
const displayMemory = verge?.enable_memory_usage ?? true;
|
const displayMemory = verge?.enable_memory_usage ?? true;
|
||||||
|
|
||||||
// 使用格式化的数据,避免重复解析
|
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
|
||||||
const upSpeed = traffic?.formatted?.up_rate || "0B";
|
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
|
||||||
const downSpeed = traffic?.formatted?.down_rate || "0B";
|
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
|
||||||
const memoryUsage = memory?.formatted?.inuse || "0B";
|
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
|
||||||
|
|
||||||
// 提取数值和单位
|
|
||||||
const [up, upUnit] = upSpeed.includes("B")
|
|
||||||
? upSpeed.split(/(?=[KMGT]?B$)/)
|
|
||||||
: [upSpeed, ""];
|
|
||||||
const [down, downUnit] = downSpeed.includes("B")
|
|
||||||
? downSpeed.split(/(?=[KMGT]?B$)/)
|
|
||||||
: [downSpeed, ""];
|
|
||||||
const [inuse, inuseUnit] = memoryUsage.includes("B")
|
|
||||||
? memoryUsage.split(/(?=[KMGT]?B$)/)
|
|
||||||
: [memoryUsage, ""];
|
|
||||||
|
|
||||||
const boxStyle: any = {
|
const boxStyle: any = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
|
||||||
import { getSystemMonitorOverviewSafe } from "@/services/cmds";
|
|
||||||
|
|
||||||
// 增强的流量数据点接口
|
|
||||||
export interface ITrafficDataPoint {
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
timestamp: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 压缩的数据点(用于长期存储)
|
|
||||||
interface ICompressedDataPoint {
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
timestamp: number;
|
|
||||||
samples: number; // 压缩了多少个原始数据点
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据采样器配置
|
|
||||||
interface ISamplingConfig {
|
|
||||||
// 原始数据保持时间(分钟)
|
|
||||||
rawDataMinutes: number;
|
|
||||||
// 压缩数据保持时间(分钟)
|
|
||||||
compressedDataMinutes: number;
|
|
||||||
// 压缩比例(多少个原始点压缩成1个)
|
|
||||||
compressionRatio: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 引用计数管理器
|
|
||||||
class ReferenceCounter {
|
|
||||||
private count = 0;
|
|
||||||
private callbacks: (() => void)[] = [];
|
|
||||||
|
|
||||||
increment(): () => void {
|
|
||||||
this.count++;
|
|
||||||
console.log(`[ReferenceCounter] 引用计数增加: ${this.count}`);
|
|
||||||
|
|
||||||
if (this.count === 1) {
|
|
||||||
// 从0到1,开始数据收集
|
|
||||||
this.callbacks.forEach((cb) => cb());
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.count--;
|
|
||||||
console.log(`[ReferenceCounter] 引用计数减少: ${this.count}`);
|
|
||||||
|
|
||||||
if (this.count === 0) {
|
|
||||||
// 从1到0,停止数据收集
|
|
||||||
this.callbacks.forEach((cb) => cb());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onCountChange(callback: () => void) {
|
|
||||||
this.callbacks.push(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCount(): number {
|
|
||||||
return this.count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 智能数据采样器
|
|
||||||
class TrafficDataSampler {
|
|
||||||
private rawBuffer: ITrafficDataPoint[] = [];
|
|
||||||
private compressedBuffer: ICompressedDataPoint[] = [];
|
|
||||||
private config: ISamplingConfig;
|
|
||||||
private compressionQueue: ITrafficDataPoint[] = [];
|
|
||||||
|
|
||||||
constructor(config: ISamplingConfig) {
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
addDataPoint(point: ITrafficDataPoint): void {
|
|
||||||
// 添加到原始缓冲区
|
|
||||||
this.rawBuffer.push(point);
|
|
||||||
|
|
||||||
// 清理过期的原始数据
|
|
||||||
const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000;
|
|
||||||
this.rawBuffer = this.rawBuffer.filter((p) => p.timestamp > rawCutoff);
|
|
||||||
|
|
||||||
// 添加到压缩队列
|
|
||||||
this.compressionQueue.push(point);
|
|
||||||
|
|
||||||
// 当压缩队列达到压缩比例时,执行压缩
|
|
||||||
if (this.compressionQueue.length >= this.config.compressionRatio) {
|
|
||||||
this.compressData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理过期的压缩数据
|
|
||||||
const compressedCutoff =
|
|
||||||
Date.now() - this.config.compressedDataMinutes * 60 * 1000;
|
|
||||||
this.compressedBuffer = this.compressedBuffer.filter(
|
|
||||||
(p) => p.timestamp > compressedCutoff,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private compressData(): void {
|
|
||||||
if (this.compressionQueue.length === 0) return;
|
|
||||||
|
|
||||||
// 计算平均值进行压缩
|
|
||||||
const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0);
|
|
||||||
const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0);
|
|
||||||
const avgTimestamp =
|
|
||||||
this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) /
|
|
||||||
this.compressionQueue.length;
|
|
||||||
|
|
||||||
const compressedPoint: ICompressedDataPoint = {
|
|
||||||
up: totalUp / this.compressionQueue.length,
|
|
||||||
down: totalDown / this.compressionQueue.length,
|
|
||||||
timestamp: avgTimestamp,
|
|
||||||
samples: this.compressionQueue.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.compressedBuffer.push(compressedPoint);
|
|
||||||
this.compressionQueue = [];
|
|
||||||
|
|
||||||
console.log(`[DataSampler] 压缩了 ${compressedPoint.samples} 个数据点`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataForTimeRange(minutes: number): ITrafficDataPoint[] {
|
|
||||||
const cutoff = Date.now() - minutes * 60 * 1000;
|
|
||||||
|
|
||||||
// 如果请求的时间范围在原始数据范围内,直接返回原始数据
|
|
||||||
if (minutes <= this.config.rawDataMinutes) {
|
|
||||||
return this.rawBuffer.filter((p) => p.timestamp > cutoff);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 否则组合原始数据和压缩数据
|
|
||||||
const rawData = this.rawBuffer.filter((p) => p.timestamp > cutoff);
|
|
||||||
const compressedData = this.compressedBuffer
|
|
||||||
.filter(
|
|
||||||
(p) =>
|
|
||||||
p.timestamp > cutoff &&
|
|
||||||
p.timestamp <= Date.now() - this.config.rawDataMinutes * 60 * 1000,
|
|
||||||
)
|
|
||||||
.map((p) => ({
|
|
||||||
up: p.up,
|
|
||||||
down: p.down,
|
|
||||||
timestamp: p.timestamp,
|
|
||||||
name: new Date(p.timestamp).toLocaleTimeString("en-US", {
|
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [...compressedData, ...rawData].sort(
|
|
||||||
(a, b) => a.timestamp - b.timestamp,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getStats() {
|
|
||||||
return {
|
|
||||||
rawBufferSize: this.rawBuffer.length,
|
|
||||||
compressedBufferSize: this.compressedBuffer.length,
|
|
||||||
compressionQueueSize: this.compressionQueue.length,
|
|
||||||
totalMemoryPoints: this.rawBuffer.length + this.compressedBuffer.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.rawBuffer = [];
|
|
||||||
this.compressedBuffer = [];
|
|
||||||
this.compressionQueue = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局单例
|
|
||||||
const refCounter = new ReferenceCounter();
|
|
||||||
let globalSampler: TrafficDataSampler | null = null;
|
|
||||||
let lastValidData: ISystemMonitorOverview | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增强的流量监控Hook - 支持数据压缩、采样和引用计数
|
|
||||||
*/
|
|
||||||
export const useTrafficMonitorEnhanced = () => {
|
|
||||||
const { clashInfo } = useClashInfo();
|
|
||||||
const pageVisible = useVisibility();
|
|
||||||
|
|
||||||
// 初始化采样器
|
|
||||||
if (!globalSampler) {
|
|
||||||
globalSampler = new TrafficDataSampler({
|
|
||||||
rawDataMinutes: 10, // 原始数据保持10分钟
|
|
||||||
compressedDataMinutes: 60, // 压缩数据保持1小时
|
|
||||||
compressionRatio: 5, // 每5个原始点压缩成1个
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, forceUpdate] = useState({});
|
|
||||||
const cleanupRef = useRef<(() => void) | null>(null);
|
|
||||||
|
|
||||||
// 强制组件更新
|
|
||||||
const triggerUpdate = useCallback(() => {
|
|
||||||
forceUpdate({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 注册引用计数
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("[TrafficMonitorEnhanced] 组件挂载,注册引用计数");
|
|
||||||
const cleanup = refCounter.increment();
|
|
||||||
cleanupRef.current = cleanup;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
console.log("[TrafficMonitorEnhanced] 组件卸载,清理引用计数");
|
|
||||||
cleanup();
|
|
||||||
cleanupRef.current = null;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 设置引用计数变化回调
|
|
||||||
useEffect(() => {
|
|
||||||
const handleCountChange = () => {
|
|
||||||
console.log(
|
|
||||||
`[TrafficMonitorEnhanced] 引用计数变化: ${refCounter.getCount()}`,
|
|
||||||
);
|
|
||||||
if (refCounter.getCount() === 0) {
|
|
||||||
console.log("[TrafficMonitorEnhanced] 所有组件已卸载,暂停数据收集");
|
|
||||||
} else {
|
|
||||||
console.log("[TrafficMonitorEnhanced] 开始数据收集");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
refCounter.onCountChange(handleCountChange);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 只有在有引用时才启用SWR
|
|
||||||
const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0;
|
|
||||||
|
|
||||||
const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
|
|
||||||
shouldFetch ? "getSystemMonitorOverviewSafe" : null,
|
|
||||||
getSystemMonitorOverviewSafe,
|
|
||||||
{
|
|
||||||
refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新
|
|
||||||
keepPreviousData: true,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
// console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data);
|
|
||||||
|
|
||||||
if (data?.traffic?.raw && globalSampler) {
|
|
||||||
// 保存最后有效数据
|
|
||||||
lastValidData = data;
|
|
||||||
|
|
||||||
// 添加到采样器
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const dataPoint: ITrafficDataPoint = {
|
|
||||||
up: data.traffic.raw.up_rate || 0,
|
|
||||||
down: data.traffic.raw.down_rate || 0,
|
|
||||||
timestamp,
|
|
||||||
name: new Date(timestamp).toLocaleTimeString("en-US", {
|
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
globalSampler.addDataPoint(dataPoint);
|
|
||||||
triggerUpdate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error(
|
|
||||||
"[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:",
|
|
||||||
{
|
|
||||||
message: error?.message || "未知错误",
|
|
||||||
stack: error?.stack || "无堆栈信息",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// 网络错误时不清空数据,继续使用最后有效值
|
|
||||||
// 但是添加一个错误标记的数据点(流量为0)
|
|
||||||
if (globalSampler) {
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const errorPoint: ITrafficDataPoint = {
|
|
||||||
up: 0,
|
|
||||||
down: 0,
|
|
||||||
timestamp,
|
|
||||||
name: new Date(timestamp).toLocaleTimeString("en-US", {
|
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
globalSampler.addDataPoint(errorPoint);
|
|
||||||
triggerUpdate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 获取指定时间范围的数据
|
|
||||||
const getDataForTimeRange = useCallback(
|
|
||||||
(minutes: number): ITrafficDataPoint[] => {
|
|
||||||
if (!globalSampler) return [];
|
|
||||||
return globalSampler.getDataForTimeRange(minutes);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清空数据
|
|
||||||
const clearData = useCallback(() => {
|
|
||||||
if (globalSampler) {
|
|
||||||
globalSampler.clear();
|
|
||||||
triggerUpdate();
|
|
||||||
}
|
|
||||||
}, [triggerUpdate]);
|
|
||||||
|
|
||||||
// 获取采样器统计信息
|
|
||||||
const getSamplerStats = useCallback(() => {
|
|
||||||
return (
|
|
||||||
globalSampler?.getStats() || {
|
|
||||||
rawBufferSize: 0,
|
|
||||||
compressedBufferSize: 0,
|
|
||||||
compressionQueueSize: 0,
|
|
||||||
totalMemoryPoints: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据
|
|
||||||
const currentData = monitorData || lastValidData;
|
|
||||||
const trafficMonitorData = {
|
|
||||||
traffic: currentData?.traffic || {
|
|
||||||
raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 },
|
|
||||||
formatted: {
|
|
||||||
up_rate: "0B",
|
|
||||||
down_rate: "0B",
|
|
||||||
total_up: "0B",
|
|
||||||
total_down: "0B",
|
|
||||||
},
|
|
||||||
is_fresh: false,
|
|
||||||
},
|
|
||||||
memory: currentData?.memory || {
|
|
||||||
raw: { inuse: 0, oslimit: 0, usage_percent: 0 },
|
|
||||||
formatted: { inuse: "0B", oslimit: "0B", usage_percent: 0 },
|
|
||||||
is_fresh: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 监控数据
|
|
||||||
monitorData: trafficMonitorData,
|
|
||||||
|
|
||||||
// 图表数据管理
|
|
||||||
graphData: {
|
|
||||||
dataPoints: globalSampler?.getDataForTimeRange(60) || [], // 默认获取1小时数据
|
|
||||||
getDataForTimeRange,
|
|
||||||
clearData,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 状态信息
|
|
||||||
isLoading: !currentData && !error,
|
|
||||||
error,
|
|
||||||
isDataFresh: currentData?.traffic?.is_fresh || false,
|
|
||||||
hasValidData: !!lastValidData,
|
|
||||||
|
|
||||||
// 性能统计
|
|
||||||
samplerStats: getSamplerStats(),
|
|
||||||
referenceCount: refCounter.getCount(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 轻量级流量数据Hook
|
|
||||||
*/
|
|
||||||
export const useTrafficDataEnhanced = () => {
|
|
||||||
const { monitorData, isLoading, error, isDataFresh, hasValidData } =
|
|
||||||
useTrafficMonitorEnhanced();
|
|
||||||
|
|
||||||
return {
|
|
||||||
traffic: monitorData.traffic,
|
|
||||||
memory: monitorData.memory,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
isDataFresh,
|
|
||||||
hasValidData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图表数据Hook
|
|
||||||
*/
|
|
||||||
export const useTrafficGraphDataEnhanced = () => {
|
|
||||||
const { graphData, isDataFresh, samplerStats, referenceCount } =
|
|
||||||
useTrafficMonitorEnhanced();
|
|
||||||
|
|
||||||
return {
|
|
||||||
...graphData,
|
|
||||||
isDataFresh,
|
|
||||||
samplerStats,
|
|
||||||
referenceCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -2,9 +2,9 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
import { useClashInfo } from "@/hooks/use-clash";
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
import { useVisibility } from "@/hooks/use-visibility";
|
||||||
import { getSystemMonitorOverview } from "@/services/cmds";
|
import { getSystemMonitorOverviewSafe } from "@/services/cmds";
|
||||||
|
|
||||||
// 流量数据项接口
|
// 增强的流量数据点接口
|
||||||
export interface ITrafficDataPoint {
|
export interface ITrafficDataPoint {
|
||||||
up: number;
|
up: number;
|
||||||
down: number;
|
down: number;
|
||||||
@@ -12,215 +12,365 @@ export interface ITrafficDataPoint {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流量监控数据接口
|
// 压缩的数据点(用于长期存储)
|
||||||
export interface ITrafficMonitorData {
|
interface ICompressedDataPoint {
|
||||||
traffic: {
|
|
||||||
raw: { up_rate: number; down_rate: number };
|
|
||||||
formatted: { up_rate: string; down_rate: string };
|
|
||||||
is_fresh: boolean;
|
|
||||||
};
|
|
||||||
memory: {
|
|
||||||
raw: { inuse: number; oslimit?: number };
|
|
||||||
formatted: { inuse: string; usage_percent?: number };
|
|
||||||
is_fresh: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图表数据管理接口
|
|
||||||
export interface ITrafficGraphData {
|
|
||||||
dataPoints: ITrafficDataPoint[];
|
|
||||||
addDataPoint: (data: {
|
|
||||||
up: number;
|
up: number;
|
||||||
down: number;
|
down: number;
|
||||||
timestamp?: number;
|
timestamp: number;
|
||||||
}) => void;
|
samples: number; // 压缩了多少个原始数据点
|
||||||
clearData: () => void;
|
|
||||||
getDataForTimeRange: (minutes: number) => ITrafficDataPoint[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据采样器配置
|
||||||
|
interface ISamplingConfig {
|
||||||
|
// 原始数据保持时间(分钟)
|
||||||
|
rawDataMinutes: number;
|
||||||
|
// 压缩数据保持时间(分钟)
|
||||||
|
compressedDataMinutes: number;
|
||||||
|
// 压缩比例(多少个原始点压缩成1个)
|
||||||
|
compressionRatio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用计数管理器
|
||||||
|
class ReferenceCounter {
|
||||||
|
private count = 0;
|
||||||
|
private callbacks: (() => void)[] = [];
|
||||||
|
|
||||||
|
increment(): () => void {
|
||||||
|
this.count++;
|
||||||
|
console.log(`[ReferenceCounter] 引用计数增加: ${this.count}`);
|
||||||
|
|
||||||
|
if (this.count === 1) {
|
||||||
|
// 从0到1,开始数据收集
|
||||||
|
this.callbacks.forEach((cb) => cb());
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.count--;
|
||||||
|
console.log(`[ReferenceCounter] 引用计数减少: ${this.count}`);
|
||||||
|
|
||||||
|
if (this.count === 0) {
|
||||||
|
// 从1到0,停止数据收集
|
||||||
|
this.callbacks.forEach((cb) => cb());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onCountChange(callback: () => void) {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCount(): number {
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能数据采样器
|
||||||
|
class TrafficDataSampler {
|
||||||
|
private rawBuffer: ITrafficDataPoint[] = [];
|
||||||
|
private compressedBuffer: ICompressedDataPoint[] = [];
|
||||||
|
private config: ISamplingConfig;
|
||||||
|
private compressionQueue: ITrafficDataPoint[] = [];
|
||||||
|
|
||||||
|
constructor(config: ISamplingConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDataPoint(point: ITrafficDataPoint): void {
|
||||||
|
// 添加到原始缓冲区
|
||||||
|
this.rawBuffer.push(point);
|
||||||
|
|
||||||
|
// 清理过期的原始数据
|
||||||
|
const rawCutoff = Date.now() - this.config.rawDataMinutes * 60 * 1000;
|
||||||
|
this.rawBuffer = this.rawBuffer.filter((p) => p.timestamp > rawCutoff);
|
||||||
|
|
||||||
|
// 添加到压缩队列
|
||||||
|
this.compressionQueue.push(point);
|
||||||
|
|
||||||
|
// 当压缩队列达到压缩比例时,执行压缩
|
||||||
|
if (this.compressionQueue.length >= this.config.compressionRatio) {
|
||||||
|
this.compressData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理过期的压缩数据
|
||||||
|
const compressedCutoff =
|
||||||
|
Date.now() - this.config.compressedDataMinutes * 60 * 1000;
|
||||||
|
this.compressedBuffer = this.compressedBuffer.filter(
|
||||||
|
(p) => p.timestamp > compressedCutoff,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private compressData(): void {
|
||||||
|
if (this.compressionQueue.length === 0) return;
|
||||||
|
|
||||||
|
// 计算平均值进行压缩
|
||||||
|
const totalUp = this.compressionQueue.reduce((sum, p) => sum + p.up, 0);
|
||||||
|
const totalDown = this.compressionQueue.reduce((sum, p) => sum + p.down, 0);
|
||||||
|
const avgTimestamp =
|
||||||
|
this.compressionQueue.reduce((sum, p) => sum + p.timestamp, 0) /
|
||||||
|
this.compressionQueue.length;
|
||||||
|
|
||||||
|
const compressedPoint: ICompressedDataPoint = {
|
||||||
|
up: totalUp / this.compressionQueue.length,
|
||||||
|
down: totalDown / this.compressionQueue.length,
|
||||||
|
timestamp: avgTimestamp,
|
||||||
|
samples: this.compressionQueue.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.compressedBuffer.push(compressedPoint);
|
||||||
|
this.compressionQueue = [];
|
||||||
|
|
||||||
|
console.log(`[DataSampler] 压缩了 ${compressedPoint.samples} 个数据点`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataForTimeRange(minutes: number): ITrafficDataPoint[] {
|
||||||
|
const cutoff = Date.now() - minutes * 60 * 1000;
|
||||||
|
|
||||||
|
// 如果请求的时间范围在原始数据范围内,直接返回原始数据
|
||||||
|
if (minutes <= this.config.rawDataMinutes) {
|
||||||
|
return this.rawBuffer.filter((p) => p.timestamp > cutoff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则组合原始数据和压缩数据
|
||||||
|
const rawData = this.rawBuffer.filter((p) => p.timestamp > cutoff);
|
||||||
|
const compressedData = this.compressedBuffer
|
||||||
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
p.timestamp > cutoff &&
|
||||||
|
p.timestamp <= Date.now() - this.config.rawDataMinutes * 60 * 1000,
|
||||||
|
)
|
||||||
|
.map((p) => ({
|
||||||
|
up: p.up,
|
||||||
|
down: p.down,
|
||||||
|
timestamp: p.timestamp,
|
||||||
|
name: new Date(p.timestamp).toLocaleTimeString("en-US", {
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...compressedData, ...rawData].sort(
|
||||||
|
(a, b) => a.timestamp - b.timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats() {
|
||||||
|
return {
|
||||||
|
rawBufferSize: this.rawBuffer.length,
|
||||||
|
compressedBufferSize: this.compressedBuffer.length,
|
||||||
|
compressionQueueSize: this.compressionQueue.length,
|
||||||
|
totalMemoryPoints: this.rawBuffer.length + this.compressedBuffer.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.rawBuffer = [];
|
||||||
|
this.compressedBuffer = [];
|
||||||
|
this.compressionQueue = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局单例
|
||||||
|
const refCounter = new ReferenceCounter();
|
||||||
|
let globalSampler: TrafficDataSampler | null = null;
|
||||||
|
let lastValidData: ISystemMonitorOverview | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局流量监控数据管理Hook
|
* 增强的流量监控Hook - 支持数据压缩、采样和引用计数
|
||||||
* 提供统一的流量数据获取和图表数据管理
|
|
||||||
*/
|
*/
|
||||||
export const useTrafficMonitor = () => {
|
export const useTrafficMonitorEnhanced = () => {
|
||||||
const { clashInfo } = useClashInfo();
|
const { clashInfo } = useClashInfo();
|
||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
|
|
||||||
// 图表数据缓冲区 - 使用ref保持数据持久性
|
// 初始化采样器
|
||||||
const dataBufferRef = useRef<ITrafficDataPoint[]>([]);
|
if (!globalSampler) {
|
||||||
const [, forceUpdate] = useState({});
|
globalSampler = new TrafficDataSampler({
|
||||||
|
rawDataMinutes: 10, // 原始数据保持10分钟
|
||||||
|
compressedDataMinutes: 60, // 压缩数据保持1小时
|
||||||
|
compressionRatio: 5, // 每5个原始点压缩成1个
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 强制组件更新的函数
|
const [, forceUpdate] = useState({});
|
||||||
|
const cleanupRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
|
// 强制组件更新
|
||||||
const triggerUpdate = useCallback(() => {
|
const triggerUpdate = useCallback(() => {
|
||||||
forceUpdate({});
|
forceUpdate({});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 最大缓冲区大小 (10分钟 * 60秒 = 600个数据点)
|
// 注册引用计数
|
||||||
const MAX_BUFFER_SIZE = 600;
|
|
||||||
|
|
||||||
// 初始化数据缓冲区
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dataBufferRef.current.length === 0) {
|
console.log("[TrafficMonitorEnhanced] 组件挂载,注册引用计数");
|
||||||
const now = Date.now();
|
const cleanup = refCounter.increment();
|
||||||
const tenMinutesAgo = now - 10 * 60 * 1000;
|
cleanupRef.current = cleanup;
|
||||||
|
|
||||||
const initialBuffer = Array.from(
|
return () => {
|
||||||
{ length: MAX_BUFFER_SIZE },
|
console.log("[TrafficMonitorEnhanced] 组件卸载,清理引用计数");
|
||||||
(_, index) => {
|
cleanup();
|
||||||
const pointTime =
|
cleanupRef.current = null;
|
||||||
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
};
|
||||||
const date = new Date(pointTime);
|
}, []);
|
||||||
|
|
||||||
let nameValue: string;
|
// 设置引用计数变化回调
|
||||||
try {
|
useEffect(() => {
|
||||||
if (isNaN(date.getTime())) {
|
const handleCountChange = () => {
|
||||||
nameValue = "??:??:??";
|
console.log(
|
||||||
|
`[TrafficMonitorEnhanced] 引用计数变化: ${refCounter.getCount()}`,
|
||||||
|
);
|
||||||
|
if (refCounter.getCount() === 0) {
|
||||||
|
console.log("[TrafficMonitorEnhanced] 所有组件已卸载,暂停数据收集");
|
||||||
} else {
|
} else {
|
||||||
nameValue = date.toLocaleTimeString("en-US", {
|
console.log("[TrafficMonitorEnhanced] 开始数据收集");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
refCounter.onCountChange(handleCountChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 只有在有引用时才启用SWR
|
||||||
|
const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0;
|
||||||
|
|
||||||
|
const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
|
||||||
|
shouldFetch ? "getSystemMonitorOverviewSafe" : null,
|
||||||
|
getSystemMonitorOverviewSafe,
|
||||||
|
{
|
||||||
|
refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新
|
||||||
|
keepPreviousData: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data);
|
||||||
|
|
||||||
|
if (data?.traffic?.raw && globalSampler) {
|
||||||
|
// 保存最后有效数据
|
||||||
|
lastValidData = data;
|
||||||
|
|
||||||
|
// 添加到采样器
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const dataPoint: ITrafficDataPoint = {
|
||||||
|
up: data.traffic.raw.up_rate || 0,
|
||||||
|
down: data.traffic.raw.down_rate || 0,
|
||||||
|
timestamp,
|
||||||
|
name: new Date(timestamp).toLocaleTimeString("en-US", {
|
||||||
hour12: false,
|
hour12: false,
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
});
|
}),
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
nameValue = "Err:Time";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
up: 0,
|
|
||||||
down: 0,
|
|
||||||
timestamp: pointTime,
|
|
||||||
name: nameValue,
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
dataBufferRef.current = initialBuffer;
|
globalSampler.addDataPoint(dataPoint);
|
||||||
}
|
triggerUpdate();
|
||||||
}, [MAX_BUFFER_SIZE]);
|
|
||||||
|
|
||||||
// 使用SWR获取监控数据
|
|
||||||
const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
|
|
||||||
clashInfo && pageVisible ? "getSystemMonitorOverview" : null,
|
|
||||||
getSystemMonitorOverview,
|
|
||||||
{
|
|
||||||
refreshInterval: 1000, // 1秒刷新一次
|
|
||||||
keepPreviousData: true,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
console.log("[TrafficMonitor] 获取到监控数据:", data);
|
|
||||||
|
|
||||||
if (data?.traffic) {
|
|
||||||
// 为图表添加新数据点
|
|
||||||
addDataPoint({
|
|
||||||
up: data.traffic.raw.up_rate || 0,
|
|
||||||
down: data.traffic.raw.down_rate || 0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("[TrafficMonitor] 获取数据错误:", error);
|
console.error(
|
||||||
},
|
"[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:",
|
||||||
|
{
|
||||||
|
message: error?.message || "未知错误",
|
||||||
|
stack: error?.stack || "无堆栈信息",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// 网络错误时不清空数据,继续使用最后有效值
|
||||||
// 添加数据点到缓冲区
|
// 但是添加一个错误标记的数据点(流量为0)
|
||||||
const addDataPoint = useCallback(
|
if (globalSampler) {
|
||||||
(data: { up: number; down: number; timestamp?: number }) => {
|
const timestamp = Date.now();
|
||||||
const timestamp = data.timestamp || Date.now();
|
const errorPoint: ITrafficDataPoint = {
|
||||||
const date = new Date(timestamp);
|
up: 0,
|
||||||
|
down: 0,
|
||||||
let nameValue: string;
|
timestamp,
|
||||||
try {
|
name: new Date(timestamp).toLocaleTimeString("en-US", {
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
nameValue = "??:??:??";
|
|
||||||
} else {
|
|
||||||
nameValue = date.toLocaleTimeString("en-US", {
|
|
||||||
hour12: false,
|
hour12: false,
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
});
|
}),
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
nameValue = "Err:Time";
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPoint: ITrafficDataPoint = {
|
|
||||||
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
|
||||||
down:
|
|
||||||
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
|
||||||
timestamp,
|
|
||||||
name: nameValue,
|
|
||||||
};
|
};
|
||||||
|
globalSampler.addDataPoint(errorPoint);
|
||||||
// 更新缓冲区,保持固定大小
|
|
||||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
|
||||||
dataBufferRef.current = newBuffer;
|
|
||||||
|
|
||||||
// 触发使用该数据的组件更新
|
|
||||||
triggerUpdate();
|
triggerUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[triggerUpdate],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 清空数据
|
// 获取指定时间范围的数据
|
||||||
const clearData = useCallback(() => {
|
|
||||||
dataBufferRef.current = [];
|
|
||||||
triggerUpdate();
|
|
||||||
}, [triggerUpdate]);
|
|
||||||
|
|
||||||
// 根据时间范围获取数据
|
|
||||||
const getDataForTimeRange = useCallback(
|
const getDataForTimeRange = useCallback(
|
||||||
(minutes: number): ITrafficDataPoint[] => {
|
(minutes: number): ITrafficDataPoint[] => {
|
||||||
const pointsToShow = minutes * 60; // 每分钟60个数据点
|
if (!globalSampler) return [];
|
||||||
return dataBufferRef.current.slice(-pointsToShow);
|
return globalSampler.getDataForTimeRange(minutes);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 构建图表数据管理对象
|
// 清空数据
|
||||||
const graphData: ITrafficGraphData = {
|
const clearData = useCallback(() => {
|
||||||
dataPoints: dataBufferRef.current,
|
if (globalSampler) {
|
||||||
addDataPoint,
|
globalSampler.clear();
|
||||||
clearData,
|
triggerUpdate();
|
||||||
getDataForTimeRange,
|
}
|
||||||
};
|
}, [triggerUpdate]);
|
||||||
|
|
||||||
// 构建监控数据对象
|
// 获取采样器统计信息
|
||||||
const trafficMonitorData: ITrafficMonitorData = {
|
const getSamplerStats = useCallback(() => {
|
||||||
traffic: monitorData?.traffic || {
|
return (
|
||||||
raw: { up_rate: 0, down_rate: 0 },
|
globalSampler?.getStats() || {
|
||||||
formatted: { up_rate: "0B", down_rate: "0B" },
|
rawBufferSize: 0,
|
||||||
|
compressedBufferSize: 0,
|
||||||
|
compressionQueueSize: 0,
|
||||||
|
totalMemoryPoints: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据
|
||||||
|
const currentData = monitorData || lastValidData;
|
||||||
|
const trafficMonitorData = {
|
||||||
|
traffic: currentData?.traffic || {
|
||||||
|
raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 },
|
||||||
|
formatted: {
|
||||||
|
up_rate: "0B",
|
||||||
|
down_rate: "0B",
|
||||||
|
total_up: "0B",
|
||||||
|
total_down: "0B",
|
||||||
|
},
|
||||||
is_fresh: false,
|
is_fresh: false,
|
||||||
},
|
},
|
||||||
memory: monitorData?.memory || {
|
memory: currentData?.memory || {
|
||||||
raw: { inuse: 0 },
|
raw: { inuse: 0, oslimit: 0, usage_percent: 0 },
|
||||||
formatted: { inuse: "0B" },
|
formatted: { inuse: "0B", oslimit: "0B", usage_percent: 0 },
|
||||||
is_fresh: false,
|
is_fresh: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 原始监控数据
|
// 监控数据
|
||||||
monitorData: trafficMonitorData,
|
monitorData: trafficMonitorData,
|
||||||
|
|
||||||
// 图表数据管理
|
// 图表数据管理
|
||||||
graphData,
|
graphData: {
|
||||||
// 数据获取状态
|
dataPoints: globalSampler?.getDataForTimeRange(60) || [], // 默认获取1小时数据
|
||||||
isLoading: !monitorData && !error,
|
getDataForTimeRange,
|
||||||
|
clearData,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 状态信息
|
||||||
|
isLoading: !currentData && !error,
|
||||||
error,
|
error,
|
||||||
// 数据新鲜度
|
isDataFresh: currentData?.traffic?.is_fresh || false,
|
||||||
isDataFresh: monitorData?.overall_status === "active",
|
hasValidData: !!lastValidData,
|
||||||
|
|
||||||
|
// 性能统计
|
||||||
|
samplerStats: getSamplerStats(),
|
||||||
|
referenceCount: refCounter.getCount(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅获取流量数据的轻量级Hook
|
* 轻量级流量数据Hook
|
||||||
* 适用于不需要图表数据的组件
|
|
||||||
*/
|
*/
|
||||||
export const useTrafficData = () => {
|
export const useTrafficDataEnhanced = () => {
|
||||||
const { monitorData, isLoading, error, isDataFresh } = useTrafficMonitor();
|
const { monitorData, isLoading, error, isDataFresh, hasValidData } =
|
||||||
|
useTrafficMonitorEnhanced();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
traffic: monitorData.traffic,
|
traffic: monitorData.traffic,
|
||||||
@@ -228,18 +378,21 @@ export const useTrafficData = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isDataFresh,
|
isDataFresh,
|
||||||
|
hasValidData,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅获取图表数据的Hook
|
* 图表数据Hook
|
||||||
* 适用于图表组件
|
|
||||||
*/
|
*/
|
||||||
export const useTrafficGraphData = () => {
|
export const useTrafficGraphDataEnhanced = () => {
|
||||||
const { graphData, isDataFresh } = useTrafficMonitor();
|
const { graphData, isDataFresh, samplerStats, referenceCount } =
|
||||||
|
useTrafficMonitorEnhanced();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...graphData,
|
...graphData,
|
||||||
isDataFresh,
|
isDataFresh,
|
||||||
|
samplerStats,
|
||||||
|
referenceCount,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user