'use client'; import { useMemo } from 'react'; import type { EChartsOption } from 'echarts'; import ReactECharts from 'echarts-for-react'; import { calculateNiceAxisMax, formatCompactNumber } from '@/lib/utils/metrics'; type TrendChartSeries = { label: string; color: string; fillColor?: string; values: number[]; variant?: 'line' | 'area'; valueFormatter?: (value: number) => string; }; type TrendChartSummaryScope = 'last-point' | 'total'; type TrendChartProps = { labels: string[]; series: TrendChartSeries[]; height?: number; yAxisValueFormatter?: (value: number) => string; showSummary?: boolean; summaryScope?: TrendChartSummaryScope; summaryHint?: string; }; type TooltipParam = { axisValueLabel?: string; color?: string; seriesName?: string; value?: number | string | Array; }; const defaultFormatter = (value: number) => formatCompactNumber(value); function resolveSummaryValue(values: number[], scope: TrendChartSummaryScope) { if (values.length === 0) { return 0; } if (scope === 'total') { return values.reduce((sum, value) => sum + value, 0); } return values[values.length - 1] ?? 0; } export function TrendChart({ labels, series, height = 220, yAxisValueFormatter, showSummary = true, summaryScope = 'last-point', summaryHint, }: TrendChartProps) { const option = useMemo(() => { const axisFormatter = yAxisValueFormatter ?? defaultFormatter; const maxValue = calculateNiceAxisMax( series.flatMap((item) => item.values), ); return { animationDuration: 500, animationEasing: 'cubicOut', grid: { left: 16, right: 16, top: 20, bottom: 20, containLabel: true, }, tooltip: { trigger: 'axis', backgroundColor: 'rgba(15, 23, 42, 0.92)', borderWidth: 0, textStyle: { color: '#e2e8f0', fontSize: 12, }, formatter: (params: unknown) => { const items = Array.isArray(params) ? (params as TooltipParam[]) : []; if (items.length === 0) { return ''; } const header = items[0]?.axisValueLabel ?? ''; const rows = items.map((item) => { const matchedSeries = series.find( (seriesItem) => seriesItem.label === item.seriesName, ); const formatter = matchedSeries?.valueFormatter ?? yAxisValueFormatter ?? defaultFormatter; const rawValue = Array.isArray(item.value) ? item.value[1] : item.value; const numericValue = typeof rawValue === 'number' ? rawValue : Number(rawValue ?? 0); return [ '', ``, `${item.seriesName ?? ''}`, `${formatter(numericValue)}`, '', ].join(''); }); return [header, ...rows].join('
'); }, }, legend: { show: false, }, xAxis: { type: 'category', boundaryGap: false, data: labels, axisLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.24)', }, }, axisTick: { show: false, }, axisLabel: { color: '#94a3b8', margin: 14, }, }, yAxis: { type: 'value', min: 0, max: maxValue, splitNumber: 4, axisLabel: { color: '#94a3b8', formatter: (value: number) => axisFormatter(value), }, splitLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.16)', type: 'dashed', }, }, }, series: series.map((item) => ({ name: item.label, type: 'line', smooth: true, showSymbol: false, symbol: 'circle', symbolSize: 8, lineStyle: { color: item.color, width: 3, }, itemStyle: { color: item.color, }, areaStyle: item.variant === 'area' ? { color: item.fillColor ?? `${item.color}33`, } : undefined, emphasis: { focus: 'series', scale: true, }, data: item.values, })), }; }, [labels, series, yAxisValueFormatter]); if (labels.length === 0 || series.length === 0) { return (
暂无趋势数据
); } return (
{showSummary ? (
{series.map((item) => { const summaryValue = resolveSummaryValue(item.values, summaryScope); const formatter = item.valueFormatter ?? defaultFormatter; return (

{item.label}

{formatter(summaryValue)}

{summaryHint ? (

{summaryHint}

) : null}
); })}
) : null}
); }