diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index ec7bb22..b2b6a6b 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -23,7 +23,7 @@ const CardHeader = React.forwardRef< >(({ className, ...props }, ref) => (
)); @@ -57,7 +57,7 @@ const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
+
)); CardContent.displayName = "CardContent"; @@ -67,7 +67,7 @@ const CardFooter = React.forwardRef< >(({ className, ...props }, ref) => (
)); diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..c0c807c --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,79 @@ +import { useCallback } from "react"; + +interface CustomTooltipProps { + active?: boolean; + payload?: any[]; + label?: any; + chartConfig?: any; + labelFormatter?: (label: any) => string; +} + +export const CustomTooltip = ({ + active, + payload, + label, + chartConfig, + labelFormatter, +}: CustomTooltipProps) => { + const defaultLabelFormatter = useCallback((value: any) => { + const date = new Date(value); + return date.toLocaleString([], { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + }, []); + + if (active && payload && payload.length) { + return ( +
+

+ {labelFormatter + ? labelFormatter(label) + : defaultLabelFormatter(label)} +

+
+ {payload.map((item: any, index: number) => { + const series = chartConfig?.series + ? chartConfig.series.find((s: any) => s.dataKey === item.dataKey) + : { + dataKey: chartConfig?.dataKey || item.dataKey, + tooltipLabel: chartConfig?.tooltipLabel || item.name, + tooltipFormatter: chartConfig?.tooltipFormatter, + }; + + let value = item.value; + if (series?.tooltipFormatter) { + value = series.tooltipFormatter(value, item.payload); + } else if (typeof value === "number") { + value = `${value.toFixed(0)}ms`; + } else { + value = value?.toString() || "-"; + } + + return ( +
+
+
+ + {series?.tooltipLabel || item.name || item.dataKey}: + +
+ {value} +
+ ); + })} +
+
+ ); + } + + return null; +}; diff --git a/src/hooks/useNodeCommons.ts b/src/hooks/useNodeCommons.ts index cb25c15..460c4ad 100644 --- a/src/hooks/useNodeCommons.ts +++ b/src/hooks/useNodeCommons.ts @@ -59,7 +59,7 @@ export const useNodeCommons = (node: NodeWithStatus) => { const tagList = [ ...(price ? [price] : []), - ...(daysLeftTag ? [daysLeftTag] : []), + ...(daysLeftTag && price ? [daysLeftTag] : []), ...(typeof node.tags === "string" ? node.tags .split(";") diff --git a/src/pages/instance/LoadCharts.tsx b/src/pages/instance/LoadCharts.tsx index 45be96d..66d2acc 100644 --- a/src/pages/instance/LoadCharts.tsx +++ b/src/pages/instance/LoadCharts.tsx @@ -16,6 +16,7 @@ import { formatBytes } from "@/utils"; import { Flex } from "@radix-ui/themes"; import Loading from "@/components/loading"; import { useLoadCharts } from "@/hooks/useLoadCharts"; +import { CustomTooltip } from "@/components/ui/tooltip"; interface LoadChartsProps { node: NodeData; @@ -212,54 +213,6 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => { }, ]; - // 通用提示组件 - const CustomTooltip = ({ active, payload, label, chartConfig }: any) => { - if (!active || !payload || !payload.length) return null; - - return ( -
-

- {labelFormatter(label)} -

-
- {payload.map((item: any, index: number) => { - const series = chartConfig.series - ? chartConfig.series.find((s: any) => s.dataKey === item.dataKey) - : { - dataKey: chartConfig.dataKey, - tooltipLabel: chartConfig.tooltipLabel, - tooltipFormatter: chartConfig.tooltipFormatter, - }; - - let value = item.value; - if (series?.tooltipFormatter) { - value = series.tooltipFormatter(value, item.payload); - } else { - value = value.toString(); - } - - return ( -
-
-
- - {series?.tooltipLabel || item.dataKey}: - -
- {value} -
- ); - })} -
-
- ); - }; - // 根据配置渲染图表 const renderChart = (config: any) => { const ChartComponent = config.type === "area" ? AreaChart : LineChart; @@ -327,7 +280,11 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => { ( - + )} /> {config.series ? ( diff --git a/src/pages/instance/PingChart.tsx b/src/pages/instance/PingChart.tsx index 7d6aee7..682f7d0 100644 --- a/src/pages/instance/PingChart.tsx +++ b/src/pages/instance/PingChart.tsx @@ -9,15 +9,15 @@ import { ResponsiveContainer, Brush, } from "recharts"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Switch } from "@/components/ui/switch"; import { Label } from "@radix-ui/react-label"; import type { NodeData } from "@/types/node"; import Loading from "@/components/loading"; import { usePingChart } from "@/hooks/usePingChart"; import fillMissingTimePoints, { cutPeakValues } from "@/utils/RecordHelper"; -import { Button } from "@/components/ui/button"; import { useConfigItem } from "@/config"; +import { CustomTooltip } from "@/components/ui/tooltip"; interface PingChartProps { node: NodeData; @@ -57,6 +57,8 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { [hours] ); + const chartMargin = { top: 8, right: 16, bottom: 8, left: 16 }; + const chartData = useMemo(() => { if (!pingHistory || !pingHistory.records || !pingHistory.tasks) return []; @@ -136,21 +138,24 @@ const PingChart = memo(({ node, hours }: PingChartProps) => { ); }; - const stringToColor = useCallback((str: string) => { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - let color = "#"; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xff; - color += ("00" + value.toString(16)).substr(-2); - } - return color; - }, []); + const sortedTasks = useMemo(() => { + if (!pingHistory?.tasks) return []; + return [...pingHistory.tasks].sort((a, b) => a.name.localeCompare(b.name)); + }, [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; + return `hsl(${hue}, 50%, 60%)`; + }, + [sortedTasks] + ); return ( -
+
{loading && (
@@ -161,60 +166,63 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {

{error}

)} + + + +
+ {sortedTasks.map((task) => { + const values = chartData + .map((d) => d[task.id]) + .filter((v) => v !== null && v !== undefined) as number[]; + const loss = + chartData.length > 0 + ? (1 - values.length / chartData.length) * 100 + : 0; + const min = values.length > 0 ? Math.min(...values) : 0; + const isVisible = visiblePingTasks.includes(task.id); + const color = generateColor(task.name, sortedTasks.length); + + return ( +
handleTaskVisibilityToggle(task.id)} + style={{ + outlineColor: isVisible ? color : undefined, + boxShadow: isVisible ? `0 0 8px ${color}` : undefined, + }}> +
{task.name}
+ + {loss.toFixed(1)}% | {min.toFixed(0)}ms + +
+ ); + })} +
+
+
+
- Ping 延迟 -
+
- +
-
- {(pingHistory?.tasks || []).map((task) => { - const values = chartData - .map((d) => d[task.id]) - .filter((v) => v !== null && v !== undefined) as number[]; - const loss = - chartData.length > 0 - ? (1 - values.length / chartData.length) * 100 - : 0; - const min = values.length > 0 ? Math.min(...values) : 0; - const isVisible = visiblePingTasks.includes(task.id); - - return ( -
- -
- ); - })} -
- + {pingHistory?.tasks && pingHistory.tasks.length > 0 ? ( - + { }} scale="time" /> - - - {pingHistory.tasks.map((task) => ( + + } + /> + {sortedTasks.map((task) => ( { return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", + second: "2-digit", }); } return date.toLocaleDateString([], {