276 lines
6.9 KiB
TypeScript
276 lines
6.9 KiB
TypeScript
/**
|
||
* StockChartKLineModal - K 线图表模态框组件
|
||
*
|
||
* 使用 KLineChart 库实现的专业金融图表组件
|
||
* 替换原有的 ECharts 实现(StockChartAntdModal.js)
|
||
*/
|
||
|
||
import React, { useState, useCallback, useMemo } from 'react';
|
||
import { Modal, Button, Radio, Select, Space, Spin, Alert } from 'antd';
|
||
import type { RadioChangeEvent } from 'antd';
|
||
import {
|
||
LineChartOutlined,
|
||
BarChartOutlined,
|
||
SettingOutlined,
|
||
ReloadOutlined,
|
||
} from '@ant-design/icons';
|
||
import { Box } from '@chakra-ui/react';
|
||
|
||
// 自定义 Hooks
|
||
import { useKLineChart, useKLineData, useEventMarker } from './hooks';
|
||
|
||
// 类型定义
|
||
import type { ChartType, StockInfo } from './types';
|
||
|
||
// 配置常量
|
||
import {
|
||
CHART_TYPE_CONFIG,
|
||
CHART_HEIGHTS,
|
||
INDICATORS,
|
||
DEFAULT_SUB_INDICATORS,
|
||
} from './config';
|
||
|
||
// 工具函数
|
||
import { createSubIndicators } from './utils';
|
||
|
||
// ==================== 组件 Props ====================
|
||
|
||
export interface StockChartKLineModalProps {
|
||
/** 是否显示模态框 */
|
||
visible: boolean;
|
||
/** 关闭模态框回调 */
|
||
onClose: () => void;
|
||
/** 股票信息 */
|
||
stock: StockInfo;
|
||
/** 事件时间(ISO 字符串,可选) */
|
||
eventTime?: string;
|
||
/** 事件标题(用于标记标签,可选) */
|
||
eventTitle?: string;
|
||
}
|
||
|
||
// ==================== 主组件 ====================
|
||
|
||
const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
||
visible,
|
||
onClose,
|
||
stock,
|
||
eventTime,
|
||
eventTitle,
|
||
}) => {
|
||
// ==================== 状态管理 ====================
|
||
|
||
/** 图表类型(分时图/日K线) */
|
||
const [chartType, setChartType] = useState<ChartType>('daily');
|
||
|
||
/** 选中的副图指标 */
|
||
const [selectedIndicators, setSelectedIndicators] = useState<string[]>(
|
||
DEFAULT_SUB_INDICATORS
|
||
);
|
||
|
||
// ==================== 自定义 Hooks ====================
|
||
|
||
/** 图表实例管理 */
|
||
const { chart, chartRef, isInitialized, error: chartError } = useKLineChart({
|
||
containerId: `kline-chart-${stock.stock_code}`,
|
||
height: CHART_HEIGHTS.main,
|
||
autoResize: true,
|
||
});
|
||
|
||
/** 数据加载管理 */
|
||
const {
|
||
data,
|
||
loading: dataLoading,
|
||
error: dataError,
|
||
loadData,
|
||
} = useKLineData({
|
||
chart,
|
||
stockCode: stock.stock_code,
|
||
chartType,
|
||
eventTime,
|
||
autoLoad: visible, // 模态框打开时自动加载
|
||
});
|
||
|
||
/** 事件标记管理 */
|
||
const { marker } = useEventMarker({
|
||
chart,
|
||
data,
|
||
eventTime,
|
||
eventTitle,
|
||
autoCreate: true,
|
||
});
|
||
|
||
// ==================== 事件处理 ====================
|
||
|
||
/**
|
||
* 切换图表类型(分时图 ↔ 日K线)
|
||
*/
|
||
const handleChartTypeChange = useCallback((e: RadioChangeEvent) => {
|
||
const newType = e.target.value as ChartType;
|
||
setChartType(newType);
|
||
}, []);
|
||
|
||
/**
|
||
* 切换副图指标
|
||
*/
|
||
const handleIndicatorChange = useCallback(
|
||
(values: string[]) => {
|
||
setSelectedIndicators(values);
|
||
|
||
if (!chart) {
|
||
return;
|
||
}
|
||
|
||
// 先移除所有副图指标(KLineChart 会自动移除)
|
||
// 然后创建新的指标
|
||
createSubIndicators(chart, values);
|
||
},
|
||
[chart]
|
||
);
|
||
|
||
/**
|
||
* 刷新数据
|
||
*/
|
||
const handleRefresh = useCallback(() => {
|
||
loadData();
|
||
}, [loadData]);
|
||
|
||
// ==================== 计算属性 ====================
|
||
|
||
/** 是否有错误 */
|
||
const hasError = useMemo(() => {
|
||
return !!chartError || !!dataError;
|
||
}, [chartError, dataError]);
|
||
|
||
/** 错误消息 */
|
||
const errorMessage = useMemo(() => {
|
||
if (chartError) {
|
||
return `图表初始化失败: ${chartError.message}`;
|
||
}
|
||
if (dataError) {
|
||
return `数据加载失败: ${dataError.message}`;
|
||
}
|
||
return null;
|
||
}, [chartError, dataError]);
|
||
|
||
/** 模态框标题 */
|
||
const modalTitle = useMemo(() => {
|
||
return `${stock.stock_name}(${stock.stock_code}) - ${CHART_TYPE_CONFIG[chartType].label}`;
|
||
}, [stock, chartType]);
|
||
|
||
/** 是否显示加载状态 */
|
||
const showLoading = useMemo(() => {
|
||
return dataLoading || !isInitialized;
|
||
}, [dataLoading, isInitialized]);
|
||
|
||
// ==================== 副作用 ====================
|
||
|
||
// 无副作用,都在 Hooks 中管理
|
||
|
||
// ==================== 渲染 ====================
|
||
|
||
return (
|
||
<Modal
|
||
title={modalTitle}
|
||
open={visible}
|
||
onCancel={onClose}
|
||
width={1200}
|
||
footer={null}
|
||
centered
|
||
destroyOnHidden // 关闭时销毁组件(释放图表资源)
|
||
>
|
||
{/* 工具栏 */}
|
||
<Box mb={4}>
|
||
<Space wrap>
|
||
{/* 图表类型切换 */}
|
||
<Radio.Group value={chartType} onChange={handleChartTypeChange}>
|
||
<Radio.Button value="timeline">
|
||
<LineChartOutlined /> 分时图
|
||
</Radio.Button>
|
||
<Radio.Button value="daily">
|
||
<BarChartOutlined /> 日K线
|
||
</Radio.Button>
|
||
</Radio.Group>
|
||
|
||
{/* 副图指标选择 */}
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="选择副图指标"
|
||
value={selectedIndicators}
|
||
onChange={handleIndicatorChange}
|
||
style={{ minWidth: 200 }}
|
||
maxTagCount={2}
|
||
>
|
||
{INDICATORS.sub.map((indicator) => (
|
||
<Select.Option key={indicator.name} value={indicator.name}>
|
||
<SettingOutlined /> {indicator.label}
|
||
</Select.Option>
|
||
))}
|
||
</Select>
|
||
|
||
{/* 刷新按钮 */}
|
||
<Button
|
||
icon={<ReloadOutlined />}
|
||
onClick={handleRefresh}
|
||
loading={dataLoading}
|
||
>
|
||
刷新
|
||
</Button>
|
||
</Space>
|
||
</Box>
|
||
|
||
{/* 错误提示 */}
|
||
{hasError && (
|
||
<Alert
|
||
message="加载失败"
|
||
description={errorMessage}
|
||
type="error"
|
||
closable
|
||
showIcon
|
||
style={{ marginBottom: 16 }}
|
||
/>
|
||
)}
|
||
|
||
{/* 图表容器 */}
|
||
<Box position="relative">
|
||
{/* 加载遮罩 */}
|
||
{showLoading && (
|
||
<Box
|
||
position="absolute"
|
||
top="50%"
|
||
left="50%"
|
||
transform="translate(-50%, -50%)"
|
||
zIndex={10}
|
||
>
|
||
<Spin size="large" tip="加载中..." />
|
||
</Box>
|
||
)}
|
||
|
||
{/* KLineChart 容器 */}
|
||
<div
|
||
ref={chartRef}
|
||
id={`kline-chart-${stock.stock_code}`}
|
||
style={{
|
||
width: '100%',
|
||
height: `${CHART_HEIGHTS.main}px`,
|
||
opacity: showLoading ? 0.5 : 1,
|
||
transition: 'opacity 0.3s',
|
||
}}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 数据信息(调试用,生产环境可移除) */}
|
||
{process.env.NODE_ENV === 'development' && (
|
||
<Box mt={2} fontSize="12px" color="gray.500">
|
||
<Space split="|">
|
||
<span>数据点数: {data.length}</span>
|
||
<span>事件标记: {marker ? marker.label : '无'}</span>
|
||
<span>图表ID: {chart?.id || '未初始化'}</span>
|
||
</Space>
|
||
</Box>
|
||
)}
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default StockChartKLineModal;
|