// src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js import React, { useState, useEffect, useMemo, useRef } from 'react'; import ReactECharts from 'echarts-for-react'; import dayjs from 'dayjs'; import { fetchKlineData, getCacheKey, klineDataCache } from '../StockDetailPanel/utils/klineDataCache'; /** * 迷你K线图组件 * 显示股票的K线走势(蜡烛图),支持事件时间标记 * * @param {string} stockCode - 股票代码 * @param {string} eventTime - 事件时间(可选) * @param {Function} onClick - 点击回调(可选) * @returns {JSX.Element} */ const MiniKLineChart = React.memo(function MiniKLineChart({ stockCode, eventTime, onClick }) { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const mountedRef = useRef(true); const loadedRef = useRef(false); const dataFetchedRef = useRef(false); // 稳定的事件时间 const stableEventTime = useMemo(() => { return eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : ''; }, [eventTime]); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { if (!stockCode) { setData([]); loadedRef.current = false; dataFetchedRef.current = false; return; } if (dataFetchedRef.current) { return; } // 检查缓存(K线图使用 'daily' 类型) const cacheKey = getCacheKey(stockCode, stableEventTime, 'daily'); const cachedData = klineDataCache.get(cacheKey); if (cachedData && cachedData.length > 0) { setData(cachedData); loadedRef.current = true; dataFetchedRef.current = true; return; } dataFetchedRef.current = true; setLoading(true); // 获取日K线数据 fetchKlineData(stockCode, stableEventTime, 'daily') .then((result) => { if (mountedRef.current) { setData(result); setLoading(false); loadedRef.current = true; } }) .catch(() => { if (mountedRef.current) { setData([]); setLoading(false); loadedRef.current = true; } }); }, [stockCode, stableEventTime]); const chartOption = useMemo(() => { // 提取K线数据 [open, close, low, high] const klineData = data .filter(item => item.open && item.close && item.low && item.high) .map(item => [item.open, item.close, item.low, item.high]); // 日K线使用 date 字段 const dates = data.map(item => item.date || item.time); const hasData = klineData.length > 0; if (!hasData) { return { title: { text: loading ? '加载中...' : '无数据', left: 'center', top: 'middle', textStyle: { color: '#999', fontSize: 10 } } }; } // 计算事件时间标记 let eventMarkLineData = []; if (stableEventTime && Array.isArray(dates) && dates.length > 0) { try { const eventDate = dayjs(stableEventTime).format('YYYY-MM-DD'); const eventIdx = dates.findIndex(d => { const dateStr = typeof d === 'object' ? dayjs(d).format('YYYY-MM-DD') : String(d); return dateStr.includes(eventDate); }); if (eventIdx >= 0) { eventMarkLineData.push({ xAxis: eventIdx, lineStyle: { color: '#FFD700', type: 'solid', width: 1.5 }, label: { show: false } }); } } catch (e) { // 忽略异常 } } return { grid: { left: 2, right: 2, top: 2, bottom: 2, containLabel: false }, xAxis: { type: 'category', data: dates, show: false, boundaryGap: true }, yAxis: { type: 'value', show: false, scale: true }, series: [{ type: 'candlestick', data: klineData, itemStyle: { color: '#ef5350', // 涨(阳线) color0: '#26a69a', // 跌(阴线) borderColor: '#ef5350', // 涨(边框) borderColor0: '#26a69a' // 跌(边框) }, barWidth: '60%', markLine: { silent: true, symbol: 'none', label: { show: false }, data: eventMarkLineData } }], tooltip: { show: false }, animation: false }; }, [data, loading, stableEventTime]); return (