feat: StockChartModal.tsx 替换 KLine 实现

This commit is contained in:
zdl
2025-11-24 13:59:29 +08:00
parent 0383e17a1f
commit c486bf1f43
10 changed files with 768 additions and 630 deletions

View File

@@ -5,12 +5,13 @@
*/
import { useEffect, useRef, useState } from 'react';
import { init, dispose } from 'klinecharts';
import { init, dispose, registerIndicator } from 'klinecharts';
import type { Chart } from 'klinecharts';
import { useColorMode } from '@chakra-ui/react';
import { getTheme } from '../config/klineTheme';
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 */
@@ -19,6 +20,8 @@ export interface UseKLineChartOptions {
height?: number;
/** 是否自动调整大小 */
autoResize?: boolean;
/** 图表类型timeline/daily */
chartType?: 'timeline' | 'daily';
}
export interface UseKLineChartReturn {
@@ -48,57 +51,122 @@ export interface UseKLineChartReturn {
export const useKLineChart = (
options: UseKLineChartOptions
): UseKLineChartReturn => {
const { containerId, height = 400, autoResize = true } = options;
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);
const { colorMode } = useColorMode();
// 图表初始化
// 全局注册自定义均价线指标(只执行一次)
useEffect(() => {
if (!chartRef.current) {
logger.warn('useKLineChart', 'init', '图表容器未挂载', { containerId });
return;
}
try {
logger.debug('useKLineChart', 'init', '开始初始化图表', {
containerId,
height,
colorMode,
});
registerIndicator(avgPriceIndicator);
logger.debug('useKLineChart', '✅ 自定义均价线指标(AVG)注册成功');
} catch (err) {
// 如果已注册会报错,忽略即可
logger.debug('useKLineChart', 'AVG指标已注册或注册失败', err);
}
}, []);
// 初始化图表实例KLineChart 10.0 API
const chartInstance = init(chartRef.current, {
...CHART_INIT_OPTIONS,
// 设置初始样式(根据主题)
styles: getTheme(colorMode),
});
if (!chartInstance) {
throw new Error('图表初始化失败:返回 null');
// 图表初始化(添加延迟重试机制,处理 Modal 动画延迟
useEffect(() => {
// 图表初始化函数
const initChart = (): boolean => {
if (!chartRef.current) {
logger.warn('useKLineChart', 'init', '图表容器未挂载,将在 50ms 后重试', { containerId });
return false;
}
chartInstanceRef.current = chartInstance;
setIsInitialized(true);
setError(null);
try {
logger.debug('useKLineChart', 'init', '开始初始化图表', {
containerId,
height,
colorMode,
});
logger.info('useKLineChart', 'init', '图表初始化成功', {
containerId,
chartId: chartInstance.id,
});
} catch (err) {
const error = err as Error;
logger.error('useKLineChart', 'init', error, { containerId });
setError(error);
setIsInitialized(false);
// 初始化图表实例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,
@@ -107,11 +175,12 @@ export const useKLineChart = (
dispose(chartInstanceRef.current);
chartInstanceRef.current = null;
setChartInstance(null); // ✅ 新增:清空 state
setIsInitialized(false);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerId]); // 只在 containerId 变化时重新初始化
}, [containerId, chartType]); // containerId 或 chartType 变化时重新初始化
// 主题切换:更新图表样式
useEffect(() => {
@@ -120,17 +189,21 @@ export const useKLineChart = (
}
try {
const newTheme = getTheme(colorMode);
// ✅ 根据 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 });
logger.error('useKLineChart', 'updateTheme', err as Error, { colorMode, chartType });
}
}, [colorMode, isInitialized]);
}, [colorMode, chartType, isInitialized]);
// 容器尺寸变化:调整图表大小
useEffect(() => {
@@ -165,7 +238,7 @@ export const useKLineChart = (
}, [isInitialized, autoResize]);
return {
chart: chartInstanceRef.current,
chart: chartInstance, // ✅ 返回 state 而非 ref确保变化触发重渲染
chartRef,
isInitialized,
error,