From bff440ff8a9f24735cce8e0f52d850ee03553a5f Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 19 Dec 2025 13:37:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor(TradeDataPanel):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=20KLineModule=20=E4=B8=BA=E7=8B=AC=E7=AB=8B=E5=AD=90=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KLineModule: 611行精简至157行,专注状态管理 - 提取 KLineToolbar: 工具栏组件(模式切换、指标选择) - 提取 DailyKLineChart: 日K图表(useMemo缓存配置) - 提取 MinuteChartWithOrderBook: 分时图+五档盘口 - 提取 constants.ts: 指标选项常量 - 提取 styles.ts: 按钮样式常量 - 所有组件使用 React.memo 优化 - 更新 README.md 文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/MarketDataView/README.md | 109 ++- .../panels/TradeDataPanel/KLineModule.tsx | 641 +++--------------- .../components/DailyKLineChart.tsx | 90 +++ .../components/KLineToolbar.tsx | 335 +++++++++ .../components/MinuteChartWithOrderBook.tsx | 229 +++++++ .../panels/TradeDataPanel/components/index.ts | 11 + .../panels/TradeDataPanel/constants.ts | 41 ++ .../panels/TradeDataPanel/index.tsx | 64 +- .../panels/TradeDataPanel/styles.ts | 65 ++ 9 files changed, 969 insertions(+), 616 deletions(-) create mode 100644 src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/DailyKLineChart.tsx create mode 100644 src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/KLineToolbar.tsx create mode 100644 src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/MinuteChartWithOrderBook.tsx create mode 100644 src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/index.ts create mode 100644 src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/constants.ts create mode 100644 src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/styles.ts diff --git a/src/views/Company/components/MarketDataView/README.md b/src/views/Company/components/MarketDataView/README.md index 6d3a38cb..ad3e89c1 100644 --- a/src/views/Company/components/MarketDataView/README.md +++ b/src/views/Company/components/MarketDataView/README.md @@ -12,7 +12,7 @@ MarketDataView/ ├── README.md # 本文档 │ ├── hooks/ -│ └── useMarketData.ts # 市场数据获取 +│ └── useMarketData.ts # 市场数据获取(含 AbortController) │ ├── services/ │ └── marketService.ts # API 服务层 @@ -32,14 +32,23 @@ MarketDataView/ │ │ │ ├── panels/ # 数据面板 │ │ ├── index.ts # 统一导出 -│ │ ├── TradeDataPanel/ # 交易数据面板 -│ │ │ ├── index.tsx # 面板入口 -│ │ │ ├── KLineModule.tsx # K线模块 -│ │ │ └── MetricOverlaySearch.tsx # 指标叠加搜索 -│ │ ├── FundingPanel.tsx # 资金流向面板 -│ │ ├── BigDealPanel.tsx # 大宗交易面板 -│ │ ├── UnusualPanel.tsx # 异动信息面板 -│ │ └── PledgePanel.tsx # 股权质押面板 +│ │ │ +│ │ ├── TradeDataPanel/ # 交易数据面板(K线模块) +│ │ │ ├── index.tsx # 统一导出 +│ │ │ ├── KLineModule.tsx # K线主模块(157行,memo优化) +│ │ │ ├── constants.ts # 指标选项常量 +│ │ │ ├── styles.ts # 按钮样式常量 +│ │ │ ├── MetricOverlaySearch.tsx # 指标叠加搜索 +│ │ │ └── components/ # 子组件 +│ │ │ ├── index.ts # 统一导出 +│ │ │ ├── KLineToolbar.tsx # 工具栏(模式切换、指标选择) +│ │ │ ├── DailyKLineChart.tsx # 日K图表(useMemo缓存) +│ │ │ └── MinuteChartWithOrderBook.tsx # 分时图+五档盘口 +│ │ │ +│ │ ├── FundingPanel.tsx # 融资融券面板(memo优化) +│ │ ├── BigDealPanel.tsx # 大宗交易面板(memo优化) +│ │ ├── UnusualPanel.tsx # 龙虎榜面板(memo优化) +│ │ └── PledgePanel.tsx # 股权质押面板(memo优化) │ │ │ └── StockSummaryCard/ # 股票摘要卡片 │ ├── index.tsx # 卡片入口 @@ -62,10 +71,10 @@ MarketDataView/ | 模块 | 说明 | |------|------| -| 交易数据 | K线图、分时图、成交量 | -| 资金流向 | 主力资金、北向资金、融资融券 | +| 交易数据 | K线图、分时图、成交量、五档盘口 | +| 融资融券 | 融资余额、融券余额、资金趋势 | | 大宗交易 | 大宗交易记录、成交统计 | -| 异动信息 | 涨跌停、龙虎榜、异常波动 | +| 龙虎榜 | 买入卖出席位、净买入额 | | 股权质押 | 质押比例、质押明细 | ## 主题系统 @@ -87,15 +96,41 @@ const darkGoldTheme = { ``` MarketDataView -├── SubTabContainer # Tab 容器 -│ ├── TradeDataPanel # 交易数据 -│ │ └── KLineModule -│ ├── FundingPanel # 资金流向 -│ ├── BigDealPanel # 大宗交易 -│ ├── UnusualPanel # 异动信息 -│ └── PledgePanel # 股权质押 +├── StockSummaryCard # 股票概览 +└── SubTabContainer # Tab 容器 + ├── TradeDataPanel # 交易数据 + │ └── KLineModule + │ ├── KLineToolbar # 工具栏 + │ ├── DailyKLineChart # 日K图表 + │ └── MinuteChartWithOrderBook # 分时+盘口 + ├── FundingPanel # 融资融券 + ├── BigDealPanel # 大宗交易 + ├── UnusualPanel # 龙虎榜 + └── PledgePanel # 股权质押 ``` +## 性能优化 + +### 已实现的优化 + +| 优化项 | 说明 | +|--------|------| +| React.memo | 所有 Panel 和子组件使用 memo 包装 | +| useMemo | 图表配置缓存,避免重复计算技术指标 | +| useCallback | 事件处理函数稳定化 | +| AbortController | 请求取消,防止内存泄漏 | +| Tab 懒加载 | 切换 Tab 时按需加载数据 | + +### TradeDataPanel 重构 + +KLineModule 从 611 行精简至 157 行,拆分为独立子组件: + +| 子组件 | 职责 | 行数 | +|--------|------|------| +| KLineToolbar | 模式切换、指标选择、时间范围 | 275 | +| DailyKLineChart | 日K图表渲染、useMemo缓存 | 85 | +| MinuteChartWithOrderBook | 分时图、实时行情、五档盘口 | 212 | + ## 使用示例 ```tsx @@ -103,3 +138,39 @@ import MarketDataView from '@views/Company/components/MarketDataView'; ``` + +### 单独使用 K 线模块 + +```tsx +import { KLineModule } from '@views/Company/components/MarketDataView/components/panels/TradeDataPanel'; + + +``` + +### 单独使用子组件 + +```tsx +import { + KLineToolbar, + DailyKLineChart, + MinuteChartWithOrderBook, +} from '@views/Company/components/MarketDataView/components/panels/TradeDataPanel'; + +// 仅工具栏 + + +// 仅日K图表 + + +// 仅分时图+盘口 + +``` diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx index 4ec87784..12e18409 100644 --- a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx @@ -1,112 +1,24 @@ // src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx // K线模块 - 日K线/分时图切换展示(黑金主题 + 专业技术指标 + 商品数据叠加 + 分时盘口) -import React, { useState, useMemo, useCallback, useEffect } from 'react'; -import { - Box, - Text, - VStack, - HStack, - Button, - ButtonGroup, - Badge, - Center, - Spinner, - Icon, - Select, - Menu, - MenuButton, - MenuList, - MenuItem, - MenuDivider, - Tooltip, - Grid, - GridItem, -} from '@chakra-ui/react'; -import { RepeatIcon, InfoIcon, ChevronDownIcon, ViewIcon, ViewOffIcon } from '@chakra-ui/icons'; -import { BarChart2, Clock, TrendingUp, Calendar, LineChart, Activity, Pencil } from 'lucide-react'; -import ReactECharts from 'echarts-for-react'; +import React, { useState, useMemo, useCallback, memo } from 'react'; +import { Box } from '@chakra-ui/react'; -// 导入实时行情 Hook 和五档盘口组件 -import { useRealtimeQuote } from '@views/StockOverview/components/FlexScreen/hooks'; -import OrderBookPanel from '@views/StockOverview/components/FlexScreen/components/OrderBookPanel'; - -import { darkGoldTheme, PERIOD_OPTIONS } from '../../../constants'; -import { - getKLineDarkGoldOption, - getMinuteKLineDarkGoldOption, - type IndicatorType, - type MainIndicatorType, - type DrawingType, -} from '../../../utils/chartOptions'; +import { darkGoldTheme } from '../../../constants'; +import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../utils/chartOptions'; import type { KLineModuleProps, OverlayMetricData } from '../../../types'; -import MetricOverlaySearch from './MetricOverlaySearch'; +import type { ChartMode } from './constants'; -// 空状态组件(内联) -const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => ( -
- - - - {title} - {description} - - -
-); +// 子组件导入 +import { KLineToolbar, DailyKLineChart, MinuteChartWithOrderBook } from './components'; // 重新导出类型供外部使用 export type { KLineModuleProps } from '../../../types'; -type ChartMode = 'daily' | 'minute'; - -// 副图指标选项 -const SUB_INDICATOR_OPTIONS: { value: IndicatorType; label: string; description: string }[] = [ - { value: 'MACD', label: 'MACD', description: '平滑异同移动平均线' }, - { value: 'KDJ', label: 'KDJ', description: '随机指标' }, - { value: 'RSI', label: 'RSI', description: '相对强弱指标' }, - { value: 'WR', label: 'WR', description: '威廉指标(超买超卖)' }, - { value: 'CCI', label: 'CCI', description: '商品通道指标' }, - { value: 'BIAS', label: 'BIAS', description: '乖离率' }, - { value: 'VOL', label: '仅成交量', description: '不显示副图指标' }, -]; - -// 主图指标选项 -const MAIN_INDICATOR_OPTIONS: { value: MainIndicatorType; label: string; description: string }[] = [ - { value: 'MA', label: 'MA均线', description: 'MA5/MA10/MA20' }, - { value: 'BOLL', label: '布林带', description: '布林通道指标' }, - { value: 'NONE', label: '无', description: '不显示主图指标' }, -]; - -// 绘图工具选项 -const DRAWING_OPTIONS: { value: DrawingType; label: string; description: string }[] = [ - { value: 'NONE', label: '无', description: '不显示绘图工具' }, - { value: 'SUPPORT_RESISTANCE', label: '支撑/阻力', description: '自动识别支撑位和阻力位' }, - { value: 'TREND_LINE', label: '趋势线', description: '基于线性回归的趋势线' }, - { value: 'ALL', label: '全部显示', description: '显示所有参考线' }, -]; - -// 黑金主题按钮样式(提取到组件外部避免每次渲染重建) -const ACTIVE_BUTTON_STYLE = { - bg: `linear-gradient(135deg, ${darkGoldTheme.gold} 0%, ${darkGoldTheme.orange} 100%)`, - color: '#1a1a2e', - borderColor: darkGoldTheme.gold, - _hover: { - bg: `linear-gradient(135deg, ${darkGoldTheme.goldLight} 0%, ${darkGoldTheme.gold} 100%)`, - }, -} as const; - -const INACTIVE_BUTTON_STYLE = { - bg: 'transparent', - color: darkGoldTheme.textMuted, - borderColor: darkGoldTheme.border, - _hover: { - bg: 'rgba(212, 175, 55, 0.1)', - borderColor: darkGoldTheme.gold, - color: darkGoldTheme.gold, - }, -} as const; - +/** + * K线模块主组件 + * 职责:状态管理、组合子组件、事件处理 + */ const KLineModule: React.FC = ({ theme, tradeData, @@ -119,6 +31,7 @@ const KLineModule: React.FC = ({ onPeriodChange, stockCode, }) => { + // ========== 状态管理 ========== const [mode, setMode] = useState('daily'); const [subIndicator, setSubIndicator] = useState('MACD'); const [mainIndicator, setMainIndicator] = useState('MA'); @@ -126,33 +39,10 @@ const KLineModule: React.FC = ({ const [drawingType, setDrawingType] = useState('NONE'); const [overlayMetrics, setOverlayMetrics] = useState([]); const [showOrderBook, setShowOrderBook] = useState(true); + + // ========== 计算属性 ========== const hasMinuteData = minuteData && minuteData.data && minuteData.data.length > 0; - // 实时行情数据(用于五档盘口) - const subscribedCodes = useMemo(() => { - if (!stockCode || mode !== 'minute') return []; - return [stockCode]; - }, [stockCode, mode]); - - const { quotes, connected } = useRealtimeQuote(subscribedCodes); - - // 获取当前股票的行情数据 - const currentQuote = useMemo(() => { - if (!stockCode) return null; - // 尝试不同的代码格式 - return quotes[stockCode] || quotes[`${stockCode}.SH`] || quotes[`${stockCode}.SZ`] || null; - }, [quotes, stockCode]); - - // 判断是否在交易时间 - const isInTradingHours = useMemo(() => { - const now = new Date(); - const hours = now.getHours(); - const minutes = now.getMinutes(); - const totalMinutes = hours * 60 + minutes; - // 9:15-11:30 或 13:00-15:00 - return (totalMinutes >= 555 && totalMinutes <= 690) || (totalMinutes >= 780 && totalMinutes <= 900); - }, []); - // 计算股票数据的日期范围(用于查询商品数据) const stockDateRange = useMemo(() => { if (tradeData.length === 0) return undefined; @@ -162,6 +52,26 @@ const KLineModule: React.FC = ({ }; }, [tradeData]); + // ========== 事件处理 ========== + + // 切换到分时模式时自动加载数据 + const handleModeChange = useCallback((newMode: ChartMode) => { + setMode(newMode); + if (newMode === 'minute' && !hasMinuteData && !minuteLoading) { + onLoadMinuteData(); + } + }, [hasMinuteData, minuteLoading, onLoadMinuteData]); + + // 切换显示/隐藏分析 + const handleToggleAnalysis = useCallback(() => { + setShowAnalysis(prev => !prev); + }, []); + + // 切换显示/隐藏盘口 + const handleToggleOrderBook = useCallback(() => { + setShowOrderBook(prev => !prev); + }, []); + // 添加叠加指标 const handleAddOverlayMetric = useCallback((metric: OverlayMetricData) => { setOverlayMetrics(prev => [...prev, metric]); @@ -172,440 +82,75 @@ const KLineModule: React.FC = ({ setOverlayMetrics(prev => prev.filter(m => m.metric_id !== metricId)); }, []); - // 切换到分时模式时自动加载数据(使用 useCallback 避免不必要的重渲染) - const handleModeChange = useCallback((newMode: ChartMode) => { - setMode(newMode); - if (newMode === 'minute' && !hasMinuteData && !minuteLoading) { - onLoadMinuteData(); - } - }, [hasMinuteData, minuteLoading, onLoadMinuteData]); + // 主图指标变更 + const handleMainIndicatorChange = useCallback((indicator: MainIndicatorType) => { + setMainIndicator(indicator); + }, []); + // 副图指标变更 + const handleSubIndicatorChange = useCallback((indicator: IndicatorType) => { + setSubIndicator(indicator); + }, []); + + // 绘图工具变更 + const handleDrawingTypeChange = useCallback((type: DrawingType) => { + setDrawingType(type); + }, []); + + // ========== 渲染 ========== return ( - - {/* 卡片头部 */} - - - - - {mode === 'daily' ? ( - - ) : ( - - )} - - - {mode === 'daily' ? '日K线图' : '分时走势'} - - {mode === 'minute' && minuteData?.trade_date && ( - - {minuteData.trade_date} - - )} - + + {/* 工具栏 */} + - - {/* 日K模式下显示时间范围选择器和指标选择 */} - {mode === 'daily' && ( - <> - {/* 时间范围选择器 */} - {onPeriodChange && ( - - - - - )} - - {/* 隐藏/显示涨幅分析 */} - - - - - {/* 主图指标选择 */} - - - } - {...INACTIVE_BUTTON_STYLE} - minW="90px" - > - {MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'} - - - - {MAIN_INDICATOR_OPTIONS.map((option) => ( - setMainIndicator(option.value)} - bg={mainIndicator === option.value ? 'rgba(212, 175, 55, 0.2)' : 'transparent'} - color={mainIndicator === option.value ? darkGoldTheme.gold : darkGoldTheme.textPrimary} - _hover={{ bg: 'rgba(212, 175, 55, 0.1)' }} - > - - - {option.label} - - - {option.description} - - - - ))} - - - - {/* 副图指标选择 */} - - - } - leftIcon={} - {...INACTIVE_BUTTON_STYLE} - minW="100px" - > - {SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'} - - - - {SUB_INDICATOR_OPTIONS.map((option) => ( - setSubIndicator(option.value)} - bg={subIndicator === option.value ? 'rgba(212, 175, 55, 0.2)' : 'transparent'} - color={subIndicator === option.value ? darkGoldTheme.gold : darkGoldTheme.textPrimary} - _hover={{ bg: 'rgba(212, 175, 55, 0.1)' }} - > - - - {option.label} - - - {option.description} - - - - ))} - - - - {/* 绘图工具选择 */} - - - } - leftIcon={} - {...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)} - minW="90px" - > - {DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'} - - - - {DRAWING_OPTIONS.map((option) => ( - setDrawingType(option.value)} - bg={drawingType === option.value ? 'rgba(212, 175, 55, 0.2)' : 'transparent'} - color={drawingType === option.value ? darkGoldTheme.gold : darkGoldTheme.textPrimary} - _hover={{ bg: 'rgba(212, 175, 55, 0.1)' }} - > - - - {option.label} - - - {option.description} - - - - ))} - - - - {/* 商品数据叠加搜索 */} - - - )} - - {/* 分时模式下的控制按钮 */} - {mode === 'minute' && ( - <> - {/* 显示/隐藏盘口 */} - - - - - {/* 刷新按钮 */} - - - )} - - {/* 模式切换按钮组 */} - - - - - - - - - {/* 卡片内容 */} + {/* 图表内容区域 */} {mode === 'daily' ? ( - // 日K线图(带技术指标) - tradeData.length > 0 ? ( - - - - ) : ( - - ) + // 日K线图 + ) : ( // 分时走势图 + 五档盘口 - minuteLoading ? ( -
- - - - 加载分时数据中... - - -
- ) : hasMinuteData ? ( - - {/* 分时图表 */} - - - - - - - {/* 五档盘口 */} - {showOrderBook && ( - - - {/* 盘口标题 */} - - - 五档盘口 - - {/* 连接状态指示 */} - - {isInTradingHours && ( - - {connected.SSE || connected.SZSE ? '实时' : '离线'} - - )} - - - - {/* 当前价格信息 */} - {currentQuote && ( - - - 当前价 - 0 ? '#ff4d4d' : - currentQuote.changePct < 0 ? '#22c55e' : - darkGoldTheme.textPrimary - } - > - {currentQuote.price?.toFixed(2) || '-'} - - - - 涨跌幅 - 0 ? '#ff4d4d' : - currentQuote.changePct < 0 ? '#22c55e' : - darkGoldTheme.textMuted - } - > - {currentQuote.changePct > 0 ? '+' : ''}{currentQuote.changePct?.toFixed(2) || '0.00'}% - - - - )} - - {/* 五档盘口面板 */} - {currentQuote && (currentQuote.bidPrices?.length > 0 || currentQuote.askPrices?.length > 0) ? ( - - ) : ( -
- - - {isInTradingHours ? '获取盘口数据中...' : '非交易时间'} - - {!isInTradingHours && ( - - 交易时间: 9:30-11:30, 13:00-15:00 - - )} - -
- )} -
-
- )} -
- ) : ( - - ) + )}
); }; -export default KLineModule; +export default memo(KLineModule); diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/DailyKLineChart.tsx b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/DailyKLineChart.tsx new file mode 100644 index 00000000..5d03248c --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/DailyKLineChart.tsx @@ -0,0 +1,90 @@ +// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/DailyKLineChart.tsx +// 日K线图表组件 + +import React, { memo, useMemo } from 'react'; +import { Box, Text, VStack, Center, Icon } from '@chakra-ui/react'; +import { InfoIcon } from '@chakra-ui/icons'; +import ReactECharts from 'echarts-for-react'; + +import { darkGoldTheme } from '../../../../constants'; +import { + getKLineDarkGoldOption, + type IndicatorType, + type MainIndicatorType, + type DrawingType, +} from '../../../../utils/chartOptions'; +import type { TradeDayData, RiseAnalysis, OverlayMetricData } from '../../../../types'; + +export interface DailyKLineChartProps { + tradeData: TradeDayData[]; + analysisMap: Record; + subIndicator: IndicatorType; + mainIndicator: MainIndicatorType; + showAnalysis: boolean; + drawingType: DrawingType; + overlayMetrics: OverlayMetricData[]; + onChartClick?: (params: { seriesName?: string; data?: [number, number] }) => void; +} + +/** + * 空状态组件 + */ +const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => ( +
+ + + + {title} + {description} + + +
+); + +/** + * 日K线图表组件 + */ +const DailyKLineChart: React.FC = ({ + tradeData, + analysisMap, + subIndicator, + mainIndicator, + showAnalysis, + drawingType, + overlayMetrics, + onChartClick, +}) => { + // 缓存图表配置 + const chartOption = useMemo(() => { + if (tradeData.length === 0) return {}; + return getKLineDarkGoldOption( + tradeData, + analysisMap, + subIndicator, + mainIndicator, + showAnalysis, + drawingType, + overlayMetrics + ); + }, [tradeData, analysisMap, subIndicator, mainIndicator, showAnalysis, drawingType, overlayMetrics]); + + // 空数据状态 + if (tradeData.length === 0) { + return ; + } + + return ( + + + + ); +}; + +export default memo(DailyKLineChart); diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/KLineToolbar.tsx b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/KLineToolbar.tsx new file mode 100644 index 00000000..18811ab3 --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/KLineToolbar.tsx @@ -0,0 +1,335 @@ +// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/KLineToolbar.tsx +// K线工具栏组件 + +import React, { memo } from 'react'; +import { + Box, + Text, + VStack, + HStack, + Button, + ButtonGroup, + Badge, + Icon, + Select, + Menu, + MenuButton, + MenuList, + MenuItem, + Tooltip, +} from '@chakra-ui/react'; +import { RepeatIcon, ChevronDownIcon, ViewIcon, ViewOffIcon } from '@chakra-ui/icons'; +import { BarChart2, TrendingUp, Calendar, LineChart, Activity, Pencil } from 'lucide-react'; + +import { darkGoldTheme, PERIOD_OPTIONS } from '../../../../constants'; +import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../../utils/chartOptions'; +import type { OverlayMetricData, MinuteData } from '../../../../types'; +import { + SUB_INDICATOR_OPTIONS, + MAIN_INDICATOR_OPTIONS, + DRAWING_OPTIONS, + type ChartMode, +} from '../constants'; +import { + ACTIVE_BUTTON_STYLE, + INACTIVE_BUTTON_STYLE, + MENU_LIST_STYLE, + getMenuItemStyle, + SELECT_STYLE, +} from '../styles'; +import MetricOverlaySearch from '../MetricOverlaySearch'; + +export interface KLineToolbarProps { + // 模式相关 + mode: ChartMode; + onModeChange: (mode: ChartMode) => void; + + // 日K模式 + selectedPeriod?: number; + onPeriodChange?: (period: number) => void; + showAnalysis: boolean; + onToggleAnalysis: () => void; + mainIndicator: MainIndicatorType; + onMainIndicatorChange: (indicator: MainIndicatorType) => void; + subIndicator: IndicatorType; + onSubIndicatorChange: (indicator: IndicatorType) => void; + drawingType: DrawingType; + onDrawingTypeChange: (type: DrawingType) => void; + overlayMetrics: OverlayMetricData[]; + onAddOverlayMetric: (metric: OverlayMetricData) => void; + onRemoveOverlayMetric: (metricId: string) => void; + stockDateRange?: { startDate: string; endDate: string }; + + // 分时模式 + minuteData?: MinuteData | null; + minuteLoading: boolean; + showOrderBook: boolean; + onToggleOrderBook: () => void; + onRefreshMinuteData: () => void; +} + +const KLineToolbar: React.FC = ({ + mode, + onModeChange, + selectedPeriod, + onPeriodChange, + showAnalysis, + onToggleAnalysis, + mainIndicator, + onMainIndicatorChange, + subIndicator, + onSubIndicatorChange, + drawingType, + onDrawingTypeChange, + overlayMetrics, + onAddOverlayMetric, + onRemoveOverlayMetric, + stockDateRange, + minuteData, + minuteLoading, + showOrderBook, + onToggleOrderBook, + onRefreshMinuteData, +}) => { + return ( + + + {/* 左侧标题区域 */} + + + {mode === 'daily' ? ( + + ) : ( + + )} + + + {mode === 'daily' ? '日K线图' : '分时走势'} + + {mode === 'minute' && minuteData?.trade_date && ( + + {minuteData.trade_date} + + )} + + + {/* 右侧控制按钮区域 */} + + {/* 日K模式下的控制按钮 */} + {mode === 'daily' && ( + <> + {/* 时间范围选择器 */} + {onPeriodChange && ( + + + + + )} + + {/* 隐藏/显示涨幅分析 */} + + + + + {/* 主图指标选择 */} + + + } + {...INACTIVE_BUTTON_STYLE} + minW="90px" + > + {MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'} + + + + {MAIN_INDICATOR_OPTIONS.map((option) => ( + onMainIndicatorChange(option.value)} + {...getMenuItemStyle(mainIndicator === option.value)} + > + + + {option.label} + + + {option.description} + + + + ))} + + + + {/* 副图指标选择 */} + + + } + leftIcon={} + {...INACTIVE_BUTTON_STYLE} + minW="100px" + > + {SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'} + + + + {SUB_INDICATOR_OPTIONS.map((option) => ( + onSubIndicatorChange(option.value)} + {...getMenuItemStyle(subIndicator === option.value)} + > + + + {option.label} + + + {option.description} + + + + ))} + + + + {/* 绘图工具选择 */} + + + } + leftIcon={} + {...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)} + minW="90px" + > + {DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'} + + + + {DRAWING_OPTIONS.map((option) => ( + onDrawingTypeChange(option.value)} + {...getMenuItemStyle(drawingType === option.value)} + > + + + {option.label} + + + {option.description} + + + + ))} + + + + {/* 商品数据叠加搜索 */} + + + )} + + {/* 分时模式下的控制按钮 */} + {mode === 'minute' && ( + <> + {/* 显示/隐藏盘口 */} + + + + + {/* 刷新按钮 */} + + + )} + + {/* 模式切换按钮组 */} + + + + + + + + ); +}; + +export default memo(KLineToolbar); diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/MinuteChartWithOrderBook.tsx b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/MinuteChartWithOrderBook.tsx new file mode 100644 index 00000000..10aaef25 --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/MinuteChartWithOrderBook.tsx @@ -0,0 +1,229 @@ +// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/MinuteChartWithOrderBook.tsx +// 分时图 + 五档盘口组件 + +import React, { memo, useMemo } from 'react'; +import { + Box, + Text, + VStack, + HStack, + Center, + Spinner, + Badge, + Grid, + GridItem, + Icon, +} from '@chakra-ui/react'; +import { InfoIcon } from '@chakra-ui/icons'; +import ReactECharts from 'echarts-for-react'; + +// 导入实时行情 Hook 和五档盘口组件 +import { useRealtimeQuote } from '@views/StockOverview/components/FlexScreen/hooks'; +import OrderBookPanel from '@views/StockOverview/components/FlexScreen/components/OrderBookPanel'; + +import { darkGoldTheme } from '../../../../constants'; +import { getMinuteKLineDarkGoldOption } from '../../../../utils/chartOptions'; +import type { MinuteData } from '../../../../types'; + +export interface MinuteChartWithOrderBookProps { + minuteData: MinuteData | null; + minuteLoading: boolean; + stockCode?: string; + showOrderBook: boolean; +} + +/** + * 空状态组件 + */ +const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => ( +
+ + + + {title} + {description} + + +
+); + +/** + * 分时图 + 五档盘口组件 + */ +const MinuteChartWithOrderBook: React.FC = ({ + minuteData, + minuteLoading, + stockCode, + showOrderBook, +}) => { + const hasMinuteData = minuteData && minuteData.data && minuteData.data.length > 0; + + // 实时行情订阅 + const subscribedCodes = useMemo(() => { + if (!stockCode) return []; + return [stockCode]; + }, [stockCode]); + + const { quotes, connected } = useRealtimeQuote(subscribedCodes); + + // 获取当前股票的行情数据 + const currentQuote = useMemo(() => { + if (!stockCode) return null; + return quotes[stockCode] || quotes[`${stockCode}.SH`] || quotes[`${stockCode}.SZ`] || null; + }, [quotes, stockCode]); + + // 判断是否在交易时间 + const isInTradingHours = useMemo(() => { + const now = new Date(); + const hours = now.getHours(); + const minutes = now.getMinutes(); + const totalMinutes = hours * 60 + minutes; + // 9:15-11:30 或 13:00-15:00 + return (totalMinutes >= 555 && totalMinutes <= 690) || (totalMinutes >= 780 && totalMinutes <= 900); + }, []); + + // 缓存图表配置 + const chartOption = useMemo(() => { + if (!minuteData) return {}; + return getMinuteKLineDarkGoldOption(minuteData); + }, [minuteData]); + + // 加载中状态 + if (minuteLoading) { + return ( +
+ + + + 加载分时数据中... + + +
+ ); + } + + // 无数据状态 + if (!hasMinuteData) { + return ; + } + + return ( + + {/* 分时图表 */} + + + + + + + {/* 五档盘口 */} + {showOrderBook && ( + + + {/* 盘口标题 */} + + + 五档盘口 + + {/* 连接状态指示 */} + + {isInTradingHours && ( + + {connected.SSE || connected.SZSE ? '实时' : '离线'} + + )} + + + + {/* 当前价格信息 */} + {currentQuote && ( + + + 当前价 + 0 ? '#ff4d4d' : + currentQuote.changePct < 0 ? '#22c55e' : + darkGoldTheme.textPrimary + } + > + {currentQuote.price?.toFixed(2) || '-'} + + + + 涨跌幅 + 0 ? '#ff4d4d' : + currentQuote.changePct < 0 ? '#22c55e' : + darkGoldTheme.textMuted + } + > + {currentQuote.changePct > 0 ? '+' : ''}{currentQuote.changePct?.toFixed(2) || '0.00'}% + + + + )} + + {/* 五档盘口面板 */} + {currentQuote && (currentQuote.bidPrices?.length > 0 || currentQuote.askPrices?.length > 0) ? ( + + ) : ( +
+ + + {isInTradingHours ? '获取盘口数据中...' : '非交易时间'} + + {!isInTradingHours && ( + + 交易时间: 9:30-11:30, 13:00-15:00 + + )} + +
+ )} +
+
+ )} +
+ ); +}; + +export default memo(MinuteChartWithOrderBook); diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/index.ts b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/index.ts new file mode 100644 index 00000000..622ad3c8 --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/index.ts @@ -0,0 +1,11 @@ +// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/index.ts +// 子组件统一导出 + +export { default as KLineToolbar } from './KLineToolbar'; +export type { KLineToolbarProps } from './KLineToolbar'; + +export { default as DailyKLineChart } from './DailyKLineChart'; +export type { DailyKLineChartProps } from './DailyKLineChart'; + +export { default as MinuteChartWithOrderBook } from './MinuteChartWithOrderBook'; +export type { MinuteChartWithOrderBookProps } from './MinuteChartWithOrderBook'; diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/constants.ts b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/constants.ts new file mode 100644 index 00000000..8b890e3f --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/constants.ts @@ -0,0 +1,41 @@ +// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/constants.ts +// K线模块常量定义 + +import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../utils/chartOptions'; + +/** + * 图表模式类型 + */ +export type ChartMode = 'daily' | 'minute'; + +/** + * 副图指标选项 + */ +export const SUB_INDICATOR_OPTIONS: { value: IndicatorType; label: string; description: string }[] = [ + { value: 'MACD', label: 'MACD', description: '平滑异同移动平均线' }, + { value: 'KDJ', label: 'KDJ', description: '随机指标' }, + { value: 'RSI', label: 'RSI', description: '相对强弱指标' }, + { value: 'WR', label: 'WR', description: '威廉指标(超买超卖)' }, + { value: 'CCI', label: 'CCI', description: '商品通道指标' }, + { value: 'BIAS', label: 'BIAS', description: '乖离率' }, + { value: 'VOL', label: '仅成交量', description: '不显示副图指标' }, +]; + +/** + * 主图指标选项 + */ +export const MAIN_INDICATOR_OPTIONS: { value: MainIndicatorType; label: string; description: string }[] = [ + { value: 'MA', label: 'MA均线', description: 'MA5/MA10/MA20' }, + { value: 'BOLL', label: '布林带', description: '布林通道指标' }, + { value: 'NONE', label: '无', description: '不显示主图指标' }, +]; + +/** + * 绘图工具选项 + */ +export const DRAWING_OPTIONS: { value: DrawingType; label: string; description: string }[] = [ + { value: 'NONE', label: '无', description: '不显示绘图工具' }, + { value: 'SUPPORT_RESISTANCE', label: '支撑/阻力', description: '自动识别支撑位和阻力位' }, + { value: 'TREND_LINE', label: '趋势线', description: '基于线性回归的趋势线' }, + { value: 'ALL', label: '全部显示', description: '显示所有参考线' }, +]; diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx index 68ab6687..c9deb315 100644 --- a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx @@ -1,54 +1,20 @@ // src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx -// 交易数据面板 - K线模块(日K/分钟切换) +// 交易数据面板 - 统一导出 -import React from 'react'; +// 默认导出 KLineModule 作为 TradeDataPanel +export { default } from './KLineModule'; -import KLineModule from './KLineModule'; -import type { Theme, TradeDayData, MinuteData, RiseAnalysis } from '../../../types'; - -export interface TradeDataPanelProps { - theme: Theme; - tradeData: TradeDayData[]; - minuteData: MinuteData | null; - minuteLoading: boolean; - analysisMap: Record; - onLoadMinuteData: () => void; - onChartClick: (params: { seriesName?: string; data?: [number, number] }) => void; - selectedPeriod?: number; - onPeriodChange?: (period: number) => void; - stockCode?: string; -} - -const TradeDataPanel: React.FC = ({ - theme, - tradeData, - minuteData, - minuteLoading, - analysisMap, - onLoadMinuteData, - onChartClick, - selectedPeriod, - onPeriodChange, - stockCode, -}) => { - return ( - - ); -}; - -export default TradeDataPanel; - -// 导出子组件供外部按需使用 +// 导出 KLineModule 及其类型 export { default as KLineModule } from './KLineModule'; export type { KLineModuleProps } from './KLineModule'; + +// 导出子组件供外部按需使用 +export { KLineToolbar, DailyKLineChart, MinuteChartWithOrderBook } from './components'; +export type { KLineToolbarProps, DailyKLineChartProps, MinuteChartWithOrderBookProps } from './components'; + +// 导出常量和样式 +export * from './constants'; +export * from './styles'; + +// 保持向后兼容的类型别名 +export type { KLineModuleProps as TradeDataPanelProps } from './KLineModule'; diff --git a/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/styles.ts b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/styles.ts new file mode 100644 index 00000000..d28fe9d0 --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/styles.ts @@ -0,0 +1,65 @@ +// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/styles.ts +// K线模块样式常量 + +import { darkGoldTheme } from '../../../constants'; + +/** + * 激活状态按钮样式 + */ +export const ACTIVE_BUTTON_STYLE = { + bg: `linear-gradient(135deg, ${darkGoldTheme.gold} 0%, ${darkGoldTheme.orange} 100%)`, + color: '#1a1a2e', + borderColor: darkGoldTheme.gold, + _hover: { + bg: `linear-gradient(135deg, ${darkGoldTheme.goldLight} 0%, ${darkGoldTheme.gold} 100%)`, + }, +} as const; + +/** + * 非激活状态按钮样式 + */ +export const INACTIVE_BUTTON_STYLE = { + bg: 'transparent', + color: darkGoldTheme.textMuted, + borderColor: darkGoldTheme.border, + _hover: { + bg: 'rgba(212, 175, 55, 0.1)', + borderColor: darkGoldTheme.gold, + color: darkGoldTheme.gold, + }, +} as const; + +/** + * 菜单项样式 + */ +export const MENU_LIST_STYLE = { + bg: '#1a1a2e', + borderColor: darkGoldTheme.border, + boxShadow: '0 4px 20px rgba(0,0,0,0.5)', +} as const; + +/** + * 获取菜单项样式 + */ +export const getMenuItemStyle = (isActive: boolean) => ({ + bg: isActive ? 'rgba(212, 175, 55, 0.2)' : 'transparent', + color: isActive ? darkGoldTheme.gold : darkGoldTheme.textPrimary, + _hover: { bg: 'rgba(212, 175, 55, 0.1)' }, +}); + +/** + * Select 下拉框样式 + */ +export const SELECT_STYLE = { + bg: 'transparent', + borderColor: darkGoldTheme.border, + color: darkGoldTheme.textPrimary, + _hover: { borderColor: darkGoldTheme.gold }, + _focus: { borderColor: darkGoldTheme.gold, boxShadow: 'none' }, + sx: { + option: { + background: '#1a1a2e', + color: darkGoldTheme.textPrimary, + }, + }, +} as const;