/** * useKLineData Hook * * 管理 K 线数据的加载、转换和更新 */ import { useEffect, useState, useCallback } from 'react'; import type { Chart } from 'klinecharts'; import type { ChartType, KLineDataPoint, RawDataPoint } from '../types'; import { processChartData } from '../utils/dataAdapter'; import { logger } from '@utils/logger'; import { stockService } from '@services/eventService'; import { klineDataCache, getCacheKey } from '@views/Community/components/StockDetailPanel/utils/klineDataCache'; export interface UseKLineDataOptions { /** KLineChart 实例 */ chart: Chart | null; /** 股票代码 */ stockCode: string; /** 图表类型 */ chartType: ChartType; /** 事件时间(用于调整数据加载范围) */ eventTime?: string; /** 是否自动加载数据 */ autoLoad?: boolean; } export interface UseKLineDataReturn { /** 处理后的 K 线数据 */ data: KLineDataPoint[]; /** 原始数据 */ rawData: RawDataPoint[]; /** 是否加载中 */ loading: boolean; /** 加载错误 */ error: Error | null; /** 手动加载数据 */ loadData: () => Promise; /** 更新数据 */ updateData: (newData: KLineDataPoint[]) => void; /** 清空数据 */ clearData: () => void; } /** * K 线数据加载和管理 Hook * * @param options 配置选项 * @returns UseKLineDataReturn * * @example * const { data, loading, error, loadData } = useKLineData({ * chart, * stockCode: '600000.SH', * chartType: 'daily', * eventTime: '2024-01-01 10:00:00', * autoLoad: true, * }); */ export const useKLineData = ( options: UseKLineDataOptions ): UseKLineDataReturn => { const { chart, stockCode, chartType, eventTime, autoLoad = true, } = options; const [data, setData] = useState([]); const [rawData, setRawData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); /** * 加载数据(从后端 API) */ const loadData = useCallback(async () => { if (!stockCode) { logger.warn('useKLineData', 'loadData', '股票代码为空', { chartType }); return; } setLoading(true); setError(null); try { logger.debug('useKLineData', 'loadData', '开始加载数据', { stockCode, chartType, eventTime, }); // 1. 先检查缓存 const cacheKey = getCacheKey(stockCode, eventTime, chartType); const cachedData = klineDataCache.get(cacheKey); let rawDataList; if (cachedData && cachedData.length > 0) { // 使用缓存数据 rawDataList = cachedData; } else { // 2. 缓存没有数据,调用 API 请求 const response = await stockService.getKlineData( stockCode, chartType, eventTime ); if (!response || !response.data) { throw new Error('后端返回数据为空'); } rawDataList = response.data; // 3. 将数据写入缓存(避免下次重复请求) klineDataCache.set(cacheKey, rawDataList); } setRawData(rawDataList); // 数据转换和处理 const processedData = processChartData(rawDataList, chartType, eventTime); setData(processedData); logger.info('useKLineData', 'loadData', '数据加载成功', { stockCode, chartType, rawCount: rawDataList.length, processedCount: processedData.length, }); } catch (err) { const error = err as Error; logger.error('useKLineData', 'loadData', error, { stockCode, chartType, }); setError(error); setData([]); setRawData([]); } finally { setLoading(false); } }, [stockCode, chartType, eventTime]); /** * 更新图表数据(使用 setDataLoader 方法) */ const updateChartData = useCallback( (klineData: KLineDataPoint[]) => { if (!chart || klineData.length === 0) { return; } try { // 步骤 1: 设置 symbol(必需!getBars 调用的前置条件) (chart as any).setSymbol({ ticker: stockCode || 'UNKNOWN', // 股票代码 pricePrecision: 2, // 价格精度(2位小数) volumePrecision: 0 // 成交量精度(整数) }); // 步骤 2: 设置 period(必需!getBars 调用的前置条件) const periodType = chartType === 'timeline' ? 'minute' : 'day'; (chart as any).setPeriod({ type: periodType, // 分时图=minute,日K=day span: 1 // 周期跨度(1分钟/1天) }); // 步骤 3: 设置 DataLoader(同步数据加载器) (chart as any).setDataLoader({ getBars: (params: any) => { if (params.type === 'init') { // 初始化加载:返回完整数据 params.callback(klineData, false); // false = 无更多数据可加载 } else if (params.type === 'forward' || params.type === 'backward') { // 向前/向后加载:我们没有更多数据,返回空数组 params.callback([], false); } } }); // 步骤 4: 触发初始化加载(这会调用 getBars with type="init") (chart as any).resetData(); // 步骤 5: 根据数据量调整可见范围和柱子间距(让 K 线柱子填满图表区域) setTimeout(() => { try { const dataLength = klineData.length; if (dataLength > 0) { // 获取图表容器宽度 const chartDom = (chart as any).getDom(); const chartWidth = chartDom?.clientWidth || 1200; // 计算最优柱子间距 // 公式:barSpace = (图表宽度 / 数据数量) * 0.7 // 0.7 是为了留出一些间距,让图表不会太拥挤 const optimalBarSpace = Math.max(8, Math.min(50, (chartWidth / dataLength) * 0.7)); (chart as any).setBarSpace(optimalBarSpace); // 减少右侧空白(默认值可能是 100-200,调小会减少右侧空白) (chart as any).setOffsetRightDistance(50); } } catch (err) { logger.error('useKLineData', 'updateChartData', err as Error, { step: '调整可见范围失败', }); } }, 100); // 延迟 100ms 确保数据已加载和渲染 // ✅ 步骤 4: 分时图添加均价线(使用自定义 AVG 指标) if (chartType === 'timeline' && klineData.length > 0) { setTimeout(() => { try { // 在主图窗格创建 AVG 均价线指标 (chart as any).createIndicator('AVG', true, { id: 'candle_pane', // 主图窗格 }); console.log('[DEBUG] ✅ 均价线(AVG指标)添加成功'); } catch (err) { console.error('[DEBUG] ❌ 均价线添加失败:', err); } }, 150); // 延迟 150ms,确保数据加载完成后再创建指标 // ✅ 步骤 5: 添加昨收价基准线(灰色虚线) setTimeout(() => { try { const prevClose = klineData[0]?.prev_close; if (prevClose && prevClose > 0) { // 创建水平线覆盖层 (chart as any).createOverlay({ name: 'horizontalStraightLine', id: 'prev_close_line', points: [{ value: prevClose }], styles: { line: { style: 'dashed', dashValue: [4, 2], size: 1, color: '#888888', // 灰色虚线 }, }, extendData: { label: `昨收: ${prevClose.toFixed(2)}`, }, }); console.log('[DEBUG] ✅ 昨收价基准线添加成功:', prevClose); } } catch (err) { console.error('[DEBUG] ❌ 昨收价基准线添加失败:', err); } }, 200); // 延迟 200ms,确保均价线创建完成后再添加 } logger.debug( 'useKLineData', `updateChartData - ${stockCode} (${chartType}) - ${klineData.length}条数据加载成功` ); } catch (err) { logger.error('useKLineData', 'updateChartData', err as Error, { dataCount: klineData.length, }); } }, [chart, stockCode, chartType] ); /** * 手动更新数据(外部调用) */ const updateData = useCallback( (newData: KLineDataPoint[]) => { setData(newData); updateChartData(newData); logger.debug( 'useKLineData', `updateData - ${stockCode} (${chartType}) - ${newData.length}条数据手动更新` ); }, [updateChartData] ); /** * 清空数据 */ const clearData = useCallback(() => { setData([]); setRawData([]); setError(null); if (chart) { chart.resetData(); logger.debug('useKLineData', `clearData - chartId: ${(chart as any).id}`); } }, [chart]); // 自动加载数据(当 stockCode/chartType/eventTime 变化时) useEffect(() => { if (autoLoad && stockCode && chart) { loadData(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [stockCode, chartType, eventTime, autoLoad, chart]); // 数据变化时更新图表 useEffect(() => { if (data.length > 0 && chart) { updateChartData(data); } }, [data, chart, updateChartData]); return { data, rawData, loading, error, loadData, updateData, clearData, }; };