/** * 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; /** 是否已初始化 */ 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(null); const chartInstanceRef = useRef(null); const [chartInstance, setChartInstance] = useState(null); // ✅ 新增:chart state(触发重渲染) const [isInitialized, setIsInitialized] = useState(false); const [error, setError] = useState(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, }; };