mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-19 03:49:22 +08:00
fix: 调整卡片内边距,优化图表和延迟显示的布局
This commit is contained in:
@@ -23,7 +23,7 @@ const CardHeader = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@@ -57,7 +57,7 @@ const CardContent = React.forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ 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";
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ const CardFooter = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn("flex items-center p-4 pt-0", className)}
|
||||||
{...props}
|
{...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 = [
|
const tagList = [
|
||||||
...(price ? [price] : []),
|
...(price ? [price] : []),
|
||||||
...(daysLeftTag ? [daysLeftTag] : []),
|
...(daysLeftTag && price ? [daysLeftTag] : []),
|
||||||
...(typeof node.tags === "string"
|
...(typeof node.tags === "string"
|
||||||
? node.tags
|
? node.tags
|
||||||
.split(";")
|
.split(";")
|
||||||
|
@@ -16,6 +16,7 @@ import { formatBytes } from "@/utils";
|
|||||||
import { Flex } from "@radix-ui/themes";
|
import { Flex } from "@radix-ui/themes";
|
||||||
import Loading from "@/components/loading";
|
import Loading from "@/components/loading";
|
||||||
import { useLoadCharts } from "@/hooks/useLoadCharts";
|
import { useLoadCharts } from "@/hooks/useLoadCharts";
|
||||||
|
import { CustomTooltip } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
interface LoadChartsProps {
|
interface LoadChartsProps {
|
||||||
node: NodeData;
|
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 renderChart = (config: any) => {
|
||||||
const ChartComponent = config.type === "area" ? AreaChart : LineChart;
|
const ChartComponent = config.type === "area" ? AreaChart : LineChart;
|
||||||
@@ -327,7 +280,11 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
cursor={false}
|
cursor={false}
|
||||||
content={(props: any) => (
|
content={(props: any) => (
|
||||||
<CustomTooltip {...props} chartConfig={config} />
|
<CustomTooltip
|
||||||
|
{...props}
|
||||||
|
chartConfig={config}
|
||||||
|
labelFormatter={labelFormatter}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{config.series ? (
|
{config.series ? (
|
||||||
|
@@ -9,15 +9,15 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
Brush,
|
Brush,
|
||||||
} from "recharts";
|
} 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 { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@radix-ui/react-label";
|
import { Label } from "@radix-ui/react-label";
|
||||||
import type { NodeData } from "@/types/node";
|
import type { NodeData } from "@/types/node";
|
||||||
import Loading from "@/components/loading";
|
import Loading from "@/components/loading";
|
||||||
import { usePingChart } from "@/hooks/usePingChart";
|
import { usePingChart } from "@/hooks/usePingChart";
|
||||||
import fillMissingTimePoints, { cutPeakValues } from "@/utils/RecordHelper";
|
import fillMissingTimePoints, { cutPeakValues } from "@/utils/RecordHelper";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useConfigItem } from "@/config";
|
import { useConfigItem } from "@/config";
|
||||||
|
import { CustomTooltip } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
interface PingChartProps {
|
interface PingChartProps {
|
||||||
node: NodeData;
|
node: NodeData;
|
||||||
@@ -57,6 +57,8 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
[hours]
|
[hours]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chartMargin = { top: 8, right: 16, bottom: 8, left: 16 };
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!pingHistory || !pingHistory.records || !pingHistory.tasks) return [];
|
if (!pingHistory || !pingHistory.records || !pingHistory.tasks) return [];
|
||||||
|
|
||||||
@@ -136,21 +138,24 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stringToColor = useCallback((str: string) => {
|
const sortedTasks = useMemo(() => {
|
||||||
let hash = 0;
|
if (!pingHistory?.tasks) return [];
|
||||||
for (let i = 0; i < str.length; i++) {
|
return [...pingHistory.tasks].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
}, [pingHistory?.tasks]);
|
||||||
}
|
|
||||||
let color = "#";
|
const generateColor = useCallback(
|
||||||
for (let i = 0; i < 3; i++) {
|
(taskName: string, total: number) => {
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
const index = sortedTasks.findIndex((t) => t.name === taskName);
|
||||||
color += ("00" + value.toString(16)).substr(-2);
|
if (index === -1) return "#000000"; // Fallback color
|
||||||
}
|
|
||||||
return color;
|
const hue = (index * (360 / total)) % 360;
|
||||||
}, []);
|
return `hsl(${hue}, 50%, 60%)`;
|
||||||
|
},
|
||||||
|
[sortedTasks]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative space-y-4">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-card/50 backdrop-blur-sm rounded-lg z-10">
|
<div className="absolute inset-0 flex items-center justify-center bg-card/50 backdrop-blur-sm rounded-lg z-10">
|
||||||
<Loading text="正在加载图表数据..." />
|
<Loading text="正在加载图表数据..." />
|
||||||
@@ -161,60 +166,63 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
<p className="text-red-500">{error}</p>
|
<p className="text-red-500">{error}</p>
|
||||||
</div>
|
</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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-sm font-medium">Ping 延迟</CardTitle>
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex items-center space-x-2 mt-2">
|
|
||||||
<Switch
|
<Switch
|
||||||
id="peak-shaving"
|
id="peak-shaving"
|
||||||
checked={cutPeak}
|
checked={cutPeak}
|
||||||
onCheckedChange={setCutPeak}
|
onCheckedChange={setCutPeak}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="peak-shaving">开启削峰</Label>
|
<Label htmlFor="peak-shaving">平滑</Label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="pt-0">
|
||||||
{pingHistory?.tasks && pingHistory.tasks.length > 0 ? (
|
{pingHistory?.tasks && pingHistory.tasks.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
<LineChart data={chartData}>
|
<LineChart data={chartData} margin={chartMargin}>
|
||||||
<CartesianGrid strokeDasharray="2 4" vertical={false} />
|
<CartesianGrid strokeDasharray="2 4" vertical={false} />
|
||||||
<XAxis
|
<XAxis
|
||||||
type="number"
|
type="number"
|
||||||
@@ -238,15 +246,18 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
}}
|
}}
|
||||||
scale="time"
|
scale="time"
|
||||||
/>
|
/>
|
||||||
<YAxis />
|
<YAxis mirror={true} width={30} />
|
||||||
<Tooltip labelFormatter={lableFormatter} />
|
<Tooltip
|
||||||
{pingHistory.tasks.map((task) => (
|
cursor={false}
|
||||||
|
content={<CustomTooltip labelFormatter={lableFormatter} />}
|
||||||
|
/>
|
||||||
|
{sortedTasks.map((task) => (
|
||||||
<Line
|
<Line
|
||||||
key={task.id}
|
key={task.id}
|
||||||
type={cutPeak ? "basis" : "linear"}
|
type={cutPeak ? "basis" : "linear"}
|
||||||
dataKey={String(task.id)}
|
dataKey={String(task.id)}
|
||||||
name={task.name}
|
name={task.name}
|
||||||
stroke={stringToColor(task.name)}
|
stroke={generateColor(task.name, sortedTasks.length)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
hide={!visiblePingTasks.includes(task.id)}
|
hide={!visiblePingTasks.includes(task.id)}
|
||||||
dot={false}
|
dot={false}
|
||||||
@@ -264,6 +275,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
return date.toLocaleTimeString([], {
|
return date.toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return date.toLocaleDateString([], {
|
return date.toLocaleDateString([], {
|
||||||
|
Reference in New Issue
Block a user