fix: 调整卡片内边距,优化图表和延迟显示的布局

This commit is contained in:
Montia37
2025-08-24 15:34:23 +08:00
parent 77575b3d45
commit 832a4dc3d9
5 changed files with 159 additions and 111 deletions

View File

@@ -23,7 +23,7 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
className={cn("flex flex-col space-y-1.5 p-4", className)}
{...props}
/>
));
@@ -57,7 +57,7 @@ const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
@@ -67,7 +67,7 @@ const CardFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
className={cn("flex items-center p-4 pt-0", className)}
{...props}
/>
));

View File

@@ -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 (
<div className="bg-background/80 p-3 border rounded-lg shadow-lg max-w-xs">
<p className="text-xs font-medium text-muted-foreground mb-2">
{labelFormatter
? labelFormatter(label)
: defaultLabelFormatter(label)}
</p>
<div className="space-y-1">
{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 (
<div
key={`${item.dataKey}-${index}`}
className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: item.color }}
/>
<span className="text-sm font-medium text-foreground">
{series?.tooltipLabel || item.name || item.dataKey}:
</span>
</div>
<span className="text-sm font-bold ml-2">{value}</span>
</div>
);
})}
</div>
</div>
);
}
return null;
};

View File

@@ -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(";")

View File

@@ -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 (
<div className="bg-background/80 p-3 border rounded-lg shadow-lg max-w-xs">
<p className="text-xs font-medium text-muted-foreground mb-2">
{labelFormatter(label)}
</p>
<div className="space-y-1">
{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 (
<div
key={`${item.dataKey}-${index}`}
className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: item.color }}
/>
<span className="text-sm font-medium text-foreground">
{series?.tooltipLabel || item.dataKey}:
</span>
</div>
<span className="text-sm font-bold ml-2">{value}</span>
</div>
);
})}
</div>
</div>
);
};
// 根据配置渲染图表
const renderChart = (config: any) => {
const ChartComponent = config.type === "area" ? AreaChart : LineChart;
@@ -327,7 +280,11 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
<Tooltip
cursor={false}
content={(props: any) => (
<CustomTooltip {...props} chartConfig={config} />
<CustomTooltip
{...props}
chartConfig={config}
labelFormatter={labelFormatter}
/>
)}
/>
{config.series ? (

View File

@@ -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 (
<div className="relative">
<div className="relative space-y-4">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-card/50 backdrop-blur-sm rounded-lg z-10">
<Loading text="正在加载图表数据..." />
@@ -161,22 +166,11 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
<p className="text-red-500">{error}</p>
</div>
)}
<Card>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-sm font-medium">Ping </CardTitle>
<div className="flex items-center space-x-2 mt-2">
<Switch
id="peak-shaving"
checked={cutPeak}
onCheckedChange={setCutPeak}
/>
<Label htmlFor="peak-shaving"></Label>
</div>
</div>
<div className="flex flex-wrap gap-2 justify-end">
{(pingHistory?.tasks || []).map((task) => {
<CardContent className="p-2">
<div className="flex flex-wrap gap-2 items-center justify-center">
{sortedTasks.map((task) => {
const values = chartData
.map((d) => d[task.id])
.filter((v) => v !== null && v !== undefined) as number[];
@@ -186,35 +180,49 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
: 0;
const min = values.length > 0 ? Math.min(...values) : 0;
const isVisible = visiblePingTasks.includes(task.id);
const color = generateColor(task.name, sortedTasks.length);
return (
<div key={task.id} className="flex flex-col items-center">
<Button
variant={isVisible ? "default" : "outline"}
size="sm"
className="h-auto px-2 py-1 flex flex-col"
<div
key={task.id}
className={`h-auto px-3 py-1.5 flex flex-col leading-snug text-center cursor-pointer rounded-md transition-all outline-2 outline ${
isVisible ? "" : "outline-transparent"
}`}
onClick={() => handleTaskVisibilityToggle(task.id)}
style={{
backgroundColor: isVisible
? stringToColor(task.name)
: undefined,
color: isVisible ? "white" : undefined,
outlineColor: isVisible ? color : undefined,
boxShadow: isVisible ? `0 0 8px ${color}` : undefined,
}}>
<div>{task.name}</div>
<span className="text-xs">
<div className="font-semibold">{task.name}</div>
<span className="text-xs font-normal">
{loss.toFixed(1)}% | {min.toFixed(0)}ms
</span>
</Button>
</div>
);
})}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<div className="flex items-center space-x-2">
<Switch
id="peak-shaving"
checked={cutPeak}
onCheckedChange={setCutPeak}
/>
<Label htmlFor="peak-shaving"></Label>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<CardContent className="pt-0">
{pingHistory?.tasks && pingHistory.tasks.length > 0 ? (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData}>
<LineChart data={chartData} margin={chartMargin}>
<CartesianGrid strokeDasharray="2 4" vertical={false} />
<XAxis
type="number"
@@ -238,15 +246,18 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
}}
scale="time"
/>
<YAxis />
<Tooltip labelFormatter={lableFormatter} />
{pingHistory.tasks.map((task) => (
<YAxis mirror={true} width={30} />
<Tooltip
cursor={false}
content={<CustomTooltip labelFormatter={lableFormatter} />}
/>
{sortedTasks.map((task) => (
<Line
key={task.id}
type={cutPeak ? "basis" : "linear"}
dataKey={String(task.id)}
name={task.name}
stroke={stringToColor(task.name)}
stroke={generateColor(task.name, sortedTasks.length)}
strokeWidth={2}
hide={!visiblePingTasks.includes(task.id)}
dot={false}
@@ -264,6 +275,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
return date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
return date.toLocaleDateString([], {