feat: 添加磨砂玻璃效果自定义配置及相关样式支持

This commit is contained in:
Montia37
2025-08-26 03:25:50 +08:00
parent 832a4dc3d9
commit 78e02f0ca2
22 changed files with 769 additions and 225 deletions

View File

@@ -8,7 +8,7 @@ import Loading from "@/components/loading";
import type { NodeWithStatus } from "@/types/node";
import { useNodeData } from "@/contexts/NodeDataContext";
import { useLiveData } from "@/contexts/LiveDataContext";
import { useConfigItem } from "@/config";
import { useAppConfig } from "@/config";
interface HomePageProps {
viewMode: "grid" | "table";
@@ -19,8 +19,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
const { nodes: staticNodes, loading, getGroups } = useNodeData();
const { liveData } = useLiveData();
const [selectedGroup, setSelectedGroup] = useState("所有");
const enableGroupedBar = useConfigItem("enableGroupedBar");
const enableStatsBar = useConfigItem("enableStatsBar");
const { enableGroupedBar, enableStatsBar, enableSwap } = useAppConfig();
const [displayOptions, setDisplayOptions] = useState({
time: true,
online: true,
@@ -95,7 +94,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
<main className="flex-1 px-4 pb-4">
{enableGroupedBar && (
<div className="flex overflow-auto whitespace-nowrap overflow-x-auto items-center min-w-[300px] text-secondary-foreground box-border border space-x-4 px-4 rounded-lg mb-4 bg-card backdrop-blur-[10px]">
<div className="flex overflow-auto whitespace-nowrap overflow-x-auto items-center min-w-[300px] text-secondary-foreground box-border border border-border space-x-4 px-4 rounded-lg mb-4 purcarte-blur">
<span></span>
{groups.map((group: string) => (
<Button
@@ -117,7 +116,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
className={
viewMode === "grid"
? ""
: "space-y-2 bg-card overflow-auto backdrop-blur-[10px] rounded-lg p-2"
: "space-y-2 overflow-auto box-border border border-border purcarte-blur rounded-lg p-2"
}>
<div
className={
@@ -125,12 +124,22 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
? "grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4"
: "min-w-[1080px]"
}>
{viewMode === "table" && <NodeListHeader />}
{viewMode === "table" && (
<NodeListHeader enableSwap={enableSwap} />
)}
{filteredNodes.map((node: NodeWithStatus) =>
viewMode === "grid" ? (
<NodeCard key={node.uuid} node={node} />
<NodeCard
key={node.uuid}
node={node}
enableSwap={enableSwap}
/>
) : (
<NodeListItem key={node.uuid} node={node} />
<NodeListItem
key={node.uuid}
node={node}
enableSwap={enableSwap}
/>
)
)}
</div>

View File

@@ -97,14 +97,14 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
? `${formatBytes(liveData.ram.used)} / ${formatBytes(
node?.mem_total || 0
)}`
: "-"}
: "N/A"}
</label>
<label>
{liveData?.swap?.used
? `${formatBytes(liveData.swap.used)} / ${formatBytes(
node?.swap_total || 0
)}`
: "-"}
: "N/A"}
</label>
</Flex>
),
@@ -199,6 +199,8 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
tooltipLabel: "UDP 连接",
},
],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? `${value}` : "",
data: chartData,
},
{
@@ -208,6 +210,8 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
value: liveData?.process || "-",
dataKey: "process",
color: colors[0],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? `${value}` : "",
data: chartData,
tooltipLabel: "进程数",
},
@@ -260,8 +264,8 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 10 }}
axisLine={{ stroke: "var(--muted-foreground)" }}
tick={{ fill: "var(--muted-foreground)" }}
tickFormatter={timeFormatter}
interval={0}
height={20}
@@ -273,8 +277,11 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
tickFormatter={config.yAxisFormatter}
orientation="left"
type="number"
tick={{ fontSize: 10, dx: -8 }}
width={25}
tick={{
dx: -8,
fill: "var(--muted-foreground)",
}}
width={200}
mirror={true}
/>
<Tooltip
@@ -319,12 +326,12 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
return (
<div className="relative">
{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 purcarte-blur rounded-lg z-10">
<Loading text="正在加载图表数据..." />
</div>
)}
{error && (
<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 purcarte-blur rounded-lg z-10">
<p className="text-red-500">{error}</p>
</div>
)}

View File

@@ -8,6 +8,7 @@ import {
Tooltip,
ResponsiveContainer,
Brush,
ReferenceLine,
} from "recharts";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
@@ -18,6 +19,7 @@ import { usePingChart } from "@/hooks/usePingChart";
import fillMissingTimePoints, { cutPeakValues } from "@/utils/RecordHelper";
import { useConfigItem } from "@/config";
import { CustomTooltip } from "@/components/ui/tooltip";
import Tips from "@/components/ui/tips";
interface PingChartProps {
node: NodeData;
@@ -29,6 +31,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
const [visiblePingTasks, setVisiblePingTasks] = useState<number[]>([]);
const [timeRange, setTimeRange] = useState<[number, number] | null>(null);
const [cutPeak, setCutPeak] = useState(false);
const [connectBreaks, setConnectBreaks] = useState(false);
const maxPointsToRender = useConfigItem("pingChartMaxPoints") || 0; // 0表示不限制
useEffect(() => {
@@ -144,70 +147,103 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
}, [pingHistory?.tasks]);
const generateColor = useCallback(
(taskName: string, total: number) => {
(taskName: string, total: number, isBreakPoints?: boolean) => {
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%)`;
return `hsla(${hue}, 50%, 60%, ${isBreakPoints ? 0.7 : 1})`;
},
[sortedTasks]
);
const breakPoints = useMemo(() => {
if (!connectBreaks || !chartData || chartData.length < 2) {
return [];
}
const points: { x: number; color: string }[] = [];
for (const task of sortedTasks) {
if (!visiblePingTasks.includes(task.id)) {
continue;
}
const taskKey = String(task.id);
for (let i = 1; i < chartData.length; i++) {
const prevPoint = chartData[i - 1];
const currentPoint = chartData[i];
const isBreak =
(currentPoint[taskKey] === null ||
currentPoint[taskKey] === undefined) &&
prevPoint[taskKey] !== null &&
prevPoint[taskKey] !== undefined;
if (isBreak) {
points.push({
x: currentPoint.time,
color: generateColor(task.name, sortedTasks.length, true),
});
}
}
}
return points;
}, [chartData, sortedTasks, visiblePingTasks, generateColor, connectBreaks]);
return (
<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">
<div className="absolute inset-0 flex items-center justify-center purcarte-blur rounded-lg z-10">
<Loading text="正在加载图表数据..." />
</div>
)}
{error && (
<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 purcarte-blur rounded-lg z-10">
<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);
{pingHistory?.tasks && pingHistory.tasks.length > 0 && (
<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>
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>
<div className="flex items-center gap-4">
<div className="flex items-center space-x-2">
<Switch
id="peak-shaving"
@@ -215,6 +251,30 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
onCheckedChange={setCutPeak}
/>
<Label htmlFor="peak-shaving"></Label>
<Tips>
<span
dangerouslySetInnerHTML={{
__html:
'<h2 class="text-lg font-bold">关于数据平滑的提示</h2><p>当您开启平滑后,您在统计图中看到的曲线经过<strong>指数加权移动平均 (EWMA)</strong> 算法处理,这是一种常用的数据平滑技术。</p></br><p>需要注意的是经过EWMA算法平滑后的曲线所展示的数值<strong>并非原始的、真实的测量数据</strong>。它们是根据EWMA算法计算得出的一个<strong>平滑趋势线</strong>,旨在减少数据波动,使数据模式和趋势更容易被识别。</p></br><p>因此,您看到的数值更像是<strong>视觉上的呈现</strong>,帮助您更好地理解数据的整体走向和长期趋势,而不是每一个时间点的精确真实值。如果您需要查看具体、原始的数据点,请参考未经平滑处理的数据视图。</p>',
}}
/>
</Tips>
</div>
<div className="flex items-center space-x-2">
<Switch
id="connect-breaks"
checked={connectBreaks}
onCheckedChange={setConnectBreaks}
/>
<Label htmlFor="connect-breaks"></Label>
<Tips>
<span
dangerouslySetInnerHTML={{
__html:
'<h2 class="text-lg font-bold">关于连接断点的提示</h2><p>当您开启"连接断点"功能后,图表中的曲线将会跨过那些由于网络问题或其他原因导致的丢包点,形成一条连续的线条。同时,系统会在丢包位置显示<strong>半透明的垂直参考线</strong>来标记断点位置。</p>',
}}
/>
</Tips>
</div>
</div>
</div>
@@ -244,24 +304,39 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
minute: "2-digit",
});
}}
tick={{ fill: "var(--muted-foreground)" }}
scale="time"
/>
<YAxis mirror={true} width={30} />
<YAxis
mirror={true}
width={30}
tick={{ fill: "var(--muted-foreground)" }}
/>
<Tooltip
cursor={false}
content={<CustomTooltip labelFormatter={lableFormatter} />}
/>
{connectBreaks &&
breakPoints.map((point, index) => (
<ReferenceLine
key={`break-${index}`}
x={point.x}
stroke={point.color}
strokeWidth={1}
strokeOpacity={0.5}
/>
))}
{sortedTasks.map((task) => (
<Line
key={task.id}
type={cutPeak ? "basis" : "linear"}
type={"basis"}
dataKey={String(task.id)}
name={task.name}
stroke={generateColor(task.name, sortedTasks.length)}
strokeWidth={2}
hide={!visiblePingTasks.includes(task.id)}
dot={false}
connectNulls={false}
connectNulls={connectBreaks}
/>
))}
<Brush

View File

@@ -11,6 +11,7 @@ const PingChart = lazy(() => import("./PingChart"));
import Loading from "@/components/loading";
import Flag from "@/components/sections/Flag";
import { useConfigItem } from "@/config";
import { useIsMobile } from "@/hooks/useMobile";
const InstancePage = () => {
const { uuid } = useParams<{ uuid: string }>();
@@ -28,6 +29,7 @@ const InstancePage = () => {
const [pingHours, setPingHours] = useState<number>(1); // 默认1小时
const enableInstanceDetail = useConfigItem("enableInstanceDetail");
const enablePingChart = useConfigItem("enablePingChart");
const isMobile = useIsMobile();
const maxRecordPreserveTime = publicSettings?.record_preserve_time || 0; // 默认0表示关闭
const maxPingRecordPreserveTime =
@@ -106,10 +108,10 @@ const InstancePage = () => {
return (
<div className="w-[90%] max-w-screen-2xl mx-auto flex-1 flex flex-col pb-15 p-4 space-y-4">
<div className="flex items-center justify-between bg-card box-border border rounded-lg p-4 mb-4 text-secondary-foreground">
<div className="flex items-center justify-between purcarte-blur box-border border border-border rounded-lg p-4 mb-4 text-secondary-foreground">
<div className="flex items-center gap-2 min-w-0">
<Button
className="bg-card flex-shrink-0"
className="flex-shrink-0"
variant="ghost"
size="icon"
onClick={() => navigate(-1)}>
@@ -127,24 +129,31 @@ const InstancePage = () => {
{enableInstanceDetail && <Instance node={node as NodeWithStatus} />}
<div className="flex justify-center w-full">
<div className="bg-card border rounded-lg py-3 px-4">
<div className="flex flex-col items-center w-full space-y-4">
<div className="purcarte-blur box-border border border-border rounded-lg p-2">
<div className="flex justify-center space-x-2">
<Button
variant={chartType === "load" ? "secondary" : "ghost"}
size="sm"
onClick={() => setChartType("load")}>
</Button>
{enablePingChart && (
<Button
variant={chartType === "ping" ? "secondary" : "ghost"}
size="sm"
onClick={() => setChartType("ping")}>
</Button>
)}
</div>
</div>
<div
className={`purcarte-blur box-border border border-border justify-center rounded-lg p-2 ${
isMobile ? "w-full" : ""
}`}>
{chartType === "load" ? (
<div className="flex justify-center space-x-2 mt-2">
<div className="flex space-x-2 overflow-x-auto whitespace-nowrap">
{loadTimeRanges.map((range) => (
<Button
key={range.label}
@@ -156,7 +165,7 @@ const InstancePage = () => {
))}
</div>
) : (
<div className="flex justify-center space-x-2 mt-2">
<div className="flex space-x-2 overflow-x-auto whitespace-nowrap">
{pingTimeRanges.map((range) => (
<Button
key={range.label}