// src/components/Charts/Stock/MiniTimelineChart.js import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import ReactECharts from 'echarts-for-react'; import { echarts } from '@lib/echarts'; import dayjs from 'dayjs'; import { fetchKlineData, getCacheKey, klineDataCache, batchPendingRequests } from '@utils/stock/klineDataCache'; /** * 迷你分时图组件 * 显示股票的分时价格走势,支持事件时间标记 * * @param {string} stockCode - 股票代码 * @param {string} eventTime - 事件时间(可选) * @param {Function} onClick - 点击回调(可选) * @param {Array} preloadedData - 预加载的K线数据(可选,由父组件批量加载后传入) * @param {boolean} loading - 外部加载状态(可选) * @returns {JSX.Element} */ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eventTime, onClick, preloadedData, loading: externalLoading }) { 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; }; }, []); // 从缓存或API获取数据的函数 const loadData = useCallback(() => { if (!stockCode || !mountedRef.current) return false; // 检查缓存(使用 'minute' 类型) const cacheKey = getCacheKey(stockCode, stableEventTime, 'minute'); const cachedData = klineDataCache.get(cacheKey); // 如果有缓存数据(包括空数组,表示已请求过但无数据),直接使用 if (cachedData !== undefined) { setData(cachedData || []); setLoading(false); loadedRef.current = true; dataFetchedRef.current = true; return true; // 表示数据已加载(或确认无数据) } return false; // 表示需要请求 }, [stockCode, stableEventTime]); useEffect(() => { if (!stockCode) { setData([]); loadedRef.current = false; dataFetchedRef.current = false; return; } // 优先使用预加载的数据(由父组件批量请求后传入) if (preloadedData !== undefined) { setData(preloadedData || []); setLoading(false); loadedRef.current = true; dataFetchedRef.current = true; return; } // 如果外部正在加载,显示loading状态,不发起单独请求 // 父组件(StockTable)会通过 preloadedData 传入数据 if (externalLoading) { setLoading(true); return; } // 如果已经请求过数据,不再重复请求 if (dataFetchedRef.current) { return; } // 尝试从缓存加载 if (loadData()) { return; } // 检查批量请求的函数 const checkBatchAndLoad = () => { // 再次检查缓存(批量请求可能已完成,使用 'minute' 类型) const cacheKey = getCacheKey(stockCode, stableEventTime, 'minute'); const cachedData = klineDataCache.get(cacheKey); if (cachedData !== undefined) { setData(cachedData || []); setLoading(false); loadedRef.current = true; dataFetchedRef.current = true; return true; // 从缓存加载成功 } const batchKey = `${stableEventTime || 'today'}|minute`; const pendingBatch = batchPendingRequests.get(batchKey); if (pendingBatch) { // 等待批量请求完成后再从缓存读取 setLoading(true); dataFetchedRef.current = true; pendingBatch.then(() => { if (mountedRef.current) { const newCachedData = klineDataCache.get(cacheKey); setData(newCachedData || []); setLoading(false); loadedRef.current = true; } }).catch(() => { if (mountedRef.current) { setData([]); setLoading(false); } }); return true; // 找到批量请求 } return false; // 没有批量请求 }; // 先立即检查一次 if (checkBatchAndLoad()) { return; } // 延迟检查(等待批量请求启动) // 注意:如果父组件正在批量加载,会传入 externalLoading=true,不会执行到这里 setLoading(true); const timeoutId = setTimeout(() => { if (!mountedRef.current || dataFetchedRef.current) return; // 再次检查批量请求 if (checkBatchAndLoad()) { return; } // 仍然没有批量请求,发起单独请求(备用方案 - 用于非批量加载场景) dataFetchedRef.current = true; // 使用 'minute' 类型获取分钟线数据 fetchKlineData(stockCode, stableEventTime, 'minute') .then((result) => { if (mountedRef.current) { setData(result); setLoading(false); loadedRef.current = true; } }) .catch(() => { if (mountedRef.current) { setData([]); setLoading(false); loadedRef.current = true; } }); }, 200); // 延迟 200ms 等待批量请求(增加等待时间) return () => clearTimeout(timeoutId); }, [stockCode, stableEventTime, loadData, preloadedData, externalLoading]); // 添加 preloadedData 和 externalLoading 依赖 const chartOption = useMemo(() => { const prices = data.map(item => item.close ?? item.price).filter(v => typeof v === 'number'); const times = data.map(item => item.time); const hasData = prices.length > 0; if (!hasData) { return { title: { text: loading ? '加载中...' : '无数据', left: 'center', top: 'middle', textStyle: { color: '#999', fontSize: 10 } } }; } const minPrice = Math.min(...prices); const maxPrice = Math.max(...prices); const isUp = prices[prices.length - 1] >= prices[0]; const lineColor = isUp ? '#ef5350' : '#26a69a'; // 计算事件时间对应的分时索引 let eventMarkLineData = []; if (stableEventTime && Array.isArray(times) && times.length > 0) { try { const eventMinute = dayjs(stableEventTime, 'YYYY-MM-DD HH:mm').format('HH:mm'); const parseMinuteTime = (timeStr) => { const [h, m] = String(timeStr).split(':').map(Number); return h * 60 + m; }; const eventMin = parseMinuteTime(eventMinute); let nearestIdx = 0; for (let i = 1; i < times.length; i++) { if (Math.abs(parseMinuteTime(times[i]) - eventMin) < Math.abs(parseMinuteTime(times[nearestIdx]) - eventMin)) { nearestIdx = i; } } eventMarkLineData.push({ xAxis: nearestIdx, 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: times, show: false, boundaryGap: false }, yAxis: { type: 'value', show: false, min: minPrice * 0.995, max: maxPrice * 1.005, scale: true }, series: [{ data: prices, type: 'line', smooth: true, symbol: 'none', lineStyle: { color: lineColor, width: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: isUp ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)' }, { offset: 1, color: isUp ? 'rgba(239, 83, 80, 0.05)' : 'rgba(38, 166, 154, 0.05)' } ]) }, markLine: { silent: true, symbol: 'none', label: { show: false }, data: [ ...(prices.length ? [{ yAxis: prices[0], lineStyle: { color: '#aaa', type: 'dashed', width: 1 } }] : []), ...eventMarkLineData ] } }], tooltip: { show: false }, animation: false }; }, [data, loading, stableEventTime]); return (
); }, (prevProps, nextProps) => { // 自定义比较函数 return prevProps.stockCode === nextProps.stockCode && prevProps.eventTime === nextProps.eventTime && prevProps.onClick === nextProps.onClick && prevProps.preloadedData === nextProps.preloadedData && prevProps.loading === nextProps.loading; }); export default MiniTimelineChart;