From 8b3e7b8f40126938ed17425fcb2598e495b1fbaa Mon Sep 17 00:00:00 2001 From: Montia37 Date: Tue, 9 Sep 2025 16:01:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=9B=BE=E8=A1=A8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.css | 2 + src/pages/instance/LoadCharts.tsx | 47 +++-------- src/pages/instance/PingChart.tsx | 126 +++++++++++++++++------------- src/utils/chartHelper.ts | 77 ++++++++++++++++++ 4 files changed, 161 insertions(+), 91 deletions(-) create mode 100644 src/utils/chartHelper.ts diff --git a/src/index.css b/src/index.css index 9e96740..3258a13 100644 --- a/src/index.css +++ b/src/index.css @@ -132,10 +132,12 @@ @layer utilities { .dark .radix-themes { --theme-text-muted-color: rgb(from var(--accent-4) r g b / 0.8); + --theme-line-muted-color: rgb(from var(--accent-2) r g b / 0.5); } .radix-themes { --theme-text-muted-color: rgb(from var(--accent-12) r g b / 0.8); + --theme-line-muted-color: rgb(from var(--accent-10) r g b / 0.5); } .dark .rt-Badge-tag-transparent { diff --git a/src/pages/instance/LoadCharts.tsx b/src/pages/instance/LoadCharts.tsx index 732517a..dcd105b 100644 --- a/src/pages/instance/LoadCharts.tsx +++ b/src/pages/instance/LoadCharts.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useRef } from "react"; +import { memo, useRef } from "react"; import { AreaChart, Area, @@ -17,6 +17,7 @@ import { Flex } from "@radix-ui/themes"; import Loading from "@/components/loading"; import { useLoadCharts } from "@/hooks/useLoadCharts"; import { CustomTooltip } from "@/components/ui/tooltip"; +import { lableFormatter, loadChartTimeFormatter } from "@/utils/chartHelper"; interface LoadChartsProps { node: NodeData; @@ -33,38 +34,6 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => { const chartDataLengthRef = useRef(0); chartDataLengthRef.current = chartData.length; - // 格式化函数 - const timeFormatter = useCallback((value: any, index: number) => { - if (chartDataLengthRef.current === 0) return ""; - if (index === 0 || index === chartDataLengthRef.current - 1) { - return new Date(value).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); - } - return ""; - }, []); - - const labelFormatter = useCallback( - (value: any) => { - const date = new Date(value); - if (hours === 0) { - return date.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - return date.toLocaleString([], { - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }); - }, - [hours] - ); - // 样式和颜色 const cn = "flex flex-col w-full overflow-hidden"; const chartMargin = { top: 8, right: 16, bottom: 8, left: 16 }; @@ -262,7 +231,7 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => { {...chartProps}> { tick={{ fill: "var(--theme-text-muted-color)", }} - tickFormatter={timeFormatter} + tickFormatter={(value, index) => + loadChartTimeFormatter( + value, + index, + chartDataLengthRef.current + ) + } interval={0} height={20} /> @@ -298,7 +273,7 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => { lableFormatter(value, hours)} /> )} /> diff --git a/src/pages/instance/PingChart.tsx b/src/pages/instance/PingChart.tsx index 6c48a39..152ba83 100644 --- a/src/pages/instance/PingChart.tsx +++ b/src/pages/instance/PingChart.tsx @@ -1,7 +1,7 @@ -import { memo, useState, useMemo, useCallback, useEffect } from "react"; +import { memo, useState, useMemo, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { useIsMobile } from "@/hooks/useMobile"; -import { Eye, EyeOff } from "lucide-react"; +import { Eye, EyeOff, ArrowRightToLine, RefreshCw } from "lucide-react"; import { LineChart, Line, @@ -26,6 +26,7 @@ import fillMissingTimePoints, { import { useConfigItem } from "@/config"; import { CustomTooltip } from "@/components/ui/tooltip"; import Tips from "@/components/ui/tips"; +import { generateColor, lableFormatter } from "@/utils/chartHelper"; interface PingChartProps { node: NodeData; @@ -36,10 +37,15 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { const { loading, error, pingHistory } = usePingChart(node, hours); const [visiblePingTasks, setVisiblePingTasks] = useState([]); const [timeRange, setTimeRange] = useState<[number, number] | null>(null); + const [brushIndices, setBrushIndices] = useState<{ + startIndex?: number; + endIndex?: number; + }>({}); const [cutPeak, setCutPeak] = useState(false); const [connectBreaks, setConnectBreaks] = useState( useConfigItem("enableConnectBreaks") ); + const [isResetting, setIsResetting] = useState(false); const maxPointsToRender = useConfigItem("pingChartMaxPoints") || 0; // 0表示不限制 const isMobile = useIsMobile(); @@ -55,25 +61,13 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { } }, [pingHistory?.tasks]); - const lableFormatter = useCallback( - (value: any) => { - const date = new Date(value); - if (hours === 0) { - return date.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - return date.toLocaleString([], { - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - }); - }, - [hours] - ); + useEffect(() => { + if (isResetting) { + setTimeRange(null); + setBrushIndices({}); + setIsResetting(false); + } + }, [isResetting]); const chartMargin = { top: 8, right: 16, bottom: 8, left: 16 }; @@ -170,32 +164,6 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { return [...pingHistory.tasks].sort((a, b) => a.id - b.id); }, [pingHistory?.tasks]); - const generateColor = useCallback( - (taskName: string, total: number) => { - const index = sortedTasks.findIndex((t) => t.name === taskName); - if (index === -1) return "#000000"; // Fallback color - - const hue = (index * (360 / total)) % 360; - - // 使用OKLCH色彩空间,优化折线图的颜色区分度 - // L=0.7 (较高亮度,便于在图表背景上清晰显示) - // C=0.2 (较高饱和度,增强颜色区分度) - const oklchColor = `oklch(0.6 0.2 ${hue} / .8)`; - - // 为不支持OKLCH的浏览器提供HSL备用色 - // 使用更高的饱和度和适中的亮度来匹配OKLCH的视觉效果 - const hslFallback = `hsl(${hue}, 50%, 60%)`; - - // 检查浏览器是否支持OKLCH - if (CSS.supports("color", oklchColor)) { - return oklchColor; - } else { - return hslFallback; - } - }, - [sortedTasks] - ); - const breakPoints = useMemo(() => { if (!connectBreaks || !chartData || chartData.length < 2) { return []; @@ -219,13 +187,13 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { if (isBreak) { points.push({ x: currentPoint.time, - color: generateColor(task.name, sortedTasks.length), + color: generateColor(task.name, sortedTasks), }); } } } return points; - }, [chartData, sortedTasks, visiblePingTasks, generateColor, connectBreaks]); + }, [chartData, sortedTasks, visiblePingTasks, connectBreaks]); const taskStats = useMemo(() => { if (!pingHistory?.records || !sortedTasks.length) return []; @@ -241,10 +209,10 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { value: latestValue, time: latestTime, loss: loss, - color: generateColor(task.name, sortedTasks.length), + color: generateColor(task.name, sortedTasks), }; }); - }, [pingHistory?.records, sortedTasks, generateColor, timeRange]); + }, [pingHistory?.records, sortedTasks, timeRange]); return (
@@ -342,7 +310,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
-
+
+
@@ -366,7 +366,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { { }); }} tick={{ fill: "var(--theme-text-muted-color)" }} + axisLine={{ + stroke: "var(--theme-line-muted-color)", + }} scale="time" /> } + content={ + lableFormatter(value, hours)} + /> + } /> {connectBreaks && breakPoints.map((point, index) => ( @@ -417,7 +427,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { type={"monotone"} dataKey={String(task.id)} name={task.name} - stroke={generateColor(task.name, sortedTasks.length)} + stroke={generateColor(task.name, sortedTasks)} strokeWidth={2} hide={!visiblePingTasks.includes(task.id)} dot={false} @@ -425,6 +435,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { /> ))} { chartData[e.startIndex].time, chartData[e.endIndex].time, ]); + setBrushIndices({ + startIndex: e.startIndex, + endIndex: e.endIndex, + }); } else { setTimeRange(null); + setBrushIndices({}); } }} /> diff --git a/src/utils/chartHelper.ts b/src/utils/chartHelper.ts new file mode 100644 index 0000000..9bedcd2 --- /dev/null +++ b/src/utils/chartHelper.ts @@ -0,0 +1,77 @@ +import type { PingTask } from "@/types/node"; + +/** + * 根据任务名称和任务列表生成颜色 + * @param taskName - 任务名称 + * @param sortedTasks - 已排序的任务列表 + * @returns CSS 颜色字符串 + */ +export const generateColor = (taskName: string, sortedTasks: PingTask[]) => { + const index = sortedTasks.findIndex((t) => t.name === taskName); + if (index === -1) return "#000000"; // Fallback color + + const total = sortedTasks.length; + const hue = (index * (360 / total)) % 360; + + // 使用OKLCH色彩空间,优化折线图的颜色区分度 + const oklchColor = `oklch(0.6 0.2 ${hue} / .8)`; + + // 为不支持OKLCH的浏览器提供HSL备用色 + const hslFallback = `hsl(${hue}, 50%, 60%)`; + + // 检查浏览器是否支持OKLCH + if ( + typeof window !== "undefined" && + window.CSS && + CSS.supports("color", oklchColor) + ) { + return oklchColor; + } else { + return hslFallback; + } +}; + +/** + * 格式化图表X轴的标签 + * @param value - 时间戳 + * @param hours - 当前选择的时间范围(小时) + * @returns 格式化后的时间字符串 + */ +export const lableFormatter = (value: any, hours: number) => { + const date = new Date(value); + if (hours === 0) { + return date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + } + return date.toLocaleString([], { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +}; + +/** + * 格式化负载图表X轴的时间标签 + * @param value - 时间戳 + * @param index - 索引 + * @param dataLength - 数据总长度 + * @returns 格式化后的时间字符串 (只显示首尾) + */ +export const loadChartTimeFormatter = ( + value: any, + index: number, + dataLength: number +) => { + if (dataLength === 0) return ""; + if (index === 0 || index === dataLength - 1) { + return new Date(value).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + } + return ""; +};