Files
vf_react/src/components/StockChart/hooks/useKLineData.ts

330 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<void>;
/** 更新数据 */
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<KLineDataPoint[]>([]);
const [rawData, setRawData] = useState<RawDataPoint[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(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,
};
};