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