refactor(TradeDataPanel): 拆分 KLineModule 为独立子组件

- 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 <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 13:37:15 +08:00
parent 9ef206a9e7
commit bff440ff8a
9 changed files with 969 additions and 616 deletions

View File

@@ -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';
<MarketDataView stockCode="600000" />
```
### 单独使用 K 线模块
```tsx
import { KLineModule } from '@views/Company/components/MarketDataView/components/panels/TradeDataPanel';
<KLineModule
tradeData={tradeData}
minuteData={minuteData}
analysisMap={analysisMap}
onLoadMinuteData={loadMinuteData}
onChartClick={handleChartClick}
selectedPeriod={60}
onPeriodChange={setPeriod}
stockCode="600000"
/>
```
### 单独使用子组件
```tsx
import {
KLineToolbar,
DailyKLineChart,
MinuteChartWithOrderBook,
} from '@views/Company/components/MarketDataView/components/panels/TradeDataPanel';
// 仅工具栏
<KLineToolbar mode="daily" onModeChange={setMode} ... />
// 仅日K图表
<DailyKLineChart tradeData={data} analysisMap={map} subIndicator="MACD" ... />
// 仅分时图+盘口
<MinuteChartWithOrderBook minuteData={data} stockCode="600000" showOrderBook />
```

View File

@@ -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 }) => (
<Center h="300px">
<VStack spacing={4}>
<Icon as={InfoIcon} color={darkGoldTheme.textMuted} boxSize={12} />
<VStack spacing={2}>
<Text color={darkGoldTheme.textMuted} fontSize="lg">{title}</Text>
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">{description}</Text>
</VStack>
</VStack>
</Center>
);
// 子组件导入
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<KLineModuleProps> = ({
theme,
tradeData,
@@ -119,6 +31,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
onPeriodChange,
stockCode,
}) => {
// ========== 状态管理 ==========
const [mode, setMode] = useState<ChartMode>('daily');
const [subIndicator, setSubIndicator] = useState<IndicatorType>('MACD');
const [mainIndicator, setMainIndicator] = useState<MainIndicatorType>('MA');
@@ -126,33 +39,10 @@ const KLineModule: React.FC<KLineModuleProps> = ({
const [drawingType, setDrawingType] = useState<DrawingType>('NONE');
const [overlayMetrics, setOverlayMetrics] = useState<OverlayMetricData[]>([]);
const [showOrderBook, setShowOrderBook] = useState<boolean>(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<KLineModuleProps> = ({
};
}, [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<KLineModuleProps> = ({
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 (
<Box
bg="transparent"
overflow="hidden"
>
{/* 卡片头部 */}
<Box py={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
<HStack justify="space-between" align="center" flexWrap="wrap" gap={2}>
<HStack spacing={3}>
<Box
p={2}
borderRadius="lg"
bg={darkGoldTheme.tagBg}
>
{mode === 'daily' ? (
<TrendingUp size={20} color={darkGoldTheme.gold} />
) : (
<LineChart size={20} color={darkGoldTheme.gold} />
)}
</Box>
<Text
fontSize="lg"
fontWeight="bold"
bgGradient={`linear(to-r, ${darkGoldTheme.gold}, ${darkGoldTheme.orange})`}
bgClip="text"
>
{mode === 'daily' ? '日K线图' : '分时走势'}
</Text>
{mode === 'minute' && minuteData?.trade_date && (
<Badge
bg={darkGoldTheme.tagBg}
color={darkGoldTheme.gold}
fontSize="xs"
px={2}
py={1}
borderRadius="md"
>
{minuteData.trade_date}
</Badge>
)}
</HStack>
<Box bg="transparent" overflow="hidden">
{/* 工具栏 */}
<KLineToolbar
mode={mode}
onModeChange={handleModeChange}
selectedPeriod={selectedPeriod}
onPeriodChange={onPeriodChange}
showAnalysis={showAnalysis}
onToggleAnalysis={handleToggleAnalysis}
mainIndicator={mainIndicator}
onMainIndicatorChange={handleMainIndicatorChange}
subIndicator={subIndicator}
onSubIndicatorChange={handleSubIndicatorChange}
drawingType={drawingType}
onDrawingTypeChange={handleDrawingTypeChange}
overlayMetrics={overlayMetrics}
onAddOverlayMetric={handleAddOverlayMetric}
onRemoveOverlayMetric={handleRemoveOverlayMetric}
stockDateRange={stockDateRange}
minuteData={minuteData}
minuteLoading={minuteLoading}
showOrderBook={showOrderBook}
onToggleOrderBook={handleToggleOrderBook}
onRefreshMinuteData={onLoadMinuteData}
/>
<HStack spacing={2} flexWrap="wrap">
{/* 日K模式下显示时间范围选择器和指标选择 */}
{mode === 'daily' && (
<>
{/* 时间范围选择器 */}
{onPeriodChange && (
<HStack spacing={1}>
<Icon as={Calendar} boxSize={4} color={darkGoldTheme.textMuted} />
<Select
size="sm"
value={selectedPeriod}
onChange={(e) => onPeriodChange(Number(e.target.value))}
bg="transparent"
borderColor={darkGoldTheme.border}
color={darkGoldTheme.textPrimary}
maxW="85px"
_hover={{ borderColor: darkGoldTheme.gold }}
_focus={{ borderColor: darkGoldTheme.gold, boxShadow: 'none' }}
sx={{
option: {
background: '#1a1a2e',
color: darkGoldTheme.textPrimary,
},
}}
>
{PERIOD_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
)}
{/* 隐藏/显示涨幅分析 */}
<Tooltip label={showAnalysis ? '隐藏涨幅分析标记' : '显示涨幅分析标记'} placement="top" hasArrow>
<Button
size="sm"
variant="outline"
leftIcon={showAnalysis ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowAnalysis(!showAnalysis)}
{...(showAnalysis ? INACTIVE_BUTTON_STYLE : ACTIVE_BUTTON_STYLE)}
minW="90px"
>
{showAnalysis ? '隐藏分析' : '显示分析'}
</Button>
</Tooltip>
{/* 主图指标选择 */}
<Menu>
<Tooltip label="主图指标" placement="top" hasArrow>
<MenuButton
as={Button}
size="sm"
variant="outline"
rightIcon={<ChevronDownIcon />}
{...INACTIVE_BUTTON_STYLE}
minW="90px"
>
{MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'}
</MenuButton>
</Tooltip>
<MenuList
bg="#1a1a2e"
borderColor={darkGoldTheme.border}
boxShadow="0 4px 20px rgba(0,0,0,0.5)"
>
{MAIN_INDICATOR_OPTIONS.map((option) => (
<MenuItem
key={option.value}
onClick={() => 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)' }}
>
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight={mainIndicator === option.value ? 'bold' : 'normal'}>
{option.label}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{option.description}
</Text>
</VStack>
</MenuItem>
))}
</MenuList>
</Menu>
{/* 副图指标选择 */}
<Menu>
<Tooltip label="副图指标" placement="top" hasArrow>
<MenuButton
as={Button}
size="sm"
variant="outline"
rightIcon={<ChevronDownIcon />}
leftIcon={<Activity size={14} />}
{...INACTIVE_BUTTON_STYLE}
minW="100px"
>
{SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'}
</MenuButton>
</Tooltip>
<MenuList
bg="#1a1a2e"
borderColor={darkGoldTheme.border}
boxShadow="0 4px 20px rgba(0,0,0,0.5)"
>
{SUB_INDICATOR_OPTIONS.map((option) => (
<MenuItem
key={option.value}
onClick={() => 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)' }}
>
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight={subIndicator === option.value ? 'bold' : 'normal'}>
{option.label}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{option.description}
</Text>
</VStack>
</MenuItem>
))}
</MenuList>
</Menu>
{/* 绘图工具选择 */}
<Menu>
<Tooltip label="绘图工具" placement="top" hasArrow>
<MenuButton
as={Button}
size="sm"
variant="outline"
rightIcon={<ChevronDownIcon />}
leftIcon={<Pencil size={14} />}
{...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
minW="90px"
>
{DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'}
</MenuButton>
</Tooltip>
<MenuList
bg="#1a1a2e"
borderColor={darkGoldTheme.border}
boxShadow="0 4px 20px rgba(0,0,0,0.5)"
>
{DRAWING_OPTIONS.map((option) => (
<MenuItem
key={option.value}
onClick={() => 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)' }}
>
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight={drawingType === option.value ? 'bold' : 'normal'}>
{option.label}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{option.description}
</Text>
</VStack>
</MenuItem>
))}
</MenuList>
</Menu>
{/* 商品数据叠加搜索 */}
<MetricOverlaySearch
overlayMetrics={overlayMetrics}
onAddMetric={handleAddOverlayMetric}
onRemoveMetric={handleRemoveOverlayMetric}
stockDateRange={stockDateRange}
/>
</>
)}
{/* 分时模式下的控制按钮 */}
{mode === 'minute' && (
<>
{/* 显示/隐藏盘口 */}
<Tooltip label={showOrderBook ? '隐藏盘口' : '显示盘口'} placement="top" hasArrow>
<Button
size="sm"
variant="outline"
onClick={() => setShowOrderBook(!showOrderBook)}
{...(showOrderBook ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
minW="80px"
>
{showOrderBook ? '隐藏盘口' : '显示盘口'}
</Button>
</Tooltip>
{/* 刷新按钮 */}
<Button
leftIcon={<RepeatIcon />}
size="sm"
variant="outline"
onClick={onLoadMinuteData}
isLoading={minuteLoading}
loadingText="获取中"
{...INACTIVE_BUTTON_STYLE}
>
</Button>
</>
)}
{/* 模式切换按钮组 */}
<ButtonGroup size="sm" isAttached>
<Button
leftIcon={<BarChart2 size={14} />}
onClick={() => handleModeChange('daily')}
{...(mode === 'daily' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
>
K
</Button>
<Button
leftIcon={<LineChart size={14} />}
onClick={() => handleModeChange('minute')}
{...(mode === 'minute' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
>
</Button>
</ButtonGroup>
</HStack>
</HStack>
</Box>
{/* 卡片内容 */}
{/* 图表内容区域 */}
<Box pt={4}>
{mode === 'daily' ? (
// 日K线图(带技术指标)
tradeData.length > 0 ? (
<Box h="650px">
<ReactECharts
option={getKLineDarkGoldOption(tradeData, analysisMap, subIndicator, mainIndicator, showAnalysis, drawingType, overlayMetrics)}
style={{ height: '100%', width: '100%' }}
theme="dark"
notMerge={true}
onEvents={{ click: onChartClick }}
opts={{ renderer: 'canvas' }}
/>
</Box>
) : (
<EmptyState title="暂无日K线数据" description="该股票暂无交易数据" />
)
// 日K线图
<DailyKLineChart
tradeData={tradeData}
analysisMap={analysisMap}
subIndicator={subIndicator}
mainIndicator={mainIndicator}
showAnalysis={showAnalysis}
drawingType={drawingType}
overlayMetrics={overlayMetrics}
onChartClick={onChartClick}
/>
) : (
// 分时走势图 + 五档盘口
minuteLoading ? (
<Center h="450px">
<VStack spacing={4}>
<Spinner
thickness="4px"
speed="0.65s"
emptyColor="rgba(212, 175, 55, 0.2)"
color={darkGoldTheme.gold}
size="lg"
/>
<Text color={darkGoldTheme.textMuted} fontSize="sm">
...
</Text>
</VStack>
</Center>
) : hasMinuteData ? (
<Grid templateColumns={showOrderBook ? '1fr 220px' : '1fr'} gap={4} h="450px">
{/* 分时图表 */}
<GridItem>
<Box h="100%">
<ReactECharts
option={getMinuteKLineDarkGoldOption(minuteData)}
style={{ height: '100%', width: '100%' }}
theme="dark"
notMerge={true}
opts={{ renderer: 'canvas' }}
/>
</Box>
</GridItem>
{/* 五档盘口 */}
{showOrderBook && (
<GridItem>
<Box
h="100%"
bg="rgba(0, 0, 0, 0.3)"
borderRadius="lg"
border="1px solid"
borderColor={darkGoldTheme.border}
p={3}
overflowY="auto"
>
{/* 盘口标题 */}
<HStack justify="space-between" mb={3}>
<Text fontSize="sm" fontWeight="bold" color={darkGoldTheme.gold}>
</Text>
{/* 连接状态指示 */}
<HStack spacing={1}>
{isInTradingHours && (
<Badge
bg={connected.SSE || connected.SZSE ? 'green.500' : 'gray.500'}
color="white"
fontSize="2xs"
px={1}
>
{connected.SSE || connected.SZSE ? '实时' : '离线'}
</Badge>
)}
</HStack>
</HStack>
{/* 当前价格信息 */}
{currentQuote && (
<VStack spacing={1} mb={3} align="stretch">
<HStack justify="space-between">
<Text fontSize="xs" color={darkGoldTheme.textMuted}></Text>
<Text
fontSize="lg"
fontWeight="bold"
color={
currentQuote.changePct > 0 ? '#ff4d4d' :
currentQuote.changePct < 0 ? '#22c55e' :
darkGoldTheme.textPrimary
}
>
{currentQuote.price?.toFixed(2) || '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text fontSize="xs" color={darkGoldTheme.textMuted}></Text>
<Text
fontSize="sm"
color={
currentQuote.changePct > 0 ? '#ff4d4d' :
currentQuote.changePct < 0 ? '#22c55e' :
darkGoldTheme.textMuted
}
>
{currentQuote.changePct > 0 ? '+' : ''}{currentQuote.changePct?.toFixed(2) || '0.00'}%
</Text>
</HStack>
</VStack>
)}
{/* 五档盘口面板 */}
{currentQuote && (currentQuote.bidPrices?.length > 0 || currentQuote.askPrices?.length > 0) ? (
<OrderBookPanel
bidPrices={currentQuote.bidPrices || []}
bidVolumes={currentQuote.bidVolumes || []}
askPrices={currentQuote.askPrices || []}
askVolumes={currentQuote.askVolumes || []}
prevClose={currentQuote.prevClose}
upperLimit={'upperLimit' in currentQuote ? currentQuote.upperLimit : undefined}
lowerLimit={'lowerLimit' in currentQuote ? currentQuote.lowerLimit : undefined}
defaultLevels={5}
/>
) : (
<Center h="200px">
<VStack spacing={2}>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{isInTradingHours ? '获取盘口数据中...' : '非交易时间'}
</Text>
{!isInTradingHours && (
<Text fontSize="2xs" color={darkGoldTheme.textMuted}>
交易时间: 9:30-11:30, 13:00-15:00
</Text>
)}
</VStack>
</Center>
)}
</Box>
</GridItem>
)}
</Grid>
) : (
<EmptyState title="暂无分时数据" description="点击刷新按钮获取当日分时数据" />
)
<MinuteChartWithOrderBook
minuteData={minuteData}
minuteLoading={minuteLoading}
stockCode={stockCode}
showOrderBook={showOrderBook}
/>
)}
</Box>
</Box>
);
};
export default KLineModule;
export default memo(KLineModule);

View File

@@ -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<number, RiseAnalysis>;
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 }) => (
<Center h="300px">
<VStack spacing={4}>
<Icon as={InfoIcon} color={darkGoldTheme.textMuted} boxSize={12} />
<VStack spacing={2}>
<Text color={darkGoldTheme.textMuted} fontSize="lg">{title}</Text>
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">{description}</Text>
</VStack>
</VStack>
</Center>
);
/**
* 日K线图表组件
*/
const DailyKLineChart: React.FC<DailyKLineChartProps> = ({
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 <EmptyState title="暂无日K线数据" description="该股票暂无交易数据" />;
}
return (
<Box h="650px">
<ReactECharts
option={chartOption}
style={{ height: '100%', width: '100%' }}
theme="dark"
notMerge={true}
onEvents={onChartClick ? { click: onChartClick } : undefined}
opts={{ renderer: 'canvas' }}
/>
</Box>
);
};
export default memo(DailyKLineChart);

View File

@@ -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<KLineToolbarProps> = ({
mode,
onModeChange,
selectedPeriod,
onPeriodChange,
showAnalysis,
onToggleAnalysis,
mainIndicator,
onMainIndicatorChange,
subIndicator,
onSubIndicatorChange,
drawingType,
onDrawingTypeChange,
overlayMetrics,
onAddOverlayMetric,
onRemoveOverlayMetric,
stockDateRange,
minuteData,
minuteLoading,
showOrderBook,
onToggleOrderBook,
onRefreshMinuteData,
}) => {
return (
<Box py={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
<HStack justify="space-between" align="center" flexWrap="wrap" gap={2}>
{/* 左侧标题区域 */}
<HStack spacing={3}>
<Box p={2} borderRadius="lg" bg={darkGoldTheme.tagBg}>
{mode === 'daily' ? (
<TrendingUp size={20} color={darkGoldTheme.gold} />
) : (
<LineChart size={20} color={darkGoldTheme.gold} />
)}
</Box>
<Text
fontSize="lg"
fontWeight="bold"
bgGradient={`linear(to-r, ${darkGoldTheme.gold}, ${darkGoldTheme.orange})`}
bgClip="text"
>
{mode === 'daily' ? '日K线图' : '分时走势'}
</Text>
{mode === 'minute' && minuteData?.trade_date && (
<Badge
bg={darkGoldTheme.tagBg}
color={darkGoldTheme.gold}
fontSize="xs"
px={2}
py={1}
borderRadius="md"
>
{minuteData.trade_date}
</Badge>
)}
</HStack>
{/* 右侧控制按钮区域 */}
<HStack spacing={2} flexWrap="wrap">
{/* 日K模式下的控制按钮 */}
{mode === 'daily' && (
<>
{/* 时间范围选择器 */}
{onPeriodChange && (
<HStack spacing={1}>
<Icon as={Calendar} boxSize={4} color={darkGoldTheme.textMuted} />
<Select
size="sm"
value={selectedPeriod}
onChange={(e) => onPeriodChange(Number(e.target.value))}
maxW="85px"
{...SELECT_STYLE}
>
{PERIOD_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</HStack>
)}
{/* 隐藏/显示涨幅分析 */}
<Tooltip label={showAnalysis ? '隐藏涨幅分析标记' : '显示涨幅分析标记'} placement="top" hasArrow>
<Button
size="sm"
variant="outline"
leftIcon={showAnalysis ? <ViewOffIcon /> : <ViewIcon />}
onClick={onToggleAnalysis}
{...(showAnalysis ? INACTIVE_BUTTON_STYLE : ACTIVE_BUTTON_STYLE)}
minW="90px"
>
{showAnalysis ? '隐藏分析' : '显示分析'}
</Button>
</Tooltip>
{/* 主图指标选择 */}
<Menu>
<Tooltip label="主图指标" placement="top" hasArrow>
<MenuButton
as={Button}
size="sm"
variant="outline"
rightIcon={<ChevronDownIcon />}
{...INACTIVE_BUTTON_STYLE}
minW="90px"
>
{MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'}
</MenuButton>
</Tooltip>
<MenuList {...MENU_LIST_STYLE}>
{MAIN_INDICATOR_OPTIONS.map((option) => (
<MenuItem
key={option.value}
onClick={() => onMainIndicatorChange(option.value)}
{...getMenuItemStyle(mainIndicator === option.value)}
>
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight={mainIndicator === option.value ? 'bold' : 'normal'}>
{option.label}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{option.description}
</Text>
</VStack>
</MenuItem>
))}
</MenuList>
</Menu>
{/* 副图指标选择 */}
<Menu>
<Tooltip label="副图指标" placement="top" hasArrow>
<MenuButton
as={Button}
size="sm"
variant="outline"
rightIcon={<ChevronDownIcon />}
leftIcon={<Activity size={14} />}
{...INACTIVE_BUTTON_STYLE}
minW="100px"
>
{SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'}
</MenuButton>
</Tooltip>
<MenuList {...MENU_LIST_STYLE}>
{SUB_INDICATOR_OPTIONS.map((option) => (
<MenuItem
key={option.value}
onClick={() => onSubIndicatorChange(option.value)}
{...getMenuItemStyle(subIndicator === option.value)}
>
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight={subIndicator === option.value ? 'bold' : 'normal'}>
{option.label}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{option.description}
</Text>
</VStack>
</MenuItem>
))}
</MenuList>
</Menu>
{/* 绘图工具选择 */}
<Menu>
<Tooltip label="绘图工具" placement="top" hasArrow>
<MenuButton
as={Button}
size="sm"
variant="outline"
rightIcon={<ChevronDownIcon />}
leftIcon={<Pencil size={14} />}
{...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
minW="90px"
>
{DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'}
</MenuButton>
</Tooltip>
<MenuList {...MENU_LIST_STYLE}>
{DRAWING_OPTIONS.map((option) => (
<MenuItem
key={option.value}
onClick={() => onDrawingTypeChange(option.value)}
{...getMenuItemStyle(drawingType === option.value)}
>
<VStack align="start" spacing={0}>
<Text fontSize="sm" fontWeight={drawingType === option.value ? 'bold' : 'normal'}>
{option.label}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{option.description}
</Text>
</VStack>
</MenuItem>
))}
</MenuList>
</Menu>
{/* 商品数据叠加搜索 */}
<MetricOverlaySearch
overlayMetrics={overlayMetrics}
onAddMetric={onAddOverlayMetric}
onRemoveMetric={onRemoveOverlayMetric}
stockDateRange={stockDateRange}
/>
</>
)}
{/* 分时模式下的控制按钮 */}
{mode === 'minute' && (
<>
{/* 显示/隐藏盘口 */}
<Tooltip label={showOrderBook ? '隐藏盘口' : '显示盘口'} placement="top" hasArrow>
<Button
size="sm"
variant="outline"
onClick={onToggleOrderBook}
{...(showOrderBook ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
minW="80px"
>
{showOrderBook ? '隐藏盘口' : '显示盘口'}
</Button>
</Tooltip>
{/* 刷新按钮 */}
<Button
leftIcon={<RepeatIcon />}
size="sm"
variant="outline"
onClick={onRefreshMinuteData}
isLoading={minuteLoading}
loadingText="获取中"
{...INACTIVE_BUTTON_STYLE}
>
</Button>
</>
)}
{/* 模式切换按钮组 */}
<ButtonGroup size="sm" isAttached>
<Button
leftIcon={<BarChart2 size={14} />}
onClick={() => onModeChange('daily')}
{...(mode === 'daily' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
>
K
</Button>
<Button
leftIcon={<LineChart size={14} />}
onClick={() => onModeChange('minute')}
{...(mode === 'minute' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
>
</Button>
</ButtonGroup>
</HStack>
</HStack>
</Box>
);
};
export default memo(KLineToolbar);

View File

@@ -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 }) => (
<Center h="300px">
<VStack spacing={4}>
<Icon as={InfoIcon} color={darkGoldTheme.textMuted} boxSize={12} />
<VStack spacing={2}>
<Text color={darkGoldTheme.textMuted} fontSize="lg">{title}</Text>
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">{description}</Text>
</VStack>
</VStack>
</Center>
);
/**
* 分时图 + 五档盘口组件
*/
const MinuteChartWithOrderBook: React.FC<MinuteChartWithOrderBookProps> = ({
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 (
<Center h="450px">
<VStack spacing={4}>
<Spinner
thickness="4px"
speed="0.65s"
emptyColor="rgba(212, 175, 55, 0.2)"
color={darkGoldTheme.gold}
size="lg"
/>
<Text color={darkGoldTheme.textMuted} fontSize="sm">
...
</Text>
</VStack>
</Center>
);
}
// 无数据状态
if (!hasMinuteData) {
return <EmptyState title="暂无分时数据" description="点击刷新按钮获取当日分时数据" />;
}
return (
<Grid templateColumns={showOrderBook ? '1fr 220px' : '1fr'} gap={4} h="450px">
{/* 分时图表 */}
<GridItem>
<Box h="100%">
<ReactECharts
option={chartOption}
style={{ height: '100%', width: '100%' }}
theme="dark"
notMerge={true}
opts={{ renderer: 'canvas' }}
/>
</Box>
</GridItem>
{/* 五档盘口 */}
{showOrderBook && (
<GridItem>
<Box
h="100%"
bg="rgba(0, 0, 0, 0.3)"
borderRadius="lg"
border="1px solid"
borderColor={darkGoldTheme.border}
p={3}
overflowY="auto"
>
{/* 盘口标题 */}
<HStack justify="space-between" mb={3}>
<Text fontSize="sm" fontWeight="bold" color={darkGoldTheme.gold}>
</Text>
{/* 连接状态指示 */}
<HStack spacing={1}>
{isInTradingHours && (
<Badge
bg={connected.SSE || connected.SZSE ? 'green.500' : 'gray.500'}
color="white"
fontSize="2xs"
px={1}
>
{connected.SSE || connected.SZSE ? '实时' : '离线'}
</Badge>
)}
</HStack>
</HStack>
{/* 当前价格信息 */}
{currentQuote && (
<VStack spacing={1} mb={3} align="stretch">
<HStack justify="space-between">
<Text fontSize="xs" color={darkGoldTheme.textMuted}></Text>
<Text
fontSize="lg"
fontWeight="bold"
color={
currentQuote.changePct > 0 ? '#ff4d4d' :
currentQuote.changePct < 0 ? '#22c55e' :
darkGoldTheme.textPrimary
}
>
{currentQuote.price?.toFixed(2) || '-'}
</Text>
</HStack>
<HStack justify="space-between">
<Text fontSize="xs" color={darkGoldTheme.textMuted}></Text>
<Text
fontSize="sm"
color={
currentQuote.changePct > 0 ? '#ff4d4d' :
currentQuote.changePct < 0 ? '#22c55e' :
darkGoldTheme.textMuted
}
>
{currentQuote.changePct > 0 ? '+' : ''}{currentQuote.changePct?.toFixed(2) || '0.00'}%
</Text>
</HStack>
</VStack>
)}
{/* 五档盘口面板 */}
{currentQuote && (currentQuote.bidPrices?.length > 0 || currentQuote.askPrices?.length > 0) ? (
<OrderBookPanel
bidPrices={currentQuote.bidPrices || []}
bidVolumes={currentQuote.bidVolumes || []}
askPrices={currentQuote.askPrices || []}
askVolumes={currentQuote.askVolumes || []}
prevClose={currentQuote.prevClose}
upperLimit={'upperLimit' in currentQuote ? currentQuote.upperLimit : undefined}
lowerLimit={'lowerLimit' in currentQuote ? currentQuote.lowerLimit : undefined}
defaultLevels={5}
/>
) : (
<Center h="200px">
<VStack spacing={2}>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{isInTradingHours ? '获取盘口数据中...' : '非交易时间'}
</Text>
{!isInTradingHours && (
<Text fontSize="2xs" color={darkGoldTheme.textMuted}>
交易时间: 9:30-11:30, 13:00-15:00
</Text>
)}
</VStack>
</Center>
)}
</Box>
</GridItem>
)}
</Grid>
);
};
export default memo(MinuteChartWithOrderBook);

View File

@@ -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';

View File

@@ -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: '显示所有参考线' },
];

View File

@@ -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<number, RiseAnalysis>;
onLoadMinuteData: () => void;
onChartClick: (params: { seriesName?: string; data?: [number, number] }) => void;
selectedPeriod?: number;
onPeriodChange?: (period: number) => void;
stockCode?: string;
}
const TradeDataPanel: React.FC<TradeDataPanelProps> = ({
theme,
tradeData,
minuteData,
minuteLoading,
analysisMap,
onLoadMinuteData,
onChartClick,
selectedPeriod,
onPeriodChange,
stockCode,
}) => {
return (
<KLineModule
theme={theme}
tradeData={tradeData}
minuteData={minuteData}
minuteLoading={minuteLoading}
analysisMap={analysisMap}
onLoadMinuteData={onLoadMinuteData}
onChartClick={onChartClick}
selectedPeriod={selectedPeriod}
onPeriodChange={onPeriodChange}
stockCode={stockCode}
/>
);
};
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';

View File

@@ -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;