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:
@@ -203,19 +203,59 @@ export const Header = ({
|
|||||||
)}
|
)}
|
||||||
{isInstancePage && (
|
{isInstancePage && (
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
{isMobile ? (
|
||||||
{theme === "dark" ? (
|
<DropdownMenu>
|
||||||
<Sun className="size-5 text-primary" />
|
<DropdownMenuTrigger asChild>
|
||||||
) : (
|
<Button
|
||||||
<Moon className="size-5 text-primary" />
|
variant="ghost"
|
||||||
)}
|
size="icon"
|
||||||
</Button>
|
className="relative group">
|
||||||
{enableAdminButton && (
|
<Menu className="size-5 text-primary transition-transform duration-300 group-data-[state=open]:rotate-180" />
|
||||||
<a href="/admin" target="_blank" rel="noopener noreferrer">
|
<span className="absolute -bottom-1 left-1/2 w-1.5 h-1.5 rounded-full bg-primary transform -translate-x-1/2 scale-0 transition-transform duration-300 group-data-[state=open]:scale-100"></span>
|
||||||
<Button variant="ghost" size="icon">
|
</Button>
|
||||||
<CircleUserIcon className="size-5 text-primary" />
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="animate-in slide-in-from-top-5 duration-300 bg-background/60 backdrop-blur-[10px] border-border/60 rounded-xl">
|
||||||
|
<DropdownMenuItem onClick={toggleTheme}>
|
||||||
|
{theme === "dark" ? (
|
||||||
|
<Sun className="size-4 mr-2 text-primary" />
|
||||||
|
) : (
|
||||||
|
<Moon className="size-4 mr-2 text-primary" />
|
||||||
|
)}
|
||||||
|
<span>{theme === "dark" ? "浅色模式" : "深色模式"}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{enableAdminButton && (
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<a
|
||||||
|
href="/admin"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center">
|
||||||
|
<CircleUserIcon className="size-4 mr-2 text-primary" />
|
||||||
|
<span>管理员</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
||||||
|
{theme === "dark" ? (
|
||||||
|
<Sun className="size-5 text-primary" />
|
||||||
|
) : (
|
||||||
|
<Moon className="size-5 text-primary" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
{enableAdminButton && (
|
||||||
|
<a href="/admin" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<CircleUserIcon className="size-5 text-primary" />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@@ -55,12 +55,17 @@ function ChartContainer({
|
|||||||
data-slot="chart"
|
data-slot="chart"
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
className={cn(
|
className={cn(
|
||||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
style={{ position: "relative", overflow: "hidden" }}
|
||||||
{...props}>
|
{...props}>
|
||||||
<ChartStyle id={chartId} config={config} />
|
<ChartStyle id={chartId} config={config} />
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
<RechartsPrimitive.ResponsiveContainer
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
debounce={50}
|
||||||
|
style={{ position: "relative", overflow: "hidden" }}>
|
||||||
{children}
|
{children}
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -116,7 +116,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">总流量</p>
|
<p className="text-muted-foreground text-sm">总流量</p>
|
||||||
<p className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{node.traffic_limit && isOnline && (
|
{node.traffic_limit && isOnline && (
|
||||||
<CircleProgress
|
<CircleProgress
|
||||||
value={trafficPercentage}
|
value={trafficPercentage}
|
||||||
@@ -139,7 +139,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">负载</p>
|
<p className="text-muted-foreground text-sm">负载</p>
|
||||||
|
@@ -65,8 +65,8 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 样式和颜色
|
// 样式和颜色
|
||||||
const cn = "flex flex-col w-full h-full gap-4 justify-between";
|
const cn = "flex flex-col w-full overflow-hidden";
|
||||||
const chartMargin = { top: 10, right: 16, bottom: 10, left: 16 };
|
const chartMargin = { top: 8, right: 16, bottom: 8, left: 16 };
|
||||||
const colors = ["#F38181", "#FCE38A", "#EAFFD0", "#95E1D3"];
|
const colors = ["#F38181", "#FCE38A", "#EAFFD0", "#95E1D3"];
|
||||||
|
|
||||||
// 图表配置
|
// 图表配置
|
||||||
@@ -266,6 +266,12 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
|
|||||||
const DataComponent =
|
const DataComponent =
|
||||||
config.type === "area" ? Area : (Line as React.ComponentType<any>);
|
config.type === "area" ? Area : (Line as React.ComponentType<any>);
|
||||||
|
|
||||||
|
// 只指定高度,让宽度自适应
|
||||||
|
const chartProps = {
|
||||||
|
height: 140, // 更小的高度以确保完全适应容器
|
||||||
|
style: { overflow: "visible" }, // 通过内联样式解决Safari溢出问题
|
||||||
|
};
|
||||||
|
|
||||||
const chartConfig = config.series
|
const chartConfig = config.series
|
||||||
? config.series.reduce((acc: any, series: any) => {
|
? config.series.reduce((acc: any, series: any) => {
|
||||||
acc[series.dataKey] = {
|
acc[series.dataKey] = {
|
||||||
@@ -282,62 +288,73 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn} key={config.id}>
|
<Card className={cn} key={config.id} style={{ height: "220px" }}>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 h-[80px]">
|
||||||
<CardTitle className="text-sm font-medium">{config.title}</CardTitle>
|
<CardTitle className="text-sm font-medium">{config.title}</CardTitle>
|
||||||
<span className="text-sm font-bold">{config.value}</span>
|
<div className="text-sm font-bold min-h-[20px] flex items-center">
|
||||||
|
{config.value}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<ChartContainer config={chartConfig}>
|
<div
|
||||||
<ChartComponent data={config.data} margin={chartMargin}>
|
className="h-[150px] w-full px-2 pb-2 align-bottom"
|
||||||
<CartesianGrid strokeDasharray="2 4" vertical={false} />
|
style={{ minHeight: 0 }}>
|
||||||
<XAxis
|
<ChartContainer config={chartConfig} className="h-full w-full">
|
||||||
dataKey="time"
|
<ChartComponent
|
||||||
tickLine={false}
|
data={config.data}
|
||||||
axisLine={false}
|
margin={chartMargin}
|
||||||
tick={{ fontSize: 11 }}
|
{...chartProps}>
|
||||||
tickFormatter={timeFormatter}
|
<CartesianGrid strokeDasharray="2 4" vertical={false} />
|
||||||
interval={0}
|
<XAxis
|
||||||
/>
|
dataKey="time"
|
||||||
<YAxis
|
tickLine={false}
|
||||||
tickLine={false}
|
axisLine={false}
|
||||||
axisLine={false}
|
tick={{ fontSize: 10 }}
|
||||||
domain={config.yAxisDomain}
|
tickFormatter={timeFormatter}
|
||||||
tickFormatter={config.yAxisFormatter}
|
interval={0}
|
||||||
orientation="left"
|
height={20}
|
||||||
type="number"
|
/>
|
||||||
tick={{ dx: -10 }}
|
<YAxis
|
||||||
mirror={true}
|
tickLine={false}
|
||||||
/>
|
axisLine={false}
|
||||||
<Tooltip
|
domain={config.yAxisDomain}
|
||||||
cursor={false}
|
tickFormatter={config.yAxisFormatter}
|
||||||
content={(props: any) => (
|
orientation="left"
|
||||||
<CustomTooltip {...props} chartConfig={config} />
|
type="number"
|
||||||
)}
|
tick={{ fontSize: 10, dx: -8 }}
|
||||||
/>
|
width={25}
|
||||||
{config.series ? (
|
mirror={true}
|
||||||
config.series.map((series: any) => (
|
/>
|
||||||
|
<Tooltip
|
||||||
|
cursor={false}
|
||||||
|
content={(props: any) => (
|
||||||
|
<CustomTooltip {...props} chartConfig={config} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{config.series ? (
|
||||||
|
config.series.map((series: any) => (
|
||||||
|
<DataComponent
|
||||||
|
key={series.dataKey}
|
||||||
|
dataKey={series.dataKey}
|
||||||
|
animationDuration={0}
|
||||||
|
stroke={series.color}
|
||||||
|
fill={config.type === "area" ? series.color : undefined}
|
||||||
|
opacity={0.8}
|
||||||
|
dot={false}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<DataComponent
|
<DataComponent
|
||||||
key={series.dataKey}
|
dataKey={config.dataKey}
|
||||||
dataKey={series.dataKey}
|
|
||||||
animationDuration={0}
|
animationDuration={0}
|
||||||
stroke={series.color}
|
stroke={config.color}
|
||||||
fill={config.type === "area" ? series.color : undefined}
|
fill={config.type === "area" ? config.color : undefined}
|
||||||
opacity={0.8}
|
opacity={0.8}
|
||||||
dot={false}
|
dot={false}
|
||||||
/>
|
/>
|
||||||
))
|
)}
|
||||||
) : (
|
</ChartComponent>
|
||||||
<DataComponent
|
</ChartContainer>
|
||||||
dataKey={config.dataKey}
|
</div>
|
||||||
animationDuration={0}
|
|
||||||
stroke={config.color}
|
|
||||||
fill={config.type === "area" ? config.color : undefined}
|
|
||||||
opacity={0.8}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ChartComponent>
|
|
||||||
</ChartContainer>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -354,7 +371,9 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
|
|||||||
<p className="text-red-500">{error}</p>
|
<p className="text-red-500">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div
|
||||||
|
className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"
|
||||||
|
style={{ minHeight: 0, overflow: "hidden" }}>
|
||||||
{chartConfigs.map(renderChart)}
|
{chartConfigs.map(renderChart)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user