"use client"; import { useEffect, useRef } from 'react'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler, ChartData, ChartOptions, TooltipItem } from 'chart.js'; import { TimeSeriesData } from '../../api/types'; // 注册 Chart.js 组件 ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler ); interface TimeSeriesChartProps { data: TimeSeriesData[]; } export default function TimeSeriesChart({ data }: TimeSeriesChartProps) { const chartRef = useRef(null); const chartInstance = useRef(null); useEffect(() => { if (!chartRef.current) return; // 销毁旧的图表实例 if (chartInstance.current) { chartInstance.current.destroy(); } const ctx = chartRef.current.getContext('2d'); if (!ctx) return; // 准备数据 const labels = data.map(item => { const date = new Date(item.timestamp); return date.toLocaleDateString(); }); const eventsData = data.map(item => Number(item.events)); const visitorsData = data.map(item => Number(item.visitors)); const conversionsData = data.map(item => Number(item.conversions)); // 创建新的图表实例 chartInstance.current = new ChartJS(ctx, { type: 'line', data: { labels, datasets: [ { label: 'Events', data: eventsData, borderColor: 'rgb(59, 130, 246)', // blue-500 backgroundColor: 'rgba(59, 130, 246, 0.1)', tension: 0.4, fill: true }, { label: 'Visitors', data: visitorsData, borderColor: 'rgb(16, 185, 129)', // green-500 backgroundColor: 'rgba(16, 185, 129, 0.1)', tension: 0.4, fill: true }, { label: 'Conversions', data: conversionsData, borderColor: 'rgb(239, 68, 68)', // red-500 backgroundColor: 'rgba(239, 68, 68, 0.1)', tension: 0.4, fill: true } ] } as ChartData<'line'>, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { position: 'top', labels: { usePointStyle: true, padding: 20, color: 'rgb(156, 163, 175)' // gray-400 } }, tooltip: { mode: 'index', intersect: false, backgroundColor: 'rgb(31, 41, 55)', // gray-800 titleColor: 'rgb(229, 231, 235)', // gray-200 bodyColor: 'rgb(229, 231, 235)', // gray-200 borderColor: 'rgb(75, 85, 99)', // gray-600 borderWidth: 1, padding: 12, displayColors: true, callbacks: { title: (items: TooltipItem<'line'>[]) => { if (items.length > 0) { const date = new Date(data[items[0].dataIndex].timestamp); return date.toLocaleDateString(); } return ''; } } } }, scales: { x: { grid: { display: false }, ticks: { color: 'rgb(156, 163, 175)' // gray-400 } }, y: { beginAtZero: true, grid: { color: 'rgb(75, 85, 99, 0.1)' // gray-600 with opacity }, ticks: { color: 'rgb(156, 163, 175)', // gray-400 callback: (value: number) => { if (value >= 1000) { return `${(value / 1000).toFixed(1)}k`; } return value; } } } } } as ChartOptions<'line'> }); // 清理函数 return () => { if (chartInstance.current) { chartInstance.current.destroy(); } }; }, [data]); return ( ); }