mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-18 19:39:22 +08:00
fix: 调整卡片内边距,优化图表和延迟显示的布局
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
));
|
||||
|
79
src/components/ui/tooltip.tsx
Normal file
79
src/components/ui/tooltip.tsx
Normal 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;
|
||||
};
|
@@ -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(";")
|
||||
|
@@ -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 ? (
|
||||
|
@@ -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,60 +166,63 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
||||
<p className="text-red-500">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<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[];
|
||||
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 (
|
||||
<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={{
|
||||
outlineColor: isVisible ? color : undefined,
|
||||
boxShadow: isVisible ? `0 0 8px ${color}` : undefined,
|
||||
}}>
|
||||
<div className="font-semibold">{task.name}</div>
|
||||
<span className="text-xs font-normal">
|
||||
{loss.toFixed(1)}% | {min.toFixed(0)}ms
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="peak-shaving"
|
||||
checked={cutPeak}
|
||||
onCheckedChange={setCutPeak}
|
||||
/>
|
||||
<Label htmlFor="peak-shaving">开启削峰</Label>
|
||||
<Label htmlFor="peak-shaving">平滑</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 justify-end">
|
||||
{(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 (
|
||||
<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"
|
||||
onClick={() => handleTaskVisibilityToggle(task.id)}
|
||||
style={{
|
||||
backgroundColor: isVisible
|
||||
? stringToColor(task.name)
|
||||
: undefined,
|
||||
color: isVisible ? "white" : undefined,
|
||||
}}>
|
||||
<div>{task.name}</div>
|
||||
<span className="text-xs">
|
||||
{loss.toFixed(1)}% | {min.toFixed(0)}ms
|
||||
</span>
|
||||
</Button>
|
||||
</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([], {
|
||||
|
Reference in New Issue
Block a user