'use client'; /** * Lightweight Charts 图表组件 * 支持多指标切换、多窗口布局 */ import { useEffect, useRef, useCallback, useState } from 'react'; import { createChart, ColorType, IChartApi, CandlestickData, LineData, Time, HistogramData, SeriesMarker } from 'lightweight-charts'; import type { KLine } from '@/lib/api'; import type { Trade } from '@/store/gameStore'; import * as indicators from '@/lib/indicators'; interface ChartProps { data: KLine[]; currentIndex: number; className?: string; mainIndicator: string; subIndicator1: string; subIndicator2: string; onCrosshairMove?: (data: KLine | null) => void; initialIndex?: number; // 初始索引,用于判断是否需要滚动到起始位置 history?: Trade[]; // 交易历史,用于显示买卖标记 } export default function Chart({ data, currentIndex, className = '', mainIndicator, subIndicator1, subIndicator2, onCrosshairMove, initialIndex, history = [] }: ChartProps) { const chartContainerRef = useRef(null); const chartRef = useRef(null); const seriesRef = useRef({}); // 存储所有 series 引用 const [legendData, setLegendData] = useState(null); // 使用 ref 存储最新的数据和回调,避免闭包陷阱 const dataRef = useRef(data); const currentIndexRef = useRef(currentIndex); const onCrosshairMoveRef = useRef(onCrosshairMove); // 记录上一次的数据长度,用于检测游戏开始或重开 const prevDataLengthRef = useRef(0); // 记录上一次的 initialIndex,用于检测游戏是否重开 const prevInitialIndexRef = useRef(undefined); // 同步更新 refs useEffect(() => { dataRef.current = data; }, [data]); useEffect(() => { currentIndexRef.current = currentIndex; }, [currentIndex]); useEffect(() => { onCrosshairMoveRef.current = onCrosshairMove; }, [onCrosshairMove]); // 转换 K 线数据 const convertToChartData = useCallback((klines: KLine[], endIndex: number): CandlestickData[] => { return klines.slice(0, endIndex + 1).map((k) => ({ time: k.date as Time, open: k.open, high: k.high, low: k.low, close: k.close, })); }, []); // 初始化图表 useEffect(() => { if (!chartContainerRef.current) return; const chart = createChart(chartContainerRef.current, { layout: { background: { type: ColorType.Solid, color: '#0D1421' }, textColor: '#9CA3AF', fontFamily: "'Roboto', sans-serif", }, grid: { vertLines: { color: '#1F2937' }, horzLines: { color: '#1F2937' }, }, width: chartContainerRef.current.clientWidth, height: chartContainerRef.current.clientHeight, crosshair: { mode: 1, vertLine: { color: '#6B7280', width: 1, style: 3, labelBackgroundColor: '#374151', }, horzLine: { color: '#6B7280', width: 1, style: 3, labelBackgroundColor: '#374151', }, }, timeScale: { borderColor: '#374151', timeVisible: true, rightOffset: 12, barSpacing: 6, minBarSpacing: 0.1, // 允许极致缩小,使 K 线看起来像曲线 }, }); chartRef.current = chart; // 基础 K 线系列 (始终存在) const candlestickSeries = chart.addCandlestickSeries({ upColor: '#FD1050', downColor: '#00F0F0', borderUpColor: '#FD1050', borderDownColor: '#00F0F0', wickUpColor: '#FD1050', wickDownColor: '#00F0F0', }); seriesRef.current.candlestick = candlestickSeries; // 十字光标移动事件 chart.subscribeCrosshairMove((param) => { if (!param.time || !param.point || param.point.x < 0 || param.point.y < 0) { setLegendData(null); // 无光标时,传递 null 表示应显示最新数据 onCrosshairMoveRef.current?.(null); return; } const data: any = { time: param.time }; // 捕获时间 // 获取 K 线数据 const candleData = param.seriesData.get(candlestickSeries) as CandlestickData; if (candleData) { data.open = candleData.open; data.high = candleData.high; data.low = candleData.low; data.close = candleData.close; } // 获取其他指标数据 Object.entries(seriesRef.current).forEach(([key, series]: [string, any]) => { if (key === 'candlestick') return; const val = param.seriesData.get(series); if (val !== undefined) { data[key] = (val as any).value; } }); setLegendData(data); // 根据 time 找到原始 K 线数据并回调 const timeStr = param.time.toString(); const currentData = dataRef.current.slice(0, currentIndexRef.current + 1); const originalKLine = currentData.find(k => k.date === timeStr); if (originalKLine) { onCrosshairMoveRef.current?.(originalKLine); } }); const handleResize = () => { if (chartContainerRef.current && chartRef.current) { chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth, height: chartContainerRef.current.clientHeight, }); } }; const resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(chartContainerRef.current); return () => { resizeObserver.disconnect(); chart.remove(); chartRef.current = null; seriesRef.current = {}; }; }, []); // 布局与指标更新逻辑 useEffect(() => { if (!chartRef.current || data.length === 0) return; const chart = chartRef.current; const currentData = data.slice(0, currentIndex + 1); // 1. 更新 K 线数据 seriesRef.current.candlestick.setData(convertToChartData(data, currentIndex)); // 1.5 添加买卖标记 // 按天去重:同一天只显示一个 B 或 S const dailyMarkers = new Map(); history.forEach(trade => { const date = trade.date; // 如果同一天既有买又有卖,保留买入标记(或者你可以根据逻辑调整优先级) if (!dailyMarkers.has(date) || trade.type === 'buy') { dailyMarkers.set(date, { type: trade.type }); } }); const markers: SeriesMarker