import { ReactNode, useId } from 'react' import { RefreshCw } from 'lucide-react' import { useTheme } from '../contexts/ThemeContext' export type StatsRangeKey = '30m' | '1h' | '1d' | '1w' export type ChartPoint = { ts: number value: number } export type ResourceChartSeries = { label: string points: ChartPoint[] current?: number color?: string } export type ResourceChartConfig = { title: string icon: ReactNode points: ChartPoint[] current: number series?: ResourceChartSeries[] detail?: string max?: number unitLabel?: string formatValue: (value: number) => string } const chartPalette = ['#2563eb', '#16a34a', '#d97706', '#dc2626'] const rangeLabels: Record = { '30m': '30分钟', '1h': '1小时', '1d': '1天', '1w': '1周', } export const statsRanges: Record = { '30m': 30 * 60 * 1000, '1h': 60 * 60 * 1000, '1d': 24 * 60 * 60 * 1000, '1w': 7 * 24 * 60 * 60 * 1000, } export default function ResourceStatsPanel({ range, onRangeChange, onRefresh, charts, }: { range: StatsRangeKey onRangeChange: (range: StatsRangeKey) => void onRefresh: () => void charts: ResourceChartConfig[] }) { return (

统计信息

{(Object.keys(rangeLabels) as StatsRangeKey[]).map((item) => ( ))}
{charts.map((chart, index) => ( ))}
) } function DetailedChart({ chart, range, className }: { chart: ResourceChartConfig; range: StatsRangeKey; className: string }) { const series = chart.series?.length ? chart.series : [{ label: chart.title, points: chart.points, current: chart.current }] const primaryStats = getSeriesStats(series[0], chart.current) return (
{chart.icon} {chart.title}
{chart.detail &&

{chart.detail}

}
{series.length > 1 ? (
{series.map((item, index) => ( ))}
) : (
)}
) } function SeriesStat({ color, label, stats, formatValue, }: { color: string label: string stats: { current: number; avg: number; peak: number } formatValue: (value: number) => string }) { return (
{label}
{formatValue(stats.current)}
均 {formatValue(stats.avg)} / 峰 {formatValue(stats.peak)}
) } function Stat({ label, value }: { label: string; value: string }) { return (
{label}
{value}
) } function getSeriesStats(series: ResourceChartSeries, fallbackCurrent = 0) { const values = series.points .map((point) => point.value) .filter((value) => Number.isFinite(value)) const current = Number.isFinite(series.current) ? Number(series.current) : fallbackCurrent const samples = values.length > 0 ? values : [current] const avg = samples.reduce((sum, value) => sum + value, 0) / samples.length const peak = Math.max(current, ...samples, 0) return { current, avg, peak } } function LineAreaChart({ series, range, max, formatValue, unitLabel, }: { series: ResourceChartSeries[] range: StatsRangeKey max?: number formatValue: (value: number) => string unitLabel?: string }) { const { theme } = useTheme() const isDark = theme === 'dark' const gradientId = `resource-chart-fill-${useId().replace(/:/g, '')}` const width = 520 const height = 150 const left = 50 const right = 10 const top = 8 const bottom = 28 const innerWidth = width - left - right const innerHeight = height - top - bottom const now = Date.now() const chartSeries = series.map((item) => { const validPoints = item.points.filter((point) => Number.isFinite(point.ts) && Number.isFinite(point.value)) return { ...item, points: validPoints.length > 0 ? validPoints : [{ ts: now, value: Number.isFinite(item.current) ? Number(item.current) : 0 }], } }) const allPoints = chartSeries.flatMap((item) => item.points) const maxValue = Math.max(max || 0, ...allPoints.map((point) => point.value), 1) const maxTs = now const minTs = now - statsRanges[range] const span = Math.max(maxTs - minTs, 1) const yTicks = [1, 0.5, 0] const xTicks = [0, 0.5, 1] // Dark mode colors const gridStroke = isDark ? '#374151' : '#e5e7eb' const gridStrokeV = isDark ? '#1f2937' : '#edf0f2' const axisStroke = isDark ? '#9ca3af' : '#888' const lineStroke = isDark ? '#f9fafb' : '#444' const gradientTop = isDark ? '#f9fafb' : '#555' const gradientBottom = isDark ? '#374151' : '#555' const primaryLine = buildLine(chartSeries[0]?.points || [{ ts: now, value: 0 }], minTs, span, left, top, innerWidth, innerHeight, maxValue) const area = `${left},${top + innerHeight} ${primaryLine} ${left + innerWidth},${top + innerHeight}` return ( {yTicks.map((tick) => { const y = top + (1 - tick) * innerHeight return ( {formatValue(maxValue * tick)} ) })} {xTicks.map((tick) => { const x = left + tick * innerWidth const ts = minTs + tick * span return ( {formatTime(ts)} ) })} {unitLabel && ( {unitLabel} )} {chartSeries.length === 1 && } {chartSeries.map((item, index) => ( ))} ) } function buildLine( points: ChartPoint[], minTs: number, span: number, left: number, top: number, innerWidth: number, innerHeight: number, maxValue: number, ) { const coords = points.map((point) => { const x = left + ((point.ts - minTs) / span) * innerWidth const y = top + innerHeight - (point.value / maxValue) * innerHeight return `${Number.isFinite(x) ? x : left},${Number.isFinite(y) ? y : top + innerHeight}` }) if (coords.length > 1) return coords.join(' ') const [, yText] = (coords[0] || `${left},${top + innerHeight}`).split(',') const y = Number(yText) const safeY = Number.isFinite(y) ? y : top + innerHeight return `${left},${safeY} ${left + innerWidth},${safeY}` } function chartBorderClass(index: number) { const right = index % 2 === 0 ? 'xl:border-r' : '' const top = index > 1 ? 'border-t' : '' return `${right} ${top} border-gray-200` } function formatTime(ts: number) { return new Date(ts).toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }) }