feat: 创建自定义 Hooks
This commit is contained in:
15
src/components/StockChart/hooks/index.ts
Normal file
15
src/components/StockChart/hooks/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* StockChart 自定义 Hooks 统一导出
|
||||
*
|
||||
* 使用方式:
|
||||
* import { useKLineChart, useKLineData, useEventMarker } from '@components/StockChart/hooks';
|
||||
*/
|
||||
|
||||
export { useKLineChart } from './useKLineChart';
|
||||
export type { UseKLineChartOptions, UseKLineChartReturn } from './useKLineChart';
|
||||
|
||||
export { useKLineData } from './useKLineData';
|
||||
export type { UseKLineDataOptions, UseKLineDataReturn } from './useKLineData';
|
||||
|
||||
export { useEventMarker } from './useEventMarker';
|
||||
export type { UseEventMarkerOptions, UseEventMarkerReturn } from './useEventMarker';
|
||||
209
src/components/StockChart/hooks/useEventMarker.ts
Normal file
209
src/components/StockChart/hooks/useEventMarker.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* useEventMarker Hook
|
||||
*
|
||||
* 管理事件标记的创建、更新和删除
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import type { Chart } from 'klinecharts';
|
||||
import type { EventMarker, KLineDataPoint } from '../types';
|
||||
import {
|
||||
createEventMarkerFromTime,
|
||||
createEventMarkerOverlay,
|
||||
removeAllEventMarkers,
|
||||
} from '../utils/eventMarkerUtils';
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
export interface UseEventMarkerOptions {
|
||||
/** KLineChart 实例 */
|
||||
chart: Chart | null;
|
||||
/** K 线数据(用于定位标记) */
|
||||
data: KLineDataPoint[];
|
||||
/** 事件时间(ISO 字符串) */
|
||||
eventTime?: string;
|
||||
/** 事件标题(用于标记标签) */
|
||||
eventTitle?: string;
|
||||
/** 是否自动创建标记 */
|
||||
autoCreate?: boolean;
|
||||
}
|
||||
|
||||
export interface UseEventMarkerReturn {
|
||||
/** 当前标记 */
|
||||
marker: EventMarker | null;
|
||||
/** 标记 ID(已添加到图表) */
|
||||
markerId: string | null;
|
||||
/** 创建标记 */
|
||||
createMarker: (time: string, label: string, color?: string) => void;
|
||||
/** 移除标记 */
|
||||
removeMarker: () => void;
|
||||
/** 移除所有标记 */
|
||||
removeAllMarkers: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件标记管理 Hook
|
||||
*
|
||||
* @param options 配置选项
|
||||
* @returns UseEventMarkerReturn
|
||||
*
|
||||
* @example
|
||||
* const { marker, createMarker, removeMarker } = useEventMarker({
|
||||
* chart,
|
||||
* data,
|
||||
* eventTime: '2024-01-01 10:00:00',
|
||||
* eventTitle: '重大公告',
|
||||
* autoCreate: true,
|
||||
* });
|
||||
*/
|
||||
export const useEventMarker = (
|
||||
options: UseEventMarkerOptions
|
||||
): UseEventMarkerReturn => {
|
||||
const {
|
||||
chart,
|
||||
data,
|
||||
eventTime,
|
||||
eventTitle = '事件发生',
|
||||
autoCreate = true,
|
||||
} = options;
|
||||
|
||||
const [marker, setMarker] = useState<EventMarker | null>(null);
|
||||
const [markerId, setMarkerId] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* 创建事件标记
|
||||
*/
|
||||
const createMarker = useCallback(
|
||||
(time: string, label: string, color?: string) => {
|
||||
if (!chart || !data || data.length === 0) {
|
||||
logger.warn('useEventMarker', 'createMarker', '图表或数据未准备好', {
|
||||
hasChart: !!chart,
|
||||
dataLength: data?.length || 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 创建事件标记配置
|
||||
const eventMarker = createEventMarkerFromTime(time, label, color);
|
||||
setMarker(eventMarker);
|
||||
|
||||
// 2. 创建 Overlay
|
||||
const overlay = createEventMarkerOverlay(eventMarker, data);
|
||||
|
||||
if (!overlay) {
|
||||
logger.warn('useEventMarker', 'createMarker', 'Overlay 创建失败', {
|
||||
eventMarker,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 添加到图表
|
||||
const id = chart.createOverlay(overlay);
|
||||
|
||||
if (!id || (Array.isArray(id) && id.length === 0)) {
|
||||
logger.warn('useEventMarker', 'createMarker', '标记添加失败', {
|
||||
overlay,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const actualId = Array.isArray(id) ? id[0] : id;
|
||||
setMarkerId(actualId as string);
|
||||
|
||||
logger.info('useEventMarker', 'createMarker', '事件标记创建成功', {
|
||||
markerId: actualId,
|
||||
label,
|
||||
time,
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useEventMarker', 'createMarker', err as Error, {
|
||||
time,
|
||||
label,
|
||||
});
|
||||
}
|
||||
},
|
||||
[chart, data]
|
||||
);
|
||||
|
||||
/**
|
||||
* 移除事件标记
|
||||
*/
|
||||
const removeMarker = useCallback(() => {
|
||||
if (!chart || !markerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
chart.removeOverlay(markerId);
|
||||
setMarker(null);
|
||||
setMarkerId(null);
|
||||
|
||||
logger.debug('useEventMarker', 'removeMarker', '移除事件标记', {
|
||||
markerId,
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useEventMarker', 'removeMarker', err as Error, {
|
||||
markerId,
|
||||
});
|
||||
}
|
||||
}, [chart, markerId]);
|
||||
|
||||
/**
|
||||
* 移除所有标记
|
||||
*/
|
||||
const removeAllMarkers = useCallback(() => {
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
removeAllEventMarkers(chart);
|
||||
setMarker(null);
|
||||
setMarkerId(null);
|
||||
|
||||
logger.debug('useEventMarker', 'removeAllMarkers', '移除所有事件标记', {
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useEventMarker', 'removeAllMarkers', err as Error);
|
||||
}
|
||||
}, [chart]);
|
||||
|
||||
// 自动创建标记(当 eventTime 和数据都准备好时)
|
||||
useEffect(() => {
|
||||
if (
|
||||
autoCreate &&
|
||||
eventTime &&
|
||||
chart &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
!markerId // 避免重复创建
|
||||
) {
|
||||
createMarker(eventTime, eventTitle);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [eventTime, chart, data, autoCreate]);
|
||||
|
||||
// 清理:组件卸载时移除所有标记
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (chart && markerId) {
|
||||
try {
|
||||
chart.removeOverlay(markerId);
|
||||
} catch (err) {
|
||||
// 忽略清理时的错误
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [chart, markerId]);
|
||||
|
||||
return {
|
||||
marker,
|
||||
markerId,
|
||||
createMarker,
|
||||
removeMarker,
|
||||
removeAllMarkers,
|
||||
};
|
||||
};
|
||||
173
src/components/StockChart/hooks/useKLineChart.ts
Normal file
173
src/components/StockChart/hooks/useKLineChart.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* useKLineChart Hook
|
||||
*
|
||||
* 管理 KLineChart 实例的初始化、配置和销毁
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { init, dispose } from 'klinecharts';
|
||||
import type { Chart } from 'klinecharts';
|
||||
import { useColorMode } from '@chakra-ui/react';
|
||||
import { getTheme } from '../config/klineTheme';
|
||||
import { CHART_INIT_OPTIONS } from '../config';
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
export interface UseKLineChartOptions {
|
||||
/** 图表容器 ID */
|
||||
containerId: string;
|
||||
/** 图表高度(px) */
|
||||
height?: number;
|
||||
/** 是否自动调整大小 */
|
||||
autoResize?: boolean;
|
||||
}
|
||||
|
||||
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 } = options;
|
||||
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const chartInstanceRef = useRef<Chart | null>(null);
|
||||
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,
|
||||
});
|
||||
|
||||
// 初始化图表实例(KLineChart 10.0 API)
|
||||
const chartInstance = init(chartRef.current, {
|
||||
...CHART_INIT_OPTIONS,
|
||||
// 设置初始样式(根据主题)
|
||||
styles: getTheme(colorMode),
|
||||
});
|
||||
|
||||
if (!chartInstance) {
|
||||
throw new Error('图表初始化失败:返回 null');
|
||||
}
|
||||
|
||||
chartInstanceRef.current = chartInstance;
|
||||
setIsInitialized(true);
|
||||
setError(null);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 清理函数:销毁图表实例
|
||||
return () => {
|
||||
if (chartInstanceRef.current) {
|
||||
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
|
||||
containerId,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
});
|
||||
|
||||
dispose(chartInstanceRef.current);
|
||||
chartInstanceRef.current = null;
|
||||
setIsInitialized(false);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [containerId]); // 只在 containerId 变化时重新初始化
|
||||
|
||||
// 主题切换:更新图表样式
|
||||
useEffect(() => {
|
||||
if (!chartInstanceRef.current || !isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newTheme = getTheme(colorMode);
|
||||
chartInstanceRef.current.setStyles(newTheme);
|
||||
|
||||
logger.debug('useKLineChart', 'updateTheme', '更新图表主题', {
|
||||
colorMode,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useKLineChart', 'updateTheme', err as Error, { colorMode });
|
||||
}
|
||||
}, [colorMode, 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: chartInstanceRef.current,
|
||||
chartRef,
|
||||
isInitialized,
|
||||
error,
|
||||
};
|
||||
};
|
||||
222
src/components/StockChart/hooks/useKLineData.ts
Normal file
222
src/components/StockChart/hooks/useKLineData.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 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/stockService';
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
// 调用后端 API 获取数据
|
||||
const response = await stockService.getKlineData(
|
||||
stockCode,
|
||||
chartType,
|
||||
eventTime
|
||||
);
|
||||
|
||||
if (!response || !response.data) {
|
||||
throw new Error('后端返回数据为空');
|
||||
}
|
||||
|
||||
const rawDataList = response.data;
|
||||
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]);
|
||||
|
||||
/**
|
||||
* 更新图表数据(使用 DataLoader 模式)
|
||||
*/
|
||||
const updateChartData = useCallback(
|
||||
(klineData: KLineDataPoint[]) => {
|
||||
if (!chart || klineData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// KLineChart 10.0: 使用 setDataLoader 方法
|
||||
chart.setDataLoader({
|
||||
getBars: (params) => {
|
||||
// 将数据传递给图表
|
||||
params.callback(klineData, { more: false });
|
||||
|
||||
logger.debug('useKLineData', 'updateChartData', 'DataLoader 回调', {
|
||||
dataCount: klineData.length,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('useKLineData', 'updateChartData', '图表数据已更新', {
|
||||
dataCount: klineData.length,
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useKLineData', 'updateChartData', err as Error, {
|
||||
dataCount: klineData.length,
|
||||
});
|
||||
}
|
||||
},
|
||||
[chart]
|
||||
);
|
||||
|
||||
/**
|
||||
* 手动更新数据(外部调用)
|
||||
*/
|
||||
const updateData = useCallback(
|
||||
(newData: KLineDataPoint[]) => {
|
||||
setData(newData);
|
||||
updateChartData(newData);
|
||||
|
||||
logger.debug('useKLineData', 'updateData', '手动更新数据', {
|
||||
newDataCount: newData.length,
|
||||
});
|
||||
},
|
||||
[updateChartData]
|
||||
);
|
||||
|
||||
/**
|
||||
* 清空数据
|
||||
*/
|
||||
const clearData = useCallback(() => {
|
||||
setData([]);
|
||||
setRawData([]);
|
||||
setError(null);
|
||||
|
||||
if (chart) {
|
||||
chart.resetData();
|
||||
logger.debug('useKLineData', 'clearData', '清空数据', {
|
||||
chartId: chart.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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user