"use client" import type { NezhaAPIMonitor, ServerMonitorChart } from "@/app/types/nezha-api" import NetworkChartLoading from "@/components/loading/NetworkChartLoading" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart" import { Label } from "@/components/ui/label" import { Switch } from "@/components/ui/switch" import getEnv from "@/lib/env-entry" import { formatTime, nezhaFetcher } from "@/lib/utils" import { useTranslations } from "next-intl" import * as React from "react" import { useCallback, useMemo } from "react" import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts" import useSWR from "swr" interface ResultItem { created_at: number [key: string]: number } export function NetworkChartClient({ server_id, show, }: { server_id: number show: boolean }) { const t = useTranslations("NetworkChartClient") const { data, error } = useSWR( `/api/monitor?server_id=${server_id}`, nezhaFetcher, { refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 15000, isVisible: () => show, }, ) if (error) { return ( <>

{error.message}

{t("chart_fetch_error_message")}

) } if (!data) return const transformedData = transformData(data) const formattedData = formatData(data) const initChartConfig = { avg_delay: { label: t("avg_delay"), }, } satisfies ChartConfig const chartDataKey = Object.keys(transformedData) return ( ) } export const NetworkChart = React.memo(function NetworkChart({ chartDataKey, chartConfig, chartData, serverName, formattedData, }: { chartDataKey: string[] chartConfig: ChartConfig chartData: ServerMonitorChart serverName: string formattedData: ResultItem[] }) { const t = useTranslations("NetworkChart") const defaultChart = "All" const [activeChart, setActiveChart] = React.useState(defaultChart) const [isPeakEnabled, setIsPeakEnabled] = React.useState(false) const handleButtonClick = useCallback( (chart: string) => { setActiveChart((prev) => (prev === chart ? defaultChart : chart)) }, [defaultChart], ) const getColorByIndex = useCallback( (chart: string) => { const index = chartDataKey.indexOf(chart) return `hsl(var(--chart-${(index % 10) + 1}))` }, [chartDataKey], ) const chartButtons = useMemo( () => chartDataKey.map((key) => ( )), [chartDataKey, activeChart, chartData, handleButtonClick], ) const chartLines = useMemo(() => { if (activeChart !== defaultChart) { return ( ) } return chartDataKey.map((key) => ( )) }, [activeChart, defaultChart, chartDataKey, getColorByIndex]) const processedData = useMemo(() => { if (!isPeakEnabled) { return activeChart === defaultChart ? formattedData : chartData[activeChart] } const data = ( activeChart === defaultChart ? formattedData : chartData[activeChart] ) as ResultItem[] const windowSize = 11 // 增加窗口大小以获取更好的统计效果 const alpha = 0.3 // EWMA平滑因子 // 辅助函数:计算中位数 const getMedian = (arr: number[]) => { const sorted = [...arr].sort((a, b) => a - b) const mid = Math.floor(sorted.length / 2) return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 } // 辅助函数:异常值处理 const processValues = (values: number[]) => { if (values.length === 0) return null const median = getMedian(values) const deviations = values.map((v) => Math.abs(v - median)) const medianDeviation = getMedian(deviations) * 1.4826 // MAD估计器 // 使用中位数绝对偏差(MAD)进行异常值检测 const validValues = values.filter( (v) => Math.abs(v - median) <= 3 * medianDeviation && // 更严格的异常值判定 v <= median * 3, // 限制最大值不超过中位数的3倍 ) if (validValues.length === 0) return median // 如果没有有效值,返回中位数 // 计算EWMA let ewma = validValues[0] for (let i = 1; i < validValues.length; i++) { ewma = alpha * validValues[i] + (1 - alpha) * ewma } return ewma } // 初始化EWMA历史值 const ewmaHistory: { [key: string]: number } = {} return data.map((point, index) => { if (index < windowSize - 1) return point const window = data.slice(index - windowSize + 1, index + 1) const smoothed = { ...point } as ResultItem if (activeChart === defaultChart) { for (const key of chartDataKey) { const values = window .map((w) => w[key]) .filter((v) => v !== undefined && v !== null) as number[] if (values.length > 0) { const processed = processValues(values) if (processed !== null) { // 应用EWMA平滑 if (ewmaHistory[key] === undefined) { ewmaHistory[key] = processed } else { ewmaHistory[key] = alpha * processed + (1 - alpha) * ewmaHistory[key] } smoothed[key] = ewmaHistory[key] } } } } else { const values = window .map((w) => w.avg_delay) .filter((v) => v !== undefined && v !== null) as number[] if (values.length > 0) { const processed = processValues(values) if (processed !== null) { // 应用EWMA平滑 if (ewmaHistory.current === undefined) { ewmaHistory.current = processed } else { ewmaHistory.current = alpha * processed + (1 - alpha) * ewmaHistory.current } smoothed.avg_delay = ewmaHistory.current } } } return smoothed }) }, [isPeakEnabled, activeChart, formattedData, chartData, chartDataKey, defaultChart]) return (
{serverName} {chartDataKey.length} {t("ServerMonitorCount")}
{chartButtons}
{ const date = new Date(item.created_at) return date.getMinutes() === 0 && date.getHours() % 3 === 0 }) .map((item) => item.created_at)} tickFormatter={(value) => { const date = new Date(value) return `${date.getHours()}:00` }} /> `${value}ms`} /> { return formatTime(payload[0].payload.created_at) }} /> } /> {activeChart === defaultChart && } />} {chartLines}
) }) const transformData = (data: NezhaAPIMonitor[]) => { const monitorData: ServerMonitorChart = {} for (const item of data) { const monitorName = item.monitor_name if (!monitorData[monitorName]) { monitorData[monitorName] = [] } for (let i = 0; i < item.created_at.length; i++) { monitorData[monitorName].push({ created_at: item.created_at[i], avg_delay: item.avg_delay[i], }) } } return monitorData } const formatData = (rawData: NezhaAPIMonitor[]) => { const result: { [time: number]: ResultItem } = {} const allTimes = new Set() for (const item of rawData) { for (const time of item.created_at) { allTimes.add(time) } } const allTimeArray = Array.from(allTimes).sort((a, b) => a - b) for (const item of rawData) { const { monitor_name, created_at, avg_delay } = item for (const time of allTimeArray) { if (!result[time]) { result[time] = { created_at: time } } const timeIndex = created_at.indexOf(time) // @ts-expect-error - avg_delay is an array result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null } } return Object.values(result).sort((a, b) => a.created_at - b.created_at) }