330 lines
9.7 KiB
TypeScript
330 lines
9.7 KiB
TypeScript
/**
|
||
* 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,
|
||
};
|
||
};
|