feat: 更新构建流程,优化变更日志生成和图表渲染

This commit is contained in:
Montia37
2025-08-14 19:55:56 +08:00
parent f9913f4c19
commit f6db5cbd64
6 changed files with 327 additions and 537 deletions

View File

@@ -7,7 +7,7 @@ on:
- "v*"
jobs:
build-and-package:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
@@ -67,41 +67,20 @@ jobs:
echo "Created package: ${ZIP_NAME}"
ls -la ${ZIP_NAME}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: theme-package
path: ${{ env.ZIP_NAME }}
retention-days: 1
create-release-on-tag-push:
needs: build-and-package
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: theme-package
path: .
- name: Get Asset Name
id: get_asset_name
run: |
ASSET_NAME=$(ls *.zip)
echo "ASSET_NAME=${ASSET_NAME}" >> $GITHUB_ENV
- name: Generate Changelog
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
id: changelog
run: |
git fetch --prune --unshallow
git fetch --prune
PREVIOUS_TAG=$(git describe --tags --abbrev=0 `git rev-list --tags --skip=1 --max-count=1` 2>/dev/null || git rev-list --max-parents=0 HEAD)
echo "Previous tag: $PREVIOUS_TAG"
CHANGELOG=$(git log $PREVIOUS_TAG..${{ github.ref_name }} --pretty=format:"* %s (%h)")
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
id: create_release
uses: actions/create-release@v1
env:
@@ -112,12 +91,14 @@ jobs:
body: ${{ env.CHANGELOG }}
draft: false
prerelease: false
- name: Upload Release Asset
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.ASSET_NAME }}
asset_name: ${{ env.ASSET_NAME }}
asset_path: ./${{ env.ZIP_NAME }}
asset_name: ${{ env.ZIP_NAME }}
asset_content_type: application/zip

View File

@@ -17,8 +17,8 @@
>
> **当前版本注意事项**
>
> - `Instance` 页面尚在完善中,目前仅基于官方样式进行了微调
> - 延迟信息图表的有较大问题仍需优化
> - [ ] `Instance` 页面尚在完善中,目前仅基于官方样式进行了微调
> - [x] 延迟信息图表的有较大问题仍需优化(优化完成)
>
> 如果您对以上页面的功能和展示有较高要求,建议暂时选用 [社区中的其他主题](https://komari-document.pages.dev/community/theme)。

View File

@@ -2,7 +2,7 @@
"name": "Komari Theme PurCarte",
"short": "PurCarte",
"description": "A frosted glass theme for Komari",
"version": "0.1.1",
"version": "0.1.2",
"author": "Montia & Gemini",
"url": "https://github.com/Montia37/Komari-theme-purcarte",
"preview": "preview.png"

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useMemo, useState, useEffect, useRef } from "react";
import { memo, useCallback, useState, useEffect, useRef } from "react";
import {
AreaChart,
Area,
@@ -25,17 +25,8 @@ interface LoadChartsProps {
liveData?: NodeStats;
}
const ChartTitle = (text: string, left: React.ReactNode) => {
return (
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{text}</CardTitle>
<span className="text-sm font-bold">{left}</span>
</CardHeader>
);
};
const LoadCharts = memo(
({ node, hours, data = [], liveData: live_data }: LoadChartsProps) => {
({ node, hours, data = [], liveData }: LoadChartsProps) => {
const { getLoadHistory } = useNodeData();
const [historicalData, setHistoricalData] = useState<RecordFormat[]>([]);
const [loading, setLoading] = useState(true);
@@ -43,6 +34,7 @@ const LoadCharts = memo(
const isRealtime = hours === 0;
// 获取历史数据
useEffect(() => {
if (!isRealtime) {
const fetchHistoricalData = async () => {
@@ -52,19 +44,18 @@ const LoadCharts = memo(
const data = await getLoadHistory(node.uuid, hours);
setHistoricalData(data?.records || []);
} catch (err: any) {
setError(err.message || "Failed to fetch historical data");
setError(err.message || "获取历史数据失败");
} finally {
setLoading(false);
}
};
fetchHistoricalData();
} else {
// For realtime, we expect data to be passed via props.
// We can set loading to false if data is present.
setLoading(false);
}
}, [node.uuid, hours, getLoadHistory, isRealtime]);
// 准备图表数据
const minute = 60;
const hour = minute * 60;
const chartData = isRealtime
@@ -81,13 +72,22 @@ const LoadCharts = memo(
maxGap
);
})();
const chartDataLengthRef = useRef(0);
chartDataLengthRef.current = chartData.length;
// 内存图表数据转换
const memoryChartData = chartData.map((item) => ({
...item,
ram: ((item.ram ?? 0) / (node?.mem_total ?? 1)) * 100,
ram_raw: item.ram,
swap: ((item.swap ?? 0) / (node?.swap_total ?? 1)) * 100,
swap_raw: item.swap,
}));
// 格式化函数
const timeFormatter = useCallback((value: any, index: number) => {
if (chartDataLengthRef.current === 0) {
return "";
}
if (chartDataLengthRef.current === 0) return "";
if (index === 0 || index === chartDataLengthRef.current - 1) {
return new Date(value).toLocaleTimeString([], {
hour: "2-digit",
@@ -97,7 +97,7 @@ const LoadCharts = memo(
return "";
}, []);
const lableFormatter = useCallback(
const labelFormatter = useCallback(
(value: any) => {
const date = new Date(value);
if (hours === 0) {
@@ -117,102 +117,180 @@ const LoadCharts = memo(
[hours]
);
// 样式和颜色
const cn = "flex flex-col w-full h-full gap-4 justify-between";
const chartMargin = {
top: 10,
right: 10,
bottom: 10,
left: 10,
};
const chartMargin = { top: 10, right: 16, bottom: 10, left: 16 };
const colors = ["#F38181", "#FCE38A", "#EAFFD0", "#95E1D3"];
const primaryColor = colors[0];
const secondaryColor = colors[1];
const percentageFormatter = (value: number) => {
return `${value.toFixed(2)}%`;
};
const memoryChartData = useMemo(() => {
return chartData.map((item) => ({
time: item.time,
ram: ((item.ram ?? 0) / (node?.mem_total ?? 1)) * 100,
ram_raw: item.ram,
swap: ((item.swap ?? 0) / (node?.swap_total ?? 1)) * 100,
swap_raw: item.swap,
}));
}, [chartData, node?.mem_total, node?.swap_total]);
// 图表配置
const chartConfigs = [
{
id: "cpu",
title: "CPU",
type: "area",
value: liveData?.cpu?.usage ? `${liveData.cpu.usage.toFixed(2)}%` : "-",
dataKey: "cpu",
yAxisDomain: [0, 100],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? `${value}%` : "",
color: colors[0],
data: chartData,
tooltipFormatter: (value: number) => `${value.toFixed(2)}%`,
tooltipLabel: "CPU 使用率",
},
{
id: "memory",
title: "内存",
type: "area",
value: (
<Flex gap="0" direction="column" align="end" className="text-sm">
<label>
{liveData?.ram?.used
? `${formatBytes(liveData.ram.used)} / ${formatBytes(
node?.mem_total || 0
)}`
: "-"}
</label>
<label>
{liveData?.swap?.used
? `${formatBytes(liveData.swap.used)} / ${formatBytes(
node?.swap_total || 0
)}`
: "-"}
</label>
</Flex>
),
series: [
{
dataKey: "ram",
color: colors[0],
tooltipLabel: "内存",
tooltipFormatter: (value: number, raw: any) =>
`${formatBytes(raw?.ram_raw || 0)} (${value.toFixed(0)}%)`,
},
{
dataKey: "swap",
color: colors[1],
tooltipLabel: "交换",
tooltipFormatter: (value: number, raw: any) =>
`${formatBytes(raw?.swap_raw || 0)} (${value.toFixed(0)}%)`,
},
],
yAxisDomain: [0, 100],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? `${value}%` : "",
data: memoryChartData,
},
{
id: "disk",
title: "磁盘",
type: "area",
value: liveData?.disk?.used
? `${formatBytes(liveData.disk.used)} / ${formatBytes(
node?.disk_total || 0
)}`
: "-",
dataKey: "disk",
yAxisDomain: [0, node?.disk_total || 100],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? formatBytes(value) : "",
color: colors[0],
data: chartData,
tooltipFormatter: (value: number) => formatBytes(value),
tooltipLabel: "磁盘使用",
},
{
id: "network",
title: "网络",
type: "line",
value: (
<>
<Flex gap="0" align="end" direction="column" className="text-sm">
<span> {formatBytes(liveData?.network.up || 0)}/s</span>
<span> {formatBytes(liveData?.network.down || 0)}/s</span>
</Flex>
</>
),
series: [
{
dataKey: "net_in",
color: colors[0],
tooltipLabel: "下载",
tooltipFormatter: (value: number) => `${formatBytes(value)}/s`,
},
{
dataKey: "net_out",
color: colors[3],
tooltipLabel: "上传",
tooltipFormatter: (value: number) => `${formatBytes(value)}/s`,
},
],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? formatBytes(value) : "",
data: chartData,
},
{
id: "connections",
title: "连接数",
type: "line",
value: (
<Flex gap="0" align="end" direction="column" className="text-sm">
<span>TCP: {liveData?.connections.tcp}</span>
<span>UDP: {liveData?.connections.udp}</span>
</Flex>
),
series: [
{
dataKey: "connections",
color: colors[0],
tooltipLabel: "TCP 连接",
},
{
dataKey: "connections_udp",
color: colors[1],
tooltipLabel: "UDP 连接",
},
],
data: chartData,
},
{
id: "process",
title: "进程数",
type: "line",
value: liveData?.process || "-",
dataKey: "process",
color: colors[0],
data: chartData,
tooltipLabel: "进程数",
},
];
// 通用自定义 Tooltip 组件
const CustomTooltip = ({ active, payload, label, chartType }: any) => {
// 通用提示组件
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">
{lableFormatter(label)}
{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;
let displayName = item.name || item.dataKey;
// 根据图表类型和 dataKey 格式化值和显示名称
switch (chartType) {
case "cpu":
value = percentageFormatter(value);
displayName = "CPU 使用率";
break;
case "memory":
if (item.dataKey === "ram") {
const rawValue = item.payload?.ram_raw;
if (rawValue !== undefined) {
value = `${formatBytes(rawValue)} (${value.toFixed(0)}%)`;
} else {
value = percentageFormatter(value);
}
displayName = "内存";
} else if (item.dataKey === "swap") {
const rawValue = item.payload?.swap_raw;
if (rawValue !== undefined) {
value = `${formatBytes(rawValue)} (${value.toFixed(0)}%)`;
} else {
value = percentageFormatter(value);
}
displayName = "交换";
}
break;
case "disk":
value = formatBytes(value);
displayName = "磁盘使用";
break;
case "network":
if (item.dataKey === "net_in") {
value = `${formatBytes(value)}/s`;
displayName = "下载";
} else if (item.dataKey === "net_out") {
value = `${formatBytes(value)}/s`;
displayName = "上传";
}
break;
case "connections":
if (item.dataKey === "connections") {
displayName = "TCP 连接";
} else if (item.dataKey === "connections_udp") {
displayName = "UDP 连接";
}
value = value.toString();
break;
case "process":
displayName = "进程数";
value = value.toString();
break;
default:
value = value.toString();
if (series?.tooltipFormatter) {
value = series.tooltipFormatter(value, item.payload);
} else {
value = value.toString();
}
return (
@@ -225,7 +303,7 @@ const LoadCharts = memo(
style={{ backgroundColor: item.color }}
/>
<span className="text-sm font-medium text-foreground">
{displayName}:
{series?.tooltipLabel || item.dataKey}:
</span>
</div>
<span className="text-sm font-bold ml-2">{value}</span>
@@ -237,6 +315,90 @@ const LoadCharts = memo(
);
};
// 根据配置渲染图表
const renderChart = (config: any) => {
const ChartComponent = config.type === "area" ? AreaChart : LineChart;
const DataComponent =
config.type === "area" ? Area : (Line as React.ComponentType<any>);
const chartConfig = config.series
? config.series.reduce((acc: any, series: any) => {
acc[series.dataKey] = {
label: series.tooltipLabel || series.dataKey,
color: series.color,
};
return acc;
}, {})
: {
[config.dataKey]: {
label: config.tooltipLabel || config.dataKey,
color: config.color,
},
};
return (
<Card className={cn} key={config.id}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{config.title}
</CardTitle>
<span className="text-sm font-bold">{config.value}</span>
</CardHeader>
<ChartContainer config={chartConfig}>
<ChartComponent data={config.data} margin={chartMargin}>
<CartesianGrid strokeDasharray="2 4" vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 11 }}
tickFormatter={timeFormatter}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
domain={config.yAxisDomain}
tickFormatter={config.yAxisFormatter}
orientation="left"
type="number"
tick={{ dx: -10 }}
mirror={true}
/>
<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
dataKey={config.dataKey}
animationDuration={0}
stroke={config.color}
fill={config.type === "area" ? config.color : undefined}
opacity={0.8}
dot={false}
/>
)}
</ChartComponent>
</ChartContainer>
</Card>
);
};
return (
<div className="relative">
{loading && (
@@ -250,381 +412,7 @@ const LoadCharts = memo(
</div>
)}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* CPU */}
<Card className={cn}>
{ChartTitle(
"CPU",
live_data?.cpu?.usage ? `${live_data.cpu.usage.toFixed(2)}%` : "-"
)}
<ChartContainer
config={{
cpu: {
label: "CPU",
color: primaryColor,
},
}}>
<AreaChart data={chartData} margin={chartMargin}>
<CartesianGrid
strokeDasharray="2 4"
vertical={false}
stroke="var(--gray-a3)"
/>
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 11 }}
tickFormatter={timeFormatter}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
domain={[0, 100]}
tickFormatter={(value, index) =>
index !== 0 ? `${value}%` : ""
}
orientation="left"
type="number"
tick={{ dx: -10 }}
mirror={true}
/>
<Tooltip
cursor={false}
content={(props) => (
<CustomTooltip {...props} chartType="cpu" />
)}
/>
<Area
dataKey="cpu"
animationDuration={0}
stroke={primaryColor}
fill={primaryColor}
opacity={0.8}
dot={false}
/>
</AreaChart>
</ChartContainer>
</Card>
{/* Ram */}
<Card className={cn}>
{ChartTitle(
"内存",
<Flex gap="0" direction="column" align="end" className="text-sm">
<label>
{live_data?.ram?.used
? `${formatBytes(live_data.ram.used)} / ${formatBytes(
node?.mem_total || 0
)}`
: "-"}
</label>
<label>
{live_data?.swap?.used
? `${formatBytes(live_data.swap.used)} / ${formatBytes(
node?.swap_total || 0
)}`
: "-"}
</label>
</Flex>
)}
<ChartContainer
config={{
ram: {
label: "Ram",
color: primaryColor,
},
swap: {
label: "Swap",
color: secondaryColor,
},
}}>
<AreaChart data={memoryChartData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 11 }}
tickFormatter={timeFormatter}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
domain={[0, 100]}
tickFormatter={(value, index) =>
index !== 0 ? `${value}%` : ""
}
orientation="left"
type="number"
tick={{ dx: -10 }}
mirror={true}
/>
<Tooltip
cursor={false}
content={(props) => (
<CustomTooltip {...props} chartType="memory" />
)}
/>
<Area
dataKey="ram"
animationDuration={0}
stroke={primaryColor}
fill={primaryColor}
opacity={0.8}
dot={false}
/>
<Area
dataKey="swap"
animationDuration={0}
stroke={secondaryColor}
fill={secondaryColor}
opacity={0.8}
dot={false}
/>
</AreaChart>
</ChartContainer>
</Card>
{/* Disk */}
<Card className={cn}>
{ChartTitle(
"磁盘",
live_data?.disk?.used
? `${formatBytes(live_data.disk.used)} / ${formatBytes(
node?.disk_total || 0
)}`
: "-"
)}
<ChartContainer
config={{
disk: {
label: "Disk",
color: primaryColor,
},
}}>
<AreaChart data={chartData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 11 }}
tickFormatter={timeFormatter}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
domain={[0, node?.disk_total || 100]}
tickFormatter={(value, index) =>
index !== 0 ? `${formatBytes(value)}` : ""
}
orientation="left"
type="number"
tick={{ dx: -10 }}
mirror={true}
/>
<Tooltip
cursor={false}
content={(props) => (
<CustomTooltip {...props} chartType="disk" />
)}
/>
<Area
dataKey="disk"
animationDuration={0}
stroke={primaryColor}
fill={primaryColor}
opacity={0.8}
dot={false}
/>
</AreaChart>
</ChartContainer>
</Card>
{/* Network */}
<Card className={cn}>
{ChartTitle(
"网络",
<Flex gap="0" align="end" direction="column" className="text-sm">
<span> {formatBytes(live_data?.network.up || 0)}/s</span>
<span> {formatBytes(live_data?.network.down || 0)}/s</span>
</Flex>
)}
<ChartContainer
config={{
net_in: {
label: "网络下载",
color: primaryColor,
},
net_out: {
label: "网络上传",
color: colors[3],
},
}}>
<LineChart data={chartData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 11 }}
tickFormatter={timeFormatter}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
tickFormatter={(value, index) =>
index !== 0 ? `${formatBytes(value)}` : ""
}
orientation="left"
type="number"
tick={{ dx: -10 }}
mirror={true}
/>
<Tooltip
cursor={false}
content={(props) => (
<CustomTooltip {...props} chartType="network" />
)}
/>
<Line
dataKey="net_in"
animationDuration={0}
stroke={primaryColor}
fill={primaryColor}
opacity={0.8}
dot={false}
/>
<Line
dataKey="net_out"
animationDuration={0}
stroke={colors[3]}
fill={colors[3]}
opacity={0.8}
dot={false}
/>
</LineChart>
</ChartContainer>
</Card>
{/* Connections */}
<Card className={cn}>
{ChartTitle(
"连接数",
<Flex gap="0" align="end" direction="column" className="text-sm">
<span>TCP: {live_data?.connections.tcp}</span>
<span>UDP: {live_data?.connections.udp}</span>
</Flex>
)}
<ChartContainer
config={{
connections: {
label: "TCP",
color: primaryColor,
},
connections_udp: {
label: "UDP",
color: colors[3],
},
}}>
<LineChart data={chartData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 11 }}
tickFormatter={timeFormatter}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
tickFormatter={(value, index) =>
index !== 0 ? `${value}` : ""
}
orientation="left"
type="number"
tick={{ dx: -10 }}
mirror={true}
/>
<Tooltip
cursor={false}
content={(props) => (
<CustomTooltip {...props} chartType="connections" />
)}
/>
<Line
dataKey="connections"
animationDuration={0}
stroke={primaryColor}
opacity={0.8}
dot={false}
name="TCP"
/>
<Line
dataKey="connections_udp"
animationDuration={0}
stroke={secondaryColor}
opacity={0.8}
dot={false}
name="UDP"
/>
</LineChart>
</ChartContainer>
</Card>
{/* Process */}
<Card className={cn}>
{ChartTitle("进程数", live_data?.process || "-")}
<ChartContainer
config={{
process: {
label: "进程数",
color: primaryColor,
},
}}>
<LineChart data={chartData} margin={chartMargin}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 11 }}
tickFormatter={timeFormatter}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
tickFormatter={(value, index) =>
index !== 0 ? `${value}` : ""
}
orientation="left"
type="number"
tick={{ dx: -10 }}
mirror={true}
/>
<Tooltip
cursor={false}
content={(props) => (
<CustomTooltip {...props} chartType="process" />
)}
/>
<Line
dataKey="process"
animationDuration={0}
stroke={primaryColor}
opacity={0.8}
dot={false}
/>
</LineChart>
</ChartContainer>
</Card>
{chartConfigs.map(renderChart)}
</div>
</div>
);

View File

@@ -59,21 +59,12 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
if (!pingHistory || !pingHistory.records || !pingHistory.tasks) return [];
const grouped: Record<string, any> = {};
const timeKeys: number[] = [];
// 优化将2秒窗口内的点分组以合并几乎同时的记录
for (const rec of pingHistory.records) {
const t = new Date(rec.time).getTime();
let foundKey = null;
for (const key of timeKeys) {
if (Math.abs(key - t) <= 1500) {
foundKey = key;
break;
}
}
const useKey = foundKey !== null ? foundKey : t;
const useKey = Math.round(t / 2000) * 2000;
if (!grouped[useKey]) {
grouped[useKey] = { time: useKey };
if (foundKey === null) timeKeys.push(useKey);
}
grouped[useKey][rec.task_id] = rec.value === -1 ? null : rec.value;
}
@@ -82,17 +73,31 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
if (hours !== 0) {
const task = pingHistory.tasks;
let interval = task[0]?.interval || 60;
let maxGap = interval * 1.2;
const selectedHours = timeRange
? (timeRange[1] - timeRange[0]) / (1000 * 60 * 60)
: hours;
let interval = task[0]?.interval || 60; // base interval in seconds
const maxGap = interval * 1.2;
if (selectedHours > 24) {
interval *= 60;
// 使用固定的 hours 值进行降采样计算,不依赖 timeRange
const selectedDurationHours = hours;
const totalDurationSeconds = hours * 60 * 60;
// 根据所选视图调整间隔,进行更积极的降采样
if (selectedDurationHours > 30 * 24) {
// > 30 天
interval = 60 * 60; // 1 hour
} else if (selectedDurationHours > 7 * 24) {
// > 7 天
interval = 15 * 60; // 15 minutes
} else if (selectedDurationHours > 24) {
// > 1 天
interval = 5 * 60; // 5 minutes
}
full = fillMissingTimePoints(full, interval, hours * 60 * 60, maxGap);
full = fillMissingTimePoints(
full,
interval,
totalDurationSeconds,
maxGap
);
full = full.map((d: any) => ({
...d,
@@ -100,13 +105,27 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
}));
}
// 添加渲染硬限制以防止崩溃,即使在间隔调整后也是如此
const MAX_POINTS_TO_RENDER = 0;
if (full.length > MAX_POINTS_TO_RENDER && MAX_POINTS_TO_RENDER > 0) {
console.log(
`数据量过大 (${full.length}), 降采样至 ${MAX_POINTS_TO_RENDER} 个点。`
);
const samplingFactor = Math.ceil(full.length / MAX_POINTS_TO_RENDER);
const sampledData = [];
for (let i = 0; i < full.length; i += samplingFactor) {
sampledData.push(full[i]);
}
full = sampledData;
}
if (cutPeak && pingHistory.tasks.length > 0) {
const taskKeys = pingHistory.tasks.map((task) => String(task.id));
full = cutPeakValues(full, taskKeys);
}
return full;
}, [pingHistory, hours, cutPeak, timeRange]);
}, [pingHistory, hours, cutPeak]);
const handleTaskVisibilityToggle = (taskId: number) => {
setVisiblePingTasks((prev) =>
@@ -172,7 +191,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
<Button
variant={isVisible ? "default" : "outline"}
size="sm"
className="h-8 px-2"
className="h-auto px-2 py-1 flex flex-col"
onClick={() => handleTaskVisibilityToggle(task.id)}
style={{
backgroundColor: isVisible
@@ -180,8 +199,8 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
: undefined,
color: isVisible ? "white" : undefined,
}}>
{task.name}
<span className="text-xs mt-1">
<div>{task.name}</div>
<span className="text-xs">
{loss.toFixed(1)}% | {min.toFixed(0)}ms
</span>
</Button>
@@ -195,13 +214,11 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
{pingHistory?.tasks && pingHistory.tasks.length > 0 ? (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<CartesianGrid strokeDasharray="2 4" vertical={false} />
<XAxis
type="number"
dataKey="time"
{...(chartData.length > 1 && {
domain: ["dataMin", "dataMax"],
})}
domain={timeRange || ["dataMin", "dataMax"]}
tickFormatter={(time) => {
const date = new Date(time);
if (hours === 0) {
@@ -239,6 +256,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
dataKey="time"
height={30}
stroke="#8884d8"
alwaysShowText
tickFormatter={(time) => {
const date = new Date(time);
if (hours === 0) {
@@ -250,6 +268,8 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
return date.toLocaleDateString([], {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}}
onChange={(e: any) => {

View File

@@ -232,10 +232,11 @@ export function cutPeakValues<T extends { [key: string]: any }>(
if (currentValue != null && typeof currentValue === "number") {
if (ewma === null) {
// 第一个有效值作为初始EWMA值
ewma = currentValue;
ewma = Math.round(currentValue * 100) / 100;
} else {
// EWMA = α * 当前值 + (1-α) * 前一个EWMA值
ewma = alpha * currentValue + (1 - alpha) * ewma;
ewma =
Math.round((alpha * currentValue + (1 - alpha) * ewma) * 100) / 100;
}
result[i] = { ...result[i], [key]: ewma };
} else if (ewma !== null) {