Files
vf_react/src/components/StockChart/hooks/useKLineChart.ts
2025-11-24 15:07:13 +08:00

248 lines
7.6 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.

/**
* useKLineChart Hook
*
* 管理 KLineChart 实例的初始化、配置和销毁
*/
import { useEffect, useRef, useState } from 'react';
import { init, dispose, registerIndicator } from 'klinecharts';
import type { Chart } from 'klinecharts';
// import { useColorMode } from '@chakra-ui/react'; // ❌ 已移除深色模式支持
import { getTheme, getTimelineTheme } from '../config/klineTheme';
import { CHART_INIT_OPTIONS } from '../config';
import { logger } from '@utils/logger';
import { avgPriceIndicator } from '../indicators/avgPriceIndicator';
export interface UseKLineChartOptions {
/** 图表容器 ID */
containerId: string;
/** 图表高度px */
height?: number;
/** 是否自动调整大小 */
autoResize?: boolean;
/** 图表类型timeline/daily */
chartType?: 'timeline' | 'daily';
}
export interface UseKLineChartReturn {
/** KLineChart 实例 */
chart: Chart | null;
/** 容器 Ref */
chartRef: React.RefObject<HTMLDivElement>;
/** 是否已初始化 */
isInitialized: boolean;
/** 初始化错误 */
error: Error | null;
}
/**
* KLineChart 初始化和生命周期管理 Hook
*
* @param options 配置选项
* @returns UseKLineChartReturn
*
* @example
* const { chart, chartRef, isInitialized } = useKLineChart({
* containerId: 'kline-chart',
* height: 400,
* autoResize: true,
* });
*/
export const useKLineChart = (
options: UseKLineChartOptions
): UseKLineChartReturn => {
const { containerId, height = 400, autoResize = true, chartType = 'daily' } = options;
const chartRef = useRef<HTMLDivElement>(null);
const chartInstanceRef = useRef<Chart | null>(null);
const [chartInstance, setChartInstance] = useState<Chart | null>(null); // ✅ 新增chart state触发重渲染
const [isInitialized, setIsInitialized] = useState(false);
const [error, setError] = useState<Error | null>(null);
// ✅ 固定使用浅色主题(已移除 useColorMode
const colorMode = 'light';
// 全局注册自定义均价线指标(只执行一次)
useEffect(() => {
try {
registerIndicator(avgPriceIndicator);
logger.debug('useKLineChart', '✅ 自定义均价线指标(AVG)注册成功');
} catch (err) {
// 如果已注册会报错,忽略即可
logger.debug('useKLineChart', 'AVG指标已注册或注册失败', err);
}
}, []);
// 图表初始化(添加延迟重试机制,处理 Modal 动画延迟)
useEffect(() => {
// 图表初始化函数
const initChart = (): boolean => {
if (!chartRef.current) {
logger.warn('useKLineChart', 'init', '图表容器未挂载,将在 50ms 后重试', { containerId });
return false;
}
try {
logger.debug('useKLineChart', 'init', '开始初始化图表', {
containerId,
height,
colorMode,
});
// 初始化图表实例KLineChart 10.0 API
// ✅ 根据 chartType 选择主题
const themeStyles = chartType === 'timeline'
? getTimelineTheme(colorMode)
: getTheme(colorMode);
const chartInstance = init(chartRef.current, {
...CHART_INIT_OPTIONS,
// 设置初始样式(根据主题和图表类型)
styles: themeStyles,
});
if (!chartInstance) {
throw new Error('图表初始化失败:返回 null');
}
chartInstanceRef.current = chartInstance;
setChartInstance(chartInstance); // ✅ 新增:更新 state触发重渲染
setIsInitialized(true);
setError(null);
// ✅ 新增:创建成交量指标窗格
try {
const volumePaneId = chartInstance.createIndicator('VOL', false, {
height: 100, // 固定高度 100px约占整体的 20-25%
});
logger.debug('useKLineChart', 'init', '成交量窗格创建成功', {
volumePaneId,
});
} catch (err) {
logger.warn('useKLineChart', 'init', '成交量窗格创建失败', {
error: err,
});
// 不阻塞主流程,继续执行
}
logger.info('useKLineChart', 'init', '✅ 图表初始化成功', {
containerId,
chartId: chartInstance.id,
});
return true;
} catch (err) {
const error = err as Error;
logger.error('useKLineChart', 'init', error, { containerId });
setError(error);
setIsInitialized(false);
return false;
}
};
// 立即尝试初始化
if (initChart()) {
// 成功,直接返回清理函数
return () => {
if (chartInstanceRef.current) {
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
containerId,
chartId: chartInstanceRef.current.id,
});
dispose(chartInstanceRef.current);
chartInstanceRef.current = null;
setChartInstance(null); // ✅ 新增:清空 state
setIsInitialized(false);
}
};
}
// 失败则延迟重试(处理 Modal 动画延迟导致的 DOM 未挂载)
const timer = setTimeout(() => {
logger.debug('useKLineChart', 'init', '执行延迟重试', { containerId });
initChart();
}, 50);
// 清理函数:清除定时器和销毁图表实例
return () => {
clearTimeout(timer);
if (chartInstanceRef.current) {
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
containerId,
chartId: chartInstanceRef.current.id,
});
dispose(chartInstanceRef.current);
chartInstanceRef.current = null;
setChartInstance(null); // ✅ 新增:清空 state
setIsInitialized(false);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerId, chartType]); // containerId 或 chartType 变化时重新初始化
// 主题切换:更新图表样式
useEffect(() => {
if (!chartInstanceRef.current || !isInitialized) {
return;
}
try {
// ✅ 根据 chartType 选择主题
const newTheme = chartType === 'timeline'
? getTimelineTheme(colorMode)
: getTheme(colorMode);
chartInstanceRef.current.setStyles(newTheme);
logger.debug('useKLineChart', 'updateTheme', '更新图表主题', {
colorMode,
chartType,
chartId: chartInstanceRef.current.id,
});
} catch (err) {
logger.error('useKLineChart', 'updateTheme', err as Error, { colorMode, chartType });
}
}, [colorMode, chartType, isInitialized]);
// 容器尺寸变化:调整图表大小
useEffect(() => {
if (!chartInstanceRef.current || !isInitialized || !autoResize) {
return;
}
const handleResize = () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.resize();
logger.debug('useKLineChart', 'resize', '调整图表大小');
}
};
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
// 使用 ResizeObserver 监听容器大小变化(更精确)
let resizeObserver: ResizeObserver | null = null;
if (chartRef.current && typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(chartRef.current);
}
return () => {
window.removeEventListener('resize', handleResize);
if (resizeObserver && chartRef.current) {
resizeObserver.unobserve(chartRef.current);
resizeObserver.disconnect();
}
};
}, [isInitialized, autoResize]);
return {
chart: chartInstance, // ✅ 返回 state 而非 ref确保变化触发重渲染
chartRef,
isInitialized,
error,
};
};