feat: 现在创建主组件:

This commit is contained in:
zdl
2025-11-22 23:14:59 +08:00
parent bcd67ed410
commit f361cb55f4
3 changed files with 610 additions and 0 deletions

View File

@@ -0,0 +1,287 @@
/**
* 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';
// 日志
import { logger } from '@utils/logger';
// ==================== 组件 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);
logger.debug('StockChartKLineModal', 'handleChartTypeChange', '切换图表类型', {
newType,
});
}, []);
/**
* 切换副图指标
*/
const handleIndicatorChange = useCallback(
(values: string[]) => {
setSelectedIndicators(values);
if (!chart) {
return;
}
// 先移除所有副图指标KLineChart 会自动移除)
// 然后创建新的指标
createSubIndicators(chart, values);
logger.debug('StockChartKLineModal', 'handleIndicatorChange', '切换副图指标', {
indicators: values,
});
},
[chart]
);
/**
* 刷新数据
*/
const handleRefresh = useCallback(() => {
loadData();
logger.debug('StockChartKLineModal', 'handleRefresh', '刷新数据');
}, [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
destroyOnClose // 关闭时销毁组件(释放图表资源)
>
{/* 工具栏 */}
<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;

View File

@@ -0,0 +1,298 @@
/**
* KLineChart 主题配置
*
* 适配 klinecharts@10.0.0-beta1
* 参考: https://github.com/klinecharts/KLineChart/blob/main/docs/en-US/guide/styles.md
*/
import type { DeepPartial, Styles } from 'klinecharts';
/**
* 图表主题颜色配置
*/
export const CHART_COLORS = {
// 涨跌颜色(中国市场习惯:红涨绿跌)
up: '#ef5350', // 上涨红色
down: '#26a69a', // 下跌绿色
neutral: '#888888', // 平盘灰色
// 主题色(继承自 Argon Dashboard
primary: '#1b3bbb', // Navy 500
secondary: '#728fea', // Navy 300
background: '#ffffff',
backgroundDark: '#1B254B',
// 文本颜色
text: '#333333',
textSecondary: '#888888',
textDark: '#ffffff',
// 网格颜色
grid: '#e0e0e0',
gridDark: '#2d3748',
// 边框颜色
border: '#e0e0e0',
borderDark: '#2d3748',
// 事件标记颜色
eventMarker: '#ff9800',
eventMarkerText: '#ffffff',
};
/**
* 浅色主题配置(默认)
*/
export const lightTheme: DeepPartial<Styles> = {
candle: {
type: 'candle_solid', // 实心蜡烛图
bar: {
upColor: CHART_COLORS.up,
downColor: CHART_COLORS.down,
noChangeColor: CHART_COLORS.neutral,
},
priceMark: {
show: true,
high: {
color: CHART_COLORS.up,
},
low: {
color: CHART_COLORS.down,
},
},
tooltip: {
showRule: 'always',
showType: 'standard',
labels: ['时间: ', '开: ', '收: ', '高: ', '低: ', '成交量: '],
text: {
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
color: CHART_COLORS.text,
},
},
},
indicator: {
tooltip: {
showRule: 'always',
showType: 'standard',
text: {
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
color: CHART_COLORS.text,
},
},
},
xAxis: {
axisLine: {
show: true,
color: CHART_COLORS.border,
},
tickLine: {
show: true,
length: 3,
color: CHART_COLORS.border,
},
tickText: {
show: true,
color: CHART_COLORS.textSecondary,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
size: 12,
},
},
yAxis: {
axisLine: {
show: true,
color: CHART_COLORS.border,
},
tickLine: {
show: true,
length: 3,
color: CHART_COLORS.border,
},
tickText: {
show: true,
color: CHART_COLORS.textSecondary,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
size: 12,
},
type: 'normal', // 'normal' | 'percentage' | 'log'
},
grid: {
show: true,
horizontal: {
show: true,
size: 1,
color: CHART_COLORS.grid,
style: 'dashed',
},
vertical: {
show: false, // 垂直网格线通常关闭,避免过于密集
},
},
separator: {
size: 1,
color: CHART_COLORS.border,
},
crosshair: {
show: true,
horizontal: {
show: true,
line: {
show: true,
style: 'dashed',
dashValue: [4, 2],
size: 1,
color: CHART_COLORS.primary,
},
text: {
show: true,
color: CHART_COLORS.textDark,
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
backgroundColor: CHART_COLORS.primary,
},
},
vertical: {
show: true,
line: {
show: true,
style: 'dashed',
dashValue: [4, 2],
size: 1,
color: CHART_COLORS.primary,
},
text: {
show: true,
color: CHART_COLORS.textDark,
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
backgroundColor: CHART_COLORS.primary,
},
},
},
overlay: {
// 事件标记覆盖层样式
point: {
color: CHART_COLORS.eventMarker,
borderColor: CHART_COLORS.eventMarker,
borderSize: 1,
radius: 5,
activeColor: CHART_COLORS.eventMarker,
activeBorderColor: CHART_COLORS.eventMarker,
activeBorderSize: 2,
activeRadius: 6,
},
line: {
style: 'solid',
smooth: false,
color: CHART_COLORS.eventMarker,
size: 1,
dashedValue: [2, 2],
},
text: {
style: 'fill',
color: CHART_COLORS.eventMarkerText,
size: 12,
family: 'Helvetica, Arial, sans-serif',
weight: 'normal',
offset: [0, 0],
},
rect: {
style: 'fill',
color: CHART_COLORS.eventMarker,
borderColor: CHART_COLORS.eventMarker,
borderSize: 1,
borderRadius: 4,
borderStyle: 'solid',
borderDashedValue: [2, 2],
},
},
};
/**
* 深色主题配置
*/
export const darkTheme: DeepPartial<Styles> = {
...lightTheme,
candle: {
...lightTheme.candle,
tooltip: {
...lightTheme.candle?.tooltip,
text: {
...lightTheme.candle?.tooltip?.text,
color: CHART_COLORS.textDark,
},
},
},
indicator: {
...lightTheme.indicator,
tooltip: {
...lightTheme.indicator?.tooltip,
text: {
...lightTheme.indicator?.tooltip?.text,
color: CHART_COLORS.textDark,
},
},
},
xAxis: {
...lightTheme.xAxis,
axisLine: {
show: true,
color: CHART_COLORS.borderDark,
},
tickLine: {
show: true,
length: 3,
color: CHART_COLORS.borderDark,
},
tickText: {
...lightTheme.xAxis?.tickText,
color: CHART_COLORS.textDark,
},
},
yAxis: {
...lightTheme.yAxis,
axisLine: {
show: true,
color: CHART_COLORS.borderDark,
},
tickLine: {
show: true,
length: 3,
color: CHART_COLORS.borderDark,
},
tickText: {
...lightTheme.yAxis?.tickText,
color: CHART_COLORS.textDark,
},
},
grid: {
show: true,
horizontal: {
show: true,
size: 1,
color: CHART_COLORS.gridDark,
style: 'dashed',
},
vertical: {
show: false,
},
},
separator: {
size: 1,
color: CHART_COLORS.borderDark,
},
};
/**
* 获取主题配置(根据 Chakra UI colorMode
*/
export const getTheme = (colorMode: 'light' | 'dark' = 'light'): DeepPartial<Styles> => {
return colorMode === 'dark' ? darkTheme : lightTheme;
};

View File

@@ -0,0 +1,25 @@
/**
* StockChart 类型定义统一导出
*
* 使用方式:
* import type { KLineDataPoint, StockInfo } from '@components/StockChart/types';
*/
// 图表相关类型
export type {
KLineDataPoint,
RawDataPoint,
ChartType,
ChartConfig,
EventMarker,
DataLoaderCallbackParams,
DataLoaderGetBarsParams,
} from './chart.types';
// 股票相关类型
export type {
StockInfo,
ChartDataResponse,
StockQuote,
EventInfo,
} from './stock.types';