feat: replace traffic chart rendering component for performance improvement and React 19 compatibility
This commit is contained in:
@@ -47,6 +47,7 @@
|
|||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.4",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
"d3-shape": "^3.2.0",
|
"d3-shape": "^3.2.0",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"peggy": "^5.0.0",
|
"peggy": "^5.0.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-error-boundary": "6.0.0",
|
"react-error-boundary": "6.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
@@ -69,7 +71,6 @@
|
|||||||
"react-monaco-editor": "0.58.0",
|
"react-monaco-editor": "0.58.0",
|
||||||
"react-router-dom": "7.6.0",
|
"react-router-dom": "7.6.0",
|
||||||
"react-virtuoso": "^4.12.7",
|
"react-virtuoso": "^4.12.7",
|
||||||
"recharts": "^2.15.1",
|
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
|
|||||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -80,6 +80,9 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.8.3
|
specifier: ^1.8.3
|
||||||
version: 1.9.0
|
version: 1.9.0
|
||||||
|
chart.js:
|
||||||
|
specifier: ^4.4.9
|
||||||
|
version: 4.4.9
|
||||||
cli-color:
|
cli-color:
|
||||||
specifier: ^2.0.4
|
specifier: ^2.0.4
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
@@ -122,6 +125,9 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
|
react-chartjs-2:
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0(chart.js@4.4.9)(react@19.1.0)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
@@ -147,7 +153,7 @@ importers:
|
|||||||
specifier: ^4.12.7
|
specifier: ^4.12.7
|
||||||
version: 4.12.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 4.12.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.15.1
|
specifier: ^2.15.3
|
||||||
version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
sockette:
|
sockette:
|
||||||
specifier: ^2.0.6
|
specifier: ^2.0.6
|
||||||
@@ -1006,6 +1012,9 @@ packages:
|
|||||||
'@juggle/resize-observer@3.4.0':
|
'@juggle/resize-observer@3.4.0':
|
||||||
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.4':
|
||||||
|
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||||
|
|
||||||
'@mui/core-downloads-tracker@7.1.0':
|
'@mui/core-downloads-tracker@7.1.0':
|
||||||
resolution: {integrity: sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==}
|
resolution: {integrity: sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==}
|
||||||
|
|
||||||
@@ -1786,6 +1795,10 @@ packages:
|
|||||||
character-reference-invalid@2.0.1:
|
character-reference-invalid@2.0.1:
|
||||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||||
|
|
||||||
|
chart.js@4.4.9:
|
||||||
|
resolution: {integrity: sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==}
|
||||||
|
engines: {pnpm: '>=8'}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
@@ -2615,6 +2628,12 @@ packages:
|
|||||||
proxy-from-env@1.1.0:
|
proxy-from-env@1.1.0:
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
|
react-chartjs-2@5.3.0:
|
||||||
|
resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
|
||||||
|
peerDependencies:
|
||||||
|
chart.js: ^4.1.1
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
react-dom@19.1.0:
|
react-dom@19.1.0:
|
||||||
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4020,6 +4039,8 @@ snapshots:
|
|||||||
|
|
||||||
'@juggle/resize-observer@3.4.0': {}
|
'@juggle/resize-observer@3.4.0': {}
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.4': {}
|
||||||
|
|
||||||
'@mui/core-downloads-tracker@7.1.0': {}
|
'@mui/core-downloads-tracker@7.1.0': {}
|
||||||
|
|
||||||
'@mui/icons-material@7.1.0(@mui/material@7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.4)(react@19.1.0)':
|
'@mui/icons-material@7.1.0(@mui/material@7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.4)(react@19.1.0)':
|
||||||
@@ -4740,6 +4761,10 @@ snapshots:
|
|||||||
|
|
||||||
character-reference-invalid@2.0.1: {}
|
character-reference-invalid@2.0.1: {}
|
||||||
|
|
||||||
|
chart.js@4.4.9:
|
||||||
|
dependencies:
|
||||||
|
'@kurkle/color': 0.3.4
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
@@ -5687,6 +5712,11 @@ snapshots:
|
|||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
|
react-chartjs-2@5.3.0(chart.js@4.4.9)(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
chart.js: 4.4.9
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
react-dom@19.1.0(react@19.1.0):
|
react-dom@19.1.0(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|||||||
@@ -11,17 +11,27 @@ import {
|
|||||||
import { Box, useTheme } from "@mui/material";
|
import { Box, useTheme } from "@mui/material";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Line as ChartJsLine } from "react-chartjs-2";
|
||||||
import {
|
import {
|
||||||
LineChart,
|
Chart as ChartJS,
|
||||||
Line,
|
CategoryScale,
|
||||||
XAxis,
|
LinearScale,
|
||||||
YAxis,
|
PointElement,
|
||||||
CartesianGrid,
|
LineElement,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ResponsiveContainer,
|
Filler,
|
||||||
AreaChart,
|
Scale,
|
||||||
Area,
|
Tick,
|
||||||
} from "recharts";
|
} from "chart.js";
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Tooltip,
|
||||||
|
Filler
|
||||||
|
);
|
||||||
|
|
||||||
// 流量数据项接口
|
// 流量数据项接口
|
||||||
export interface ITrafficItem {
|
export interface ITrafficItem {
|
||||||
@@ -30,13 +40,12 @@ export interface ITrafficItem {
|
|||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件对外暴露的方法
|
// 对外暴露的接口
|
||||||
export interface EnhancedTrafficGraphRef {
|
export interface EnhancedTrafficGraphRef {
|
||||||
appendData: (data: ITrafficItem) => void;
|
appendData: (data: ITrafficItem) => void;
|
||||||
toggleStyle: () => void;
|
toggleStyle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间范围类型
|
|
||||||
type TimeRange = 1 | 5 | 10; // 分钟
|
type TimeRange = 1 | 5 | 10; // 分钟
|
||||||
|
|
||||||
// 数据点类型
|
// 数据点类型
|
||||||
@@ -76,23 +85,30 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
up: theme.palette.secondary.main,
|
up: theme.palette.secondary.main,
|
||||||
down: theme.palette.primary.main,
|
down: theme.palette.primary.main,
|
||||||
grid: theme.palette.divider,
|
grid: theme.palette.divider,
|
||||||
tooltip: theme.palette.background.paper,
|
tooltipBg: theme.palette.background.paper,
|
||||||
text: theme.palette.text.primary,
|
text: theme.palette.text.primary,
|
||||||
|
tooltipBorder: theme.palette.divider,
|
||||||
}),
|
}),
|
||||||
[theme]
|
[theme]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 切换时间范围
|
// 切换时间范围
|
||||||
const handleTimeRangeClick = useCallback(() => {
|
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
setTimeRange((prevRange) => {
|
setTimeRange((prevRange) => {
|
||||||
// 在1、5、10分钟之间循环切换
|
|
||||||
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 点击图表主体或图例时切换样式
|
||||||
|
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// 初始化数据缓冲区
|
// 初始化数据缓冲区
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 创建初始空数据
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const tenMinutesAgo = now - 10 * 60 * 1000;
|
const tenMinutesAgo = now - 10 * 60 * 1000;
|
||||||
|
|
||||||
@@ -102,17 +118,29 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
const pointTime =
|
const pointTime =
|
||||||
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
||||||
const date = new Date(pointTime);
|
const date = new Date(pointTime);
|
||||||
|
let nameValue: string;
|
||||||
|
try {
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`);
|
||||||
|
nameValue = "??:??:??";
|
||||||
|
} else {
|
||||||
|
nameValue = date.toLocaleTimeString("en-US", {
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime);
|
||||||
|
nameValue = "Err:Time";
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
up: 0,
|
up: 0,
|
||||||
down: 0,
|
down: 0,
|
||||||
timestamp: pointTime,
|
timestamp: pointTime,
|
||||||
name: date.toLocaleTimeString("en-US", {
|
name: nameValue,
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -122,45 +150,54 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
// 更新显示数据
|
// 更新显示数据
|
||||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||||
setDisplayData(initialBuffer.slice(-pointsToShow));
|
setDisplayData(initialBuffer.slice(-pointsToShow));
|
||||||
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
|
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]);
|
||||||
|
|
||||||
// 添加数据点方法
|
// 添加数据点方法
|
||||||
const appendData = useCallback((data: ITrafficItem) => {
|
const appendData = useCallback((data: ITrafficItem) => {
|
||||||
// 安全处理数据
|
|
||||||
const safeData = {
|
const safeData = {
|
||||||
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
||||||
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用提供的时间戳或当前时间
|
|
||||||
const timestamp = data.timestamp || Date.now();
|
const timestamp = data.timestamp || Date.now();
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
|
|
||||||
|
let nameValue: string;
|
||||||
|
try {
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
|
||||||
|
nameValue = "??:??:??";
|
||||||
|
} else {
|
||||||
|
nameValue = date.toLocaleTimeString("en-US", {
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp);
|
||||||
|
nameValue = "Err:Time";
|
||||||
|
}
|
||||||
// 带时间标签的新数据点
|
// 带时间标签的新数据点
|
||||||
const newPoint: DataPoint = {
|
const newPoint: DataPoint = {
|
||||||
...safeData,
|
...safeData,
|
||||||
name: date.toLocaleTimeString("en-US", {
|
name: nameValue,
|
||||||
hour12: false,
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}),
|
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新缓冲区,保持原数组大小
|
|
||||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||||
dataBufferRef.current = newBuffer;
|
dataBufferRef.current = newBuffer;
|
||||||
|
|
||||||
// 更新显示数据
|
|
||||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||||
setDisplayData(newBuffer.slice(-pointsToShow));
|
setDisplayData(newBuffer.slice(-pointsToShow));
|
||||||
}, [timeRange, getMaxPointsByTimeRange]);
|
}, [timeRange, getMaxPointsByTimeRange]);
|
||||||
|
|
||||||
// 监听时间范围变化,更新显示数据
|
// 监听时间范围变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||||
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
|
if (dataBufferRef.current.length > 0) {
|
||||||
|
setDisplayData(dataBufferRef.current.slice(-pointsToShow));
|
||||||
|
}
|
||||||
}, [timeRange, getMaxPointsByTimeRange]);
|
}, [timeRange, getMaxPointsByTimeRange]);
|
||||||
|
|
||||||
// 切换图表样式
|
// 切换图表样式
|
||||||
@@ -178,47 +215,166 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
[appendData, toggleStyle]
|
[appendData, toggleStyle]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 格式化工具提示内容
|
|
||||||
const formatTooltip = useCallback((value: number, name: string, props: any) => {
|
|
||||||
const [num, unit] = parseTraffic(value);
|
|
||||||
return [`${num} ${unit}/s`, props?.dataKey === "up" ? t("Upload") : t("Download")];
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
// Y轴刻度格式化
|
const formatYAxis = useCallback((value: number | string): string => {
|
||||||
const formatYAxis = useCallback((value: number) => {
|
if (typeof value !== 'number') return String(value);
|
||||||
const [num, unit] = parseTraffic(value);
|
const [num, unit] = parseTraffic(value);
|
||||||
return `${num}${unit}`;
|
return `${num}${unit}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 格式化X轴标签
|
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => {
|
||||||
const formatXLabel = useCallback((value: string) => {
|
const dataPoint = displayData[index as number];
|
||||||
if (!value) return "";
|
if (dataPoint && dataPoint.name) {
|
||||||
const parts = value.split(":");
|
const parts = dataPoint.name.split(":");
|
||||||
return `${parts[0]}:${parts[1]}`;
|
return `${parts[0]}:${parts[1]}`;
|
||||||
}, []);
|
}
|
||||||
|
if(typeof tickValue === 'string') {
|
||||||
|
const parts = tickValue.split(":");
|
||||||
|
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
|
||||||
|
return tickValue;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}, [displayData]);
|
||||||
|
|
||||||
|
|
||||||
// 获取当前时间范围文本
|
// 获取当前时间范围文本
|
||||||
const getTimeRangeText = useCallback(() => {
|
const getTimeRangeText = useCallback(() => {
|
||||||
return t("{{time}} Minutes", { time: timeRange });
|
return t("{{time}} Minutes", { time: timeRange });
|
||||||
}, [timeRange, t]);
|
}, [timeRange, t]);
|
||||||
|
|
||||||
// 共享图表配置
|
const chartData = useMemo(() => {
|
||||||
const chartConfig = useMemo(() => ({
|
const labels = displayData.map(d => d.name);
|
||||||
data: displayData,
|
return {
|
||||||
margin: { top: 20, right: 10, left: 0, bottom: -10 },
|
labels,
|
||||||
}), [displayData]);
|
datasets: [
|
||||||
|
{
|
||||||
|
label: t("Upload"),
|
||||||
|
data: displayData.map(d => d.up),
|
||||||
|
borderColor: colors.up,
|
||||||
|
backgroundColor: chartStyle === "area" ? colors.up : colors.up,
|
||||||
|
fill: chartStyle === "area",
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 4,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("Download"),
|
||||||
|
data: displayData.map(d => d.down),
|
||||||
|
borderColor: colors.down,
|
||||||
|
backgroundColor: chartStyle === "area" ? colors.down : colors.down,
|
||||||
|
fill: chartStyle === "area",
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 4,
|
||||||
|
borderWidth: 2,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}, [displayData, colors.up, colors.down, t, chartStyle]);
|
||||||
|
|
||||||
// 共享的线条/区域配置
|
const chartOptions = useMemo(() => ({
|
||||||
const commonLineProps = useMemo(() => ({
|
responsive: true,
|
||||||
dot: false,
|
maintainAspectRatio: false,
|
||||||
strokeWidth: 2,
|
animation: false as false,
|
||||||
connectNulls: false,
|
scales: {
|
||||||
activeDot: { r: 4, strokeWidth: 1 },
|
x: {
|
||||||
isAnimationActive: false, // 禁用动画以减少CPU使用
|
display: true,
|
||||||
}), []);
|
type: 'category' as const,
|
||||||
|
labels: displayData.map(d => d.name),
|
||||||
|
ticks: {
|
||||||
|
display: true,
|
||||||
|
color: colors.text,
|
||||||
|
font: { size: 10 },
|
||||||
|
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined {
|
||||||
|
let labelToFormat: string | undefined = undefined;
|
||||||
|
|
||||||
|
const currentDisplayTick = ticks[index];
|
||||||
|
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') {
|
||||||
|
labelToFormat = currentDisplayTick.label;
|
||||||
|
} else {
|
||||||
|
const sourceLabels = displayData.map(d => d.name);
|
||||||
|
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) {
|
||||||
|
labelToFormat = sourceLabels[tickValue];
|
||||||
|
} else if (typeof tickValue === 'string') {
|
||||||
|
labelToFormat = tickValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof labelToFormat !== 'string') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = labelToFormat.split(':');
|
||||||
|
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat;
|
||||||
|
},
|
||||||
|
autoSkip: true,
|
||||||
|
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
|
||||||
|
minRotation: 0,
|
||||||
|
maxRotation: 0,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawOnChartArea: false,
|
||||||
|
drawTicks: true,
|
||||||
|
tickLength: 2,
|
||||||
|
color: colors.text,
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
color: colors.text,
|
||||||
|
font: { size: 10 },
|
||||||
|
callback: formatYAxis,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawTicks: true,
|
||||||
|
tickLength: 3,
|
||||||
|
color: colors.grid,
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'index' as const,
|
||||||
|
intersect: false,
|
||||||
|
backgroundColor: colors.tooltipBg,
|
||||||
|
titleColor: colors.text,
|
||||||
|
bodyColor: colors.text,
|
||||||
|
borderColor: colors.tooltipBorder,
|
||||||
|
borderWidth: 1,
|
||||||
|
cornerRadius: 4,
|
||||||
|
padding: 8,
|
||||||
|
callbacks: {
|
||||||
|
title: (tooltipItems: any[]) => {
|
||||||
|
return `${t("Time")}: ${tooltipItems[0].label}`;
|
||||||
|
},
|
||||||
|
label: (context: any): string => {
|
||||||
|
const label = context.dataset.label || '';
|
||||||
|
const value = context.parsed.y;
|
||||||
|
const [num, unit] = parseTraffic(value);
|
||||||
|
return `${label}: ${num} ${unit}/s`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
top: 16,
|
||||||
|
right: 7,
|
||||||
|
left: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), [colors, t, formatYAxis, timeRange, displayData]);
|
||||||
|
|
||||||
// 曲线类型
|
|
||||||
const curveType = "monotone";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -230,141 +386,57 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
|||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={toggleStyle}
|
onClick={handleToggleStyleClick}
|
||||||
>
|
>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<div style={{ width: "100%", height: "100%", position: "relative" }}>
|
||||||
{/* 根据chartStyle动态选择图表类型 */}
|
{displayData.length > 0 && (
|
||||||
{(() => {
|
<ChartJsLine data={chartData} options={chartOptions} />
|
||||||
// 创建共享的图表组件
|
)}
|
||||||
const commonChartComponents = (
|
|
||||||
<>
|
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
<text
|
||||||
<XAxis
|
x="3.5%"
|
||||||
dataKey="name"
|
y="10%"
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
textAnchor="start"
|
||||||
tickLine={{ stroke: colors.grid }}
|
fill={theme.palette.text.secondary}
|
||||||
axisLine={{ stroke: colors.grid }}
|
fontSize={11}
|
||||||
interval="preserveStart"
|
fontWeight="bold"
|
||||||
tickFormatter={formatXLabel}
|
onClick={handleTimeRangeClick}
|
||||||
minTickGap={30}
|
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||||
/>
|
>
|
||||||
<YAxis
|
{getTimeRangeText()}
|
||||||
tickFormatter={formatYAxis}
|
</text>
|
||||||
tick={{ fontSize: 10, fill: colors.text }}
|
|
||||||
tickLine={{ stroke: colors.grid }}
|
<text
|
||||||
axisLine={{ stroke: colors.grid }}
|
x="99%"
|
||||||
width={44}
|
y="10%"
|
||||||
domain={[0, "auto"]}
|
textAnchor="end"
|
||||||
padding={{ top: 8, bottom: 0 }}
|
fill={colors.up}
|
||||||
/>
|
fontSize={12}
|
||||||
<Tooltip
|
fontWeight="bold"
|
||||||
formatter={formatTooltip}
|
onClick={handleToggleStyleClick}
|
||||||
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||||
contentStyle={{
|
>
|
||||||
backgroundColor: colors.tooltip,
|
{t("Upload")}
|
||||||
borderColor: colors.grid,
|
</text>
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
itemStyle={{ color: colors.text }}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 可点击的时间范围标签 */}
|
|
||||||
<text
|
|
||||||
x="1%"
|
|
||||||
y="11%"
|
|
||||||
textAnchor="start"
|
|
||||||
fill={theme.palette.text.secondary}
|
|
||||||
fontSize={11}
|
|
||||||
fontWeight="bold"
|
|
||||||
onClick={handleTimeRangeClick}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
{getTimeRangeText()}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
{/* 上传标签 - 右上角 */}
|
|
||||||
<text
|
|
||||||
x="99%"
|
|
||||||
y="11%"
|
|
||||||
textAnchor="end"
|
|
||||||
fill={colors.up}
|
|
||||||
fontSize={12}
|
|
||||||
fontWeight="bold"
|
|
||||||
onClick={toggleStyle}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
{t("Upload")}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
{/* 下载标签 - 右上角下方 */}
|
<text
|
||||||
<text
|
x="99%"
|
||||||
x="99%"
|
y="19%"
|
||||||
y="19%"
|
textAnchor="end"
|
||||||
textAnchor="end"
|
fill={colors.down}
|
||||||
fill={colors.down}
|
fontSize={12}
|
||||||
fontSize={12}
|
fontWeight="bold"
|
||||||
fontWeight="bold"
|
onClick={handleToggleStyleClick}
|
||||||
onClick={toggleStyle}
|
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||||
style={{ cursor: "pointer" }}
|
>
|
||||||
>
|
{t("Download")}
|
||||||
{t("Download")}
|
</text>
|
||||||
</text>
|
</svg>
|
||||||
</>
|
</div>
|
||||||
);
|
|
||||||
|
|
||||||
// 根据chartStyle返回相应的图表类型
|
|
||||||
if (chartStyle === "line") {
|
|
||||||
return (
|
|
||||||
<LineChart {...chartConfig}>
|
|
||||||
{commonChartComponents}
|
|
||||||
<Line
|
|
||||||
type={curveType}
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="up"
|
|
||||||
name={t("Upload")}
|
|
||||||
stroke={colors.up}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type={curveType}
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="down"
|
|
||||||
name={t("Download")}
|
|
||||||
stroke={colors.down}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<AreaChart {...chartConfig}>
|
|
||||||
{commonChartComponents}
|
|
||||||
<Area
|
|
||||||
type={curveType}
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="up"
|
|
||||||
name={t("Upload")}
|
|
||||||
stroke={colors.up}
|
|
||||||
fill={colors.up}
|
|
||||||
fillOpacity={0.2}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type={curveType}
|
|
||||||
{...commonLineProps}
|
|
||||||
dataKey="down"
|
|
||||||
name={t("Download")}
|
|
||||||
stroke={colors.down}
|
|
||||||
fill={colors.down}
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
// 添加显示名称以便调试
|
|
||||||
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
|
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
|
||||||
|
|||||||
Reference in New Issue
Block a user