refactor(TradeDataPanel): 原子设计模式拆分重构
- 将 TradeDataPanel.tsx (382行) 拆分为 8 个 TypeScript 文件 - 创建 3 个原子组件: MinuteStats、TradeAnalysis、EmptyState - 创建 3 个业务组件: KLineChart、MinuteKLineSection、TradeTable - 主入口组件精简至 ~50 行,降低 87% - 更新 panels/index.ts 导出子组件 - 更新 STRUCTURE.md 文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Company 目录结构说明
|
# Company 目录结构说明
|
||||||
|
|
||||||
> 最后更新:2025-12-12
|
> 最后更新:2025-12-16
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ src/views/Company/
|
|||||||
│ ├── MarketDataView/ # Tab: 股票行情(TypeScript)
|
│ ├── MarketDataView/ # Tab: 股票行情(TypeScript)
|
||||||
│ │ ├── index.tsx # 主组件入口(~285 行,Tab 容器)
|
│ │ ├── index.tsx # 主组件入口(~285 行,Tab 容器)
|
||||||
│ │ ├── types.ts # 类型定义
|
│ │ ├── types.ts # 类型定义
|
||||||
│ │ ├── constants.ts # 主题配置、常量
|
│ │ ├── constants.ts # 主题配置、常量(含黑金主题 darkGoldTheme)
|
||||||
│ │ ├── services/
|
│ │ ├── services/
|
||||||
│ │ │ └── marketService.ts # API 服务层
|
│ │ │ └── marketService.ts # API 服务层
|
||||||
│ │ ├── hooks/
|
│ │ ├── hooks/
|
||||||
@@ -81,11 +81,31 @@ src/views/Company/
|
|||||||
│ │ ├── index.ts # 组件导出
|
│ │ ├── index.ts # 组件导出
|
||||||
│ │ ├── ThemedCard.tsx # 主题化卡片
|
│ │ ├── ThemedCard.tsx # 主题化卡片
|
||||||
│ │ ├── MarkdownRenderer.tsx # Markdown 渲染
|
│ │ ├── MarkdownRenderer.tsx # Markdown 渲染
|
||||||
│ │ ├── StockSummaryCard.tsx # 股票概览卡片
|
|
||||||
│ │ ├── AnalysisModal.tsx # 涨幅分析模态框
|
│ │ ├── AnalysisModal.tsx # 涨幅分析模态框
|
||||||
|
│ │ ├── StockSummaryCard/ # 股票概览卡片(黑金主题 4 列布局)
|
||||||
|
│ │ │ ├── index.tsx # 主组件(4 列 SimpleGrid 布局)
|
||||||
|
│ │ │ ├── StockHeaderCard.tsx # 股票信息卡片(名称、价格、涨跌幅)
|
||||||
|
│ │ │ ├── MetricCard.tsx # 指标卡片模板
|
||||||
|
│ │ │ ├── utils.ts # 状态计算工具函数
|
||||||
|
│ │ │ └── atoms/ # 原子组件
|
||||||
|
│ │ │ ├── index.ts # 原子组件导出
|
||||||
|
│ │ │ ├── DarkGoldCard.tsx # 黑金主题卡片容器
|
||||||
|
│ │ │ ├── CardTitle.tsx # 卡片标题(图标+标题+副标题)
|
||||||
|
│ │ │ ├── MetricValue.tsx # 核心数值展示
|
||||||
|
│ │ │ ├── PriceDisplay.tsx # 价格显示(价格+涨跌箭头)
|
||||||
|
│ │ │ └── StatusTag.tsx # 状态标签(活跃/健康等)
|
||||||
│ │ └── panels/ # Tab 面板组件
|
│ │ └── panels/ # Tab 面板组件
|
||||||
│ │ ├── index.ts # 面板组件统一导出
|
│ │ ├── index.ts # 面板组件统一导出
|
||||||
│ │ ├── TradeDataPanel.tsx # 交易数据(K线图、分钟图、表格)
|
│ │ ├── TradeDataPanel/ # 交易数据面板(原子设计模式)
|
||||||
|
│ │ │ ├── index.tsx # 主入口组件(~50 行)
|
||||||
|
│ │ │ ├── KLineChart.tsx # 日K线图组件(~40 行)
|
||||||
|
│ │ │ ├── MinuteKLineSection.tsx # 分钟K线区域(~95 行)
|
||||||
|
│ │ │ ├── TradeTable.tsx # 交易明细表格(~75 行)
|
||||||
|
│ │ │ └── atoms/ # 原子组件
|
||||||
|
│ │ │ ├── index.ts # 统一导出
|
||||||
|
│ │ │ ├── MinuteStats.tsx # 分钟数据统计(~80 行)
|
||||||
|
│ │ │ ├── TradeAnalysis.tsx # 成交分析(~65 行)
|
||||||
|
│ │ │ └── EmptyState.tsx # 空状态组件(~35 行)
|
||||||
│ │ ├── FundingPanel.tsx # 融资融券面板
|
│ │ ├── FundingPanel.tsx # 融资融券面板
|
||||||
│ │ ├── BigDealPanel.tsx # 大宗交易面板
|
│ │ ├── BigDealPanel.tsx # 大宗交易面板
|
||||||
│ │ ├── UnusualPanel.tsx # 龙虎榜面板
|
│ │ ├── UnusualPanel.tsx # 龙虎榜面板
|
||||||
@@ -835,3 +855,148 @@ MarketDataView/components/panels/
|
|||||||
- **职责分离**:主组件只负责 Tab 容器和状态管理
|
- **职责分离**:主组件只负责 Tab 容器和状态管理
|
||||||
- **组件复用**:面板组件可独立测试和维护
|
- **组件复用**:面板组件可独立测试和维护
|
||||||
- **类型安全**:每个面板组件有独立的 Props 类型定义
|
- **类型安全**:每个面板组件有独立的 Props 类型定义
|
||||||
|
|
||||||
|
### 2025-12-16 StockSummaryCard 黑金主题重构
|
||||||
|
|
||||||
|
**改动概述**:
|
||||||
|
- `StockSummaryCard.tsx` 从单文件重构为**原子设计模式**的目录结构
|
||||||
|
- 布局从 **1+3**(头部+三卡片)改为 **4 列横向排列**
|
||||||
|
- 新增**黑金主题**(`darkGoldTheme`)
|
||||||
|
- 提取 **5 个原子组件** + **2 个业务组件**
|
||||||
|
|
||||||
|
**拆分后文件结构**:
|
||||||
|
```
|
||||||
|
StockSummaryCard/
|
||||||
|
├── index.tsx # 主组件(4 列 SimpleGrid 布局)
|
||||||
|
├── StockHeaderCard.tsx # 股票信息卡片(名称、价格、涨跌幅、走势)
|
||||||
|
├── MetricCard.tsx # 指标卡片模板组件
|
||||||
|
├── utils.ts # 状态计算工具函数
|
||||||
|
└── atoms/ # 原子组件
|
||||||
|
├── index.ts # 统一导出
|
||||||
|
├── DarkGoldCard.tsx # 黑金主题卡片容器(渐变背景、金色边框)
|
||||||
|
├── CardTitle.tsx # 卡片标题(图标+标题+副标题)
|
||||||
|
├── MetricValue.tsx # 核心数值展示(标签+数值+后缀)
|
||||||
|
├── PriceDisplay.tsx # 价格显示(价格+涨跌箭头+百分比)
|
||||||
|
└── StatusTag.tsx # 状态标签(活跃/健康/警惕等)
|
||||||
|
```
|
||||||
|
|
||||||
|
**4 列布局设计**:
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ 股票信息 │ │ 交易热度 │ │ 估值VS安全 │ │ 情绪与风险 │
|
||||||
|
│ 平安银行 │ │ (流动性) │ │ (便宜否) │ │ (资金面) │
|
||||||
|
│ (000001) │ │ │ │ │ │ │
|
||||||
|
│ 13.50 ↗+1.89%│ │ 成交额 46.79亿│ │ PE 4.96 │ │ 融资 58.23亿 │
|
||||||
|
│ 走势:小幅上涨 │ │ 成交量|换手率 │ │ 质押率(健康) │ │ 融券 1.26亿 │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**黑金主题配置**(`constants.ts`):
|
||||||
|
```typescript
|
||||||
|
export const darkGoldTheme = {
|
||||||
|
bgCard: 'linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%)',
|
||||||
|
border: 'rgba(212, 175, 55, 0.3)',
|
||||||
|
gold: '#D4AF37',
|
||||||
|
orange: '#FF9500',
|
||||||
|
green: '#00C851',
|
||||||
|
red: '#FF4444',
|
||||||
|
textPrimary: '#FFFFFF',
|
||||||
|
textMuted: 'rgba(255, 255, 255, 0.6)',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态计算工具**(`utils.ts`):
|
||||||
|
| 函数 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `getTrendDescription` | 根据涨跌幅返回走势描述(强势上涨/小幅下跌等) |
|
||||||
|
| `getTurnoverStatus` | 换手率状态(≥3% 活跃, ≥1% 正常, <1% 冷清) |
|
||||||
|
| `getPEStatus` | 市盈率估值评级(极低估值/合理/偏高/泡沫风险) |
|
||||||
|
| `getPledgeStatus` | 质押率健康状态(<10% 健康, <30% 正常, <50% 偏高, ≥50% 警惕) |
|
||||||
|
| `getPriceColor` | 根据涨跌返回颜色(红涨绿跌) |
|
||||||
|
|
||||||
|
**原子组件说明**:
|
||||||
|
| 组件 | 行数 | 用途 | 可复用场景 |
|
||||||
|
|------|------|------|-----------|
|
||||||
|
| `DarkGoldCard` | ~40 | 黑金主题卡片容器 | 任何需要黑金风格的卡片 |
|
||||||
|
| `CardTitle` | ~30 | 卡片标题行 | 带图标的标题展示 |
|
||||||
|
| `MetricValue` | ~45 | 核心数值展示 | 各种指标数值展示 |
|
||||||
|
| `PriceDisplay` | ~55 | 价格+涨跌幅 | 股票价格展示 |
|
||||||
|
| `StatusTag` | ~20 | 状态标签 | 各种状态文字标签 |
|
||||||
|
|
||||||
|
**响应式断点**:
|
||||||
|
- `lg` (≥992px): 4 列
|
||||||
|
- `md` (≥768px): 2 列
|
||||||
|
- `base` (<768px): 1 列
|
||||||
|
|
||||||
|
**类型定义更新**(`types.ts`):
|
||||||
|
- `StockSummaryCardProps.theme` 改为可选参数,组件内置使用 `darkGoldTheme`
|
||||||
|
|
||||||
|
**优化效果**:
|
||||||
|
| 指标 | 优化前 | 优化后 | 改善 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| 主文件行数 | ~350 | ~115 | -67% |
|
||||||
|
| 文件数量 | 1 | 8 | 原子设计模式 |
|
||||||
|
| 可复用组件 | 0 | 5 原子 + 2 业务 | 提升 |
|
||||||
|
| 主题支持 | 依赖传入 | 内置黑金主题 | 独立 |
|
||||||
|
|
||||||
|
**设计原则**:
|
||||||
|
- **原子设计模式**:atoms(基础元素)→ 业务组件(MetricCard、StockHeaderCard)→ 页面组件(index.tsx)
|
||||||
|
- **主题独立**:StockSummaryCard 使用内置黑金主题,不依赖外部传入
|
||||||
|
- **职责分离**:状态计算逻辑提取到 `utils.ts`,UI 与逻辑解耦
|
||||||
|
- **组件复用**:原子组件可在其他黑金主题场景复用
|
||||||
|
|
||||||
|
### 2025-12-16 TradeDataPanel 原子设计模式拆分
|
||||||
|
|
||||||
|
**改动概述**:
|
||||||
|
- `TradeDataPanel.tsx` 从 **382 行** 拆分为 **8 个 TypeScript 文件**
|
||||||
|
- 采用**原子设计模式**组织代码
|
||||||
|
- 提取 **3 个原子组件** + **3 个业务组件**
|
||||||
|
|
||||||
|
**拆分后文件结构**:
|
||||||
|
```
|
||||||
|
TradeDataPanel/
|
||||||
|
├── index.tsx # 主入口组件(~50 行,组合 3 个子组件)
|
||||||
|
├── KLineChart.tsx # 日K线图组件(~40 行)
|
||||||
|
├── MinuteKLineSection.tsx # 分钟K线区域(~95 行,含加载/空状态处理)
|
||||||
|
├── TradeTable.tsx # 交易明细表格(~75 行)
|
||||||
|
└── atoms/ # 原子组件
|
||||||
|
├── index.ts # 统一导出
|
||||||
|
├── MinuteStats.tsx # 分钟数据统计(~80 行,4 个 Stat 卡片)
|
||||||
|
├── TradeAnalysis.tsx # 成交分析(~65 行,活跃时段/平均价格等)
|
||||||
|
└── EmptyState.tsx # 空状态组件(~35 行,可复用)
|
||||||
|
```
|
||||||
|
|
||||||
|
**组件依赖关系**:
|
||||||
|
```
|
||||||
|
index.tsx
|
||||||
|
├── KLineChart # 日K线图(ECharts)
|
||||||
|
├── MinuteKLineSection # 分钟K线区域
|
||||||
|
│ ├── MinuteStats (atom) # 开盘/当前/最高/最低价统计
|
||||||
|
│ ├── TradeAnalysis (atom) # 成交数据分析
|
||||||
|
│ └── EmptyState (atom) # 空状态提示
|
||||||
|
└── TradeTable # 交易明细表格(最近 10 天)
|
||||||
|
```
|
||||||
|
|
||||||
|
**组件职责**:
|
||||||
|
| 组件 | 行数 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `index.tsx` | ~50 | 主入口,组合 3 个子组件 |
|
||||||
|
| `KLineChart` | ~40 | 日K线图渲染,支持图表点击事件 |
|
||||||
|
| `MinuteKLineSection` | ~95 | 分钟K线区域,含加载状态、空状态、统计数据 |
|
||||||
|
| `TradeTable` | ~75 | 最近 10 天交易明细表格 |
|
||||||
|
| `MinuteStats` | ~80 | 分钟数据四宫格统计(开盘/当前/最高/最低价) |
|
||||||
|
| `TradeAnalysis` | ~65 | 成交数据分析(活跃时段、平均价格、数据点数) |
|
||||||
|
| `EmptyState` | ~35 | 通用空状态组件(可配置标题和描述) |
|
||||||
|
|
||||||
|
**优化效果**:
|
||||||
|
| 指标 | 优化前 | 优化后 | 改善 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| 主文件行数 | 382 | ~50 | -87% |
|
||||||
|
| 文件数量 | 1 | 8 | 原子设计模式 |
|
||||||
|
| 可复用组件 | 0 | 3 原子 + 3 业务 | 提升 |
|
||||||
|
|
||||||
|
**设计原则**:
|
||||||
|
- **原子设计模式**:atoms(MinuteStats、TradeAnalysis、EmptyState)→ 业务组件(KLineChart、MinuteKLineSection、TradeTable)→ 主组件
|
||||||
|
- **职责分离**:图表、统计、表格各自独立
|
||||||
|
- **组件复用**:EmptyState 可在其他场景复用
|
||||||
|
- **类型安全**:完整的 Props 类型定义和导出
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel.tsx
|
|
||||||
// 交易数据面板 - K线图、分钟图、交易明细表格
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Text,
|
|
||||||
Table,
|
|
||||||
Thead,
|
|
||||||
Tbody,
|
|
||||||
Tr,
|
|
||||||
Th,
|
|
||||||
Td,
|
|
||||||
TableContainer,
|
|
||||||
Stat,
|
|
||||||
StatLabel,
|
|
||||||
StatNumber,
|
|
||||||
StatHelpText,
|
|
||||||
StatArrow,
|
|
||||||
SimpleGrid,
|
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
|
||||||
Spinner,
|
|
||||||
Center,
|
|
||||||
Badge,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Icon,
|
|
||||||
Heading,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronUpIcon,
|
|
||||||
InfoIcon,
|
|
||||||
RepeatIcon,
|
|
||||||
TimeIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
ArrowDownIcon,
|
|
||||||
} from '@chakra-ui/icons';
|
|
||||||
import ReactECharts from 'echarts-for-react';
|
|
||||||
|
|
||||||
import ThemedCard from '../ThemedCard';
|
|
||||||
import { formatNumber, formatPercent } from '../../utils/formatUtils';
|
|
||||||
import { getKLineOption, getMinuteKLineOption } from '../../utils/chartOptions';
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TradeDataPanel: React.FC<TradeDataPanelProps> = ({
|
|
||||||
theme,
|
|
||||||
tradeData,
|
|
||||||
minuteData,
|
|
||||||
minuteLoading,
|
|
||||||
analysisMap,
|
|
||||||
onLoadMinuteData,
|
|
||||||
onChartClick,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<VStack spacing={6} align="stretch">
|
|
||||||
{/* K线图 */}
|
|
||||||
<ThemedCard theme={theme}>
|
|
||||||
<CardBody>
|
|
||||||
{tradeData.length > 0 && (
|
|
||||||
<Box h="600px">
|
|
||||||
<ReactECharts
|
|
||||||
option={getKLineOption(theme, tradeData, analysisMap)}
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
theme="light"
|
|
||||||
onEvents={{ click: onChartClick }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
|
||||||
</ThemedCard>
|
|
||||||
|
|
||||||
{/* 分钟K线数据 */}
|
|
||||||
<ThemedCard theme={theme}>
|
|
||||||
<CardHeader>
|
|
||||||
<HStack justify="space-between" align="center">
|
|
||||||
<HStack spacing={3}>
|
|
||||||
<Icon as={TimeIcon} color={theme.primary} boxSize={5} />
|
|
||||||
<Heading size="md" color={theme.textSecondary}>
|
|
||||||
当日分钟频数据
|
|
||||||
</Heading>
|
|
||||||
{minuteData && minuteData.trade_date && (
|
|
||||||
<Badge colorScheme="blue" fontSize="xs">
|
|
||||||
{minuteData.trade_date}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<Button
|
|
||||||
leftIcon={<RepeatIcon />}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
colorScheme="blue"
|
|
||||||
onClick={onLoadMinuteData}
|
|
||||||
isLoading={minuteLoading}
|
|
||||||
loadingText="获取中"
|
|
||||||
>
|
|
||||||
获取分钟数据
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
{minuteLoading ? (
|
|
||||||
<Center h="400px">
|
|
||||||
<VStack spacing={4}>
|
|
||||||
<Spinner
|
|
||||||
thickness="4px"
|
|
||||||
speed="0.65s"
|
|
||||||
emptyColor={theme.bgDark}
|
|
||||||
color={theme.primary}
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
<Text color={theme.textMuted} fontSize="sm">
|
|
||||||
加载分钟频数据中...
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
) : minuteData && minuteData.data && minuteData.data.length > 0 ? (
|
|
||||||
<VStack spacing={6} align="stretch">
|
|
||||||
<Box h="500px">
|
|
||||||
<ReactECharts
|
|
||||||
option={getMinuteKLineOption(theme, minuteData)}
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
theme="light"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 分钟数据统计 */}
|
|
||||||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
|
|
||||||
<Stat>
|
|
||||||
<StatLabel color={theme.textMuted} fontSize="sm">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={ArrowUpIcon} boxSize={3} />
|
|
||||||
<Text>开盘价</Text>
|
|
||||||
</HStack>
|
|
||||||
</StatLabel>
|
|
||||||
<StatNumber color={theme.textPrimary} fontSize="lg">
|
|
||||||
{minuteData.data[0]?.open?.toFixed(2) || '-'}
|
|
||||||
</StatNumber>
|
|
||||||
</Stat>
|
|
||||||
<Stat>
|
|
||||||
<StatLabel color={theme.textMuted} fontSize="sm">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={ArrowDownIcon} boxSize={3} />
|
|
||||||
<Text>当前价</Text>
|
|
||||||
</HStack>
|
|
||||||
</StatLabel>
|
|
||||||
<StatNumber
|
|
||||||
color={
|
|
||||||
(minuteData.data[minuteData.data.length - 1]?.close || 0) >=
|
|
||||||
(minuteData.data[0]?.open || 0)
|
|
||||||
? theme.success
|
|
||||||
: theme.danger
|
|
||||||
}
|
|
||||||
fontSize="lg"
|
|
||||||
>
|
|
||||||
{minuteData.data[minuteData.data.length - 1]?.close?.toFixed(2) || '-'}
|
|
||||||
</StatNumber>
|
|
||||||
<StatHelpText fontSize="xs">
|
|
||||||
<StatArrow
|
|
||||||
type={
|
|
||||||
(minuteData.data[minuteData.data.length - 1]?.close || 0) >=
|
|
||||||
(minuteData.data[0]?.open || 0)
|
|
||||||
? 'increase'
|
|
||||||
: 'decrease'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{(() => {
|
|
||||||
const lastClose = minuteData.data[minuteData.data.length - 1]?.close;
|
|
||||||
const firstOpen = minuteData.data[0]?.open;
|
|
||||||
if (lastClose && firstOpen) {
|
|
||||||
return Math.abs(((lastClose - firstOpen) / firstOpen) * 100).toFixed(2);
|
|
||||||
}
|
|
||||||
return '0.00';
|
|
||||||
})()}
|
|
||||||
%
|
|
||||||
</StatHelpText>
|
|
||||||
</Stat>
|
|
||||||
<Stat>
|
|
||||||
<StatLabel color={theme.textMuted} fontSize="sm">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={ChevronUpIcon} boxSize={3} />
|
|
||||||
<Text>最高价</Text>
|
|
||||||
</HStack>
|
|
||||||
</StatLabel>
|
|
||||||
<StatNumber color={theme.success} fontSize="lg">
|
|
||||||
{Math.max(...minuteData.data.map((item) => item.high).filter(Boolean)).toFixed(
|
|
||||||
2
|
|
||||||
)}
|
|
||||||
</StatNumber>
|
|
||||||
</Stat>
|
|
||||||
<Stat>
|
|
||||||
<StatLabel color={theme.textMuted} fontSize="sm">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={ChevronDownIcon} boxSize={3} />
|
|
||||||
<Text>最低价</Text>
|
|
||||||
</HStack>
|
|
||||||
</StatLabel>
|
|
||||||
<StatNumber color={theme.danger} fontSize="lg">
|
|
||||||
{Math.min(...minuteData.data.map((item) => item.low).filter(Boolean)).toFixed(2)}
|
|
||||||
</StatNumber>
|
|
||||||
</Stat>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
{/* 成交数据分析 */}
|
|
||||||
<Box
|
|
||||||
p={4}
|
|
||||||
bg={theme.bgDark}
|
|
||||||
borderRadius="lg"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor={theme.border}
|
|
||||||
>
|
|
||||||
<HStack justify="space-between" mb={4}>
|
|
||||||
<Text fontWeight="bold" color={theme.textSecondary}>
|
|
||||||
成交数据分析
|
|
||||||
</Text>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Badge colorScheme="purple" fontSize="xs">
|
|
||||||
总成交量:{' '}
|
|
||||||
{formatNumber(
|
|
||||||
minuteData.data.reduce((sum, item) => sum + item.volume, 0),
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
<Badge colorScheme="orange" fontSize="xs">
|
|
||||||
总成交额:{' '}
|
|
||||||
{formatNumber(minuteData.data.reduce((sum, item) => sum + item.amount, 0))}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<Grid templateColumns="repeat(3, 1fr)" gap={4}>
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="sm" color={theme.textMuted} mb={2}>
|
|
||||||
活跃时段
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" color={theme.textPrimary}>
|
|
||||||
{(() => {
|
|
||||||
const maxVolume = Math.max(...minuteData.data.map((item) => item.volume));
|
|
||||||
const activeTime = minuteData.data.find(
|
|
||||||
(item) => item.volume === maxVolume
|
|
||||||
);
|
|
||||||
return activeTime
|
|
||||||
? `${activeTime.time} (${formatNumber(maxVolume, 0)})`
|
|
||||||
: '-';
|
|
||||||
})()}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="sm" color={theme.textMuted} mb={2}>
|
|
||||||
平均价格
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" color={theme.textPrimary}>
|
|
||||||
{(
|
|
||||||
minuteData.data.reduce((sum, item) => sum + item.close, 0) /
|
|
||||||
minuteData.data.length
|
|
||||||
).toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="sm" color={theme.textMuted} mb={2}>
|
|
||||||
数据点数
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" color={theme.textPrimary}>
|
|
||||||
{minuteData.data.length} 个分钟
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</VStack>
|
|
||||||
) : (
|
|
||||||
<Center h="300px">
|
|
||||||
<VStack spacing={4}>
|
|
||||||
<Icon as={InfoIcon} color={theme.textMuted} boxSize={12} />
|
|
||||||
<VStack spacing={2}>
|
|
||||||
<Text color={theme.textMuted} fontSize="lg">
|
|
||||||
暂无分钟频数据
|
|
||||||
</Text>
|
|
||||||
<Text color={theme.textMuted} fontSize="sm" textAlign="center">
|
|
||||||
点击"获取分钟数据"按钮加载最新的交易日分钟频数据
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
|
||||||
</ThemedCard>
|
|
||||||
|
|
||||||
{/* 交易明细表格 */}
|
|
||||||
<ThemedCard theme={theme}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md" color={theme.textSecondary}>
|
|
||||||
交易明细
|
|
||||||
</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<TableContainer>
|
|
||||||
<Table variant="simple" size="sm">
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th color={theme.textSecondary}>日期</Th>
|
|
||||||
<Th isNumeric color={theme.textSecondary}>
|
|
||||||
开盘
|
|
||||||
</Th>
|
|
||||||
<Th isNumeric color={theme.textSecondary}>
|
|
||||||
最高
|
|
||||||
</Th>
|
|
||||||
<Th isNumeric color={theme.textSecondary}>
|
|
||||||
最低
|
|
||||||
</Th>
|
|
||||||
<Th isNumeric color={theme.textSecondary}>
|
|
||||||
收盘
|
|
||||||
</Th>
|
|
||||||
<Th isNumeric color={theme.textSecondary}>
|
|
||||||
涨跌幅
|
|
||||||
</Th>
|
|
||||||
<Th isNumeric color={theme.textSecondary}>
|
|
||||||
成交量
|
|
||||||
</Th>
|
|
||||||
<Th isNumeric color={theme.textSecondary}>
|
|
||||||
成交额
|
|
||||||
</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{tradeData
|
|
||||||
.slice(-10)
|
|
||||||
.reverse()
|
|
||||||
.map((item, idx) => (
|
|
||||||
<Tr key={idx} _hover={{ bg: theme.bgDark }}>
|
|
||||||
<Td color={theme.textPrimary}>{item.date}</Td>
|
|
||||||
<Td isNumeric color={theme.textPrimary}>
|
|
||||||
{item.open}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color={theme.textPrimary}>
|
|
||||||
{item.high}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color={theme.textPrimary}>
|
|
||||||
{item.low}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color={theme.textPrimary} fontWeight="bold">
|
|
||||||
{item.close}
|
|
||||||
</Td>
|
|
||||||
<Td
|
|
||||||
isNumeric
|
|
||||||
color={item.change_percent >= 0 ? theme.success : theme.danger}
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{item.change_percent >= 0 ? '+' : ''}
|
|
||||||
{formatPercent(item.change_percent)}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color={theme.textPrimary}>
|
|
||||||
{formatNumber(item.volume, 0)}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color={theme.textPrimary}>
|
|
||||||
{formatNumber(item.amount)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</CardBody>
|
|
||||||
</ThemedCard>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TradeDataPanel;
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineChart.tsx
|
||||||
|
// 日K线图组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, CardBody } from '@chakra-ui/react';
|
||||||
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
|
||||||
|
import ThemedCard from '../../ThemedCard';
|
||||||
|
import { getKLineOption } from '../../../utils/chartOptions';
|
||||||
|
import type { Theme, TradeDayData, RiseAnalysis } from '../../../types';
|
||||||
|
|
||||||
|
export interface KLineChartProps {
|
||||||
|
theme: Theme;
|
||||||
|
tradeData: TradeDayData[];
|
||||||
|
analysisMap: Record<number, RiseAnalysis>;
|
||||||
|
onChartClick: (params: { seriesName?: string; data?: [number, number] }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KLineChart: React.FC<KLineChartProps> = ({
|
||||||
|
theme,
|
||||||
|
tradeData,
|
||||||
|
analysisMap,
|
||||||
|
onChartClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ThemedCard theme={theme}>
|
||||||
|
<CardBody>
|
||||||
|
{tradeData.length > 0 && (
|
||||||
|
<Box h="600px">
|
||||||
|
<ReactECharts
|
||||||
|
option={getKLineOption(theme, tradeData, analysisMap)}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
theme="light"
|
||||||
|
onEvents={{ click: onChartClick }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</ThemedCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KLineChart;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/MinuteKLineSection.tsx
|
||||||
|
// 分钟K线数据区域组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
Center,
|
||||||
|
Spinner,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
Icon,
|
||||||
|
Heading,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { TimeIcon, RepeatIcon } from '@chakra-ui/icons';
|
||||||
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
|
||||||
|
import ThemedCard from '../../ThemedCard';
|
||||||
|
import { getMinuteKLineOption } from '../../../utils/chartOptions';
|
||||||
|
import { MinuteStats, TradeAnalysis, EmptyState } from './atoms';
|
||||||
|
import type { Theme, MinuteData } from '../../../types';
|
||||||
|
|
||||||
|
export interface MinuteKLineSectionProps {
|
||||||
|
theme: Theme;
|
||||||
|
minuteData: MinuteData | null;
|
||||||
|
loading: boolean;
|
||||||
|
onLoadMinuteData: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MinuteKLineSection: React.FC<MinuteKLineSectionProps> = ({
|
||||||
|
theme,
|
||||||
|
minuteData,
|
||||||
|
loading,
|
||||||
|
onLoadMinuteData,
|
||||||
|
}) => {
|
||||||
|
const hasData = minuteData && minuteData.data && minuteData.data.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemedCard theme={theme}>
|
||||||
|
<CardHeader>
|
||||||
|
<HStack justify="space-between" align="center">
|
||||||
|
<HStack spacing={3}>
|
||||||
|
<Icon as={TimeIcon} color={theme.primary} boxSize={5} />
|
||||||
|
<Heading size="md" color={theme.textSecondary}>
|
||||||
|
当日分钟频数据
|
||||||
|
</Heading>
|
||||||
|
{minuteData?.trade_date && (
|
||||||
|
<Badge colorScheme="blue" fontSize="xs">
|
||||||
|
{minuteData.trade_date}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<Button
|
||||||
|
leftIcon={<RepeatIcon />}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={onLoadMinuteData}
|
||||||
|
isLoading={loading}
|
||||||
|
loadingText="获取中"
|
||||||
|
>
|
||||||
|
获取分钟数据
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardBody>
|
||||||
|
{loading ? (
|
||||||
|
<Center h="400px">
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Spinner
|
||||||
|
thickness="4px"
|
||||||
|
speed="0.65s"
|
||||||
|
emptyColor={theme.bgDark}
|
||||||
|
color={theme.primary}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<Text color={theme.textMuted} fontSize="sm">
|
||||||
|
加载分钟频数据中...
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
) : hasData ? (
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
<Box h="500px">
|
||||||
|
<ReactECharts
|
||||||
|
option={getMinuteKLineOption(theme, minuteData)}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
theme="light"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<MinuteStats theme={theme} data={minuteData.data} />
|
||||||
|
<TradeAnalysis theme={theme} data={minuteData.data} />
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<EmptyState theme={theme} />
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</ThemedCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MinuteKLineSection;
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/TradeTable.tsx
|
||||||
|
// 交易明细表格组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
TableContainer,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
Heading,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import ThemedCard from '../../ThemedCard';
|
||||||
|
import { formatNumber, formatPercent } from '../../../utils/formatUtils';
|
||||||
|
import type { Theme, TradeDayData } from '../../../types';
|
||||||
|
|
||||||
|
export interface TradeTableProps {
|
||||||
|
theme: Theme;
|
||||||
|
tradeData: TradeDayData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TradeTable: React.FC<TradeTableProps> = ({ theme, tradeData }) => {
|
||||||
|
return (
|
||||||
|
<ThemedCard theme={theme}>
|
||||||
|
<CardHeader>
|
||||||
|
<Heading size="md" color={theme.textSecondary}>
|
||||||
|
交易明细
|
||||||
|
</Heading>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<TableContainer>
|
||||||
|
<Table variant="simple" size="sm">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th color={theme.textSecondary}>日期</Th>
|
||||||
|
<Th isNumeric color={theme.textSecondary}>开盘</Th>
|
||||||
|
<Th isNumeric color={theme.textSecondary}>最高</Th>
|
||||||
|
<Th isNumeric color={theme.textSecondary}>最低</Th>
|
||||||
|
<Th isNumeric color={theme.textSecondary}>收盘</Th>
|
||||||
|
<Th isNumeric color={theme.textSecondary}>涨跌幅</Th>
|
||||||
|
<Th isNumeric color={theme.textSecondary}>成交量</Th>
|
||||||
|
<Th isNumeric color={theme.textSecondary}>成交额</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{tradeData
|
||||||
|
.slice(-10)
|
||||||
|
.reverse()
|
||||||
|
.map((item, idx) => (
|
||||||
|
<Tr key={idx} _hover={{ bg: theme.bgDark }}>
|
||||||
|
<Td color={theme.textPrimary}>{item.date}</Td>
|
||||||
|
<Td isNumeric color={theme.textPrimary}>{item.open}</Td>
|
||||||
|
<Td isNumeric color={theme.textPrimary}>{item.high}</Td>
|
||||||
|
<Td isNumeric color={theme.textPrimary}>{item.low}</Td>
|
||||||
|
<Td isNumeric color={theme.textPrimary} fontWeight="bold">
|
||||||
|
{item.close}
|
||||||
|
</Td>
|
||||||
|
<Td
|
||||||
|
isNumeric
|
||||||
|
color={item.change_percent >= 0 ? theme.success : theme.danger}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{item.change_percent >= 0 ? '+' : ''}
|
||||||
|
{formatPercent(item.change_percent)}
|
||||||
|
</Td>
|
||||||
|
<Td isNumeric color={theme.textPrimary}>
|
||||||
|
{formatNumber(item.volume, 0)}
|
||||||
|
</Td>
|
||||||
|
<Td isNumeric color={theme.textPrimary}>
|
||||||
|
{formatNumber(item.amount)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</CardBody>
|
||||||
|
</ThemedCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TradeTable;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/EmptyState.tsx
|
||||||
|
// 空状态组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Center, VStack, Text, Icon } from '@chakra-ui/react';
|
||||||
|
import { InfoIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
|
import type { Theme } from '../../../../types';
|
||||||
|
|
||||||
|
export interface EmptyStateProps {
|
||||||
|
theme: Theme;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
height?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
|
theme,
|
||||||
|
title = '暂无分钟频数据',
|
||||||
|
description = '点击"获取分钟数据"按钮加载最新的交易日分钟频数据',
|
||||||
|
height = '300px',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Center h={height}>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Icon as={InfoIcon} color={theme.textMuted} boxSize={12} />
|
||||||
|
<VStack spacing={2}>
|
||||||
|
<Text color={theme.textMuted} fontSize="lg">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text color={theme.textMuted} fontSize="sm" textAlign="center">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyState;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/MinuteStats.tsx
|
||||||
|
// 分钟数据统计组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
SimpleGrid,
|
||||||
|
Stat,
|
||||||
|
StatLabel,
|
||||||
|
StatNumber,
|
||||||
|
StatHelpText,
|
||||||
|
StatArrow,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
ArrowDownIcon,
|
||||||
|
} from '@chakra-ui/icons';
|
||||||
|
|
||||||
|
import type { Theme, MinuteDataPoint } from '../../../../types';
|
||||||
|
|
||||||
|
export interface MinuteStatsProps {
|
||||||
|
theme: Theme;
|
||||||
|
data: MinuteDataPoint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MinuteStats: React.FC<MinuteStatsProps> = ({ theme, data }) => {
|
||||||
|
if (data.length === 0) return null;
|
||||||
|
|
||||||
|
const firstOpen = data[0]?.open || 0;
|
||||||
|
const lastClose = data[data.length - 1]?.close || 0;
|
||||||
|
const highPrice = Math.max(...data.map((item) => item.high).filter(Boolean));
|
||||||
|
const lowPrice = Math.min(...data.map((item) => item.low).filter(Boolean));
|
||||||
|
const isUp = lastClose >= firstOpen;
|
||||||
|
const changePercent = firstOpen ? Math.abs(((lastClose - firstOpen) / firstOpen) * 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
|
||||||
|
<Stat>
|
||||||
|
<StatLabel color={theme.textMuted} fontSize="sm">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={ArrowUpIcon} boxSize={3} />
|
||||||
|
<Text>开盘价</Text>
|
||||||
|
</HStack>
|
||||||
|
</StatLabel>
|
||||||
|
<StatNumber color={theme.textPrimary} fontSize="lg">
|
||||||
|
{firstOpen?.toFixed(2) || '-'}
|
||||||
|
</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
|
||||||
|
<Stat>
|
||||||
|
<StatLabel color={theme.textMuted} fontSize="sm">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={ArrowDownIcon} boxSize={3} />
|
||||||
|
<Text>当前价</Text>
|
||||||
|
</HStack>
|
||||||
|
</StatLabel>
|
||||||
|
<StatNumber color={isUp ? theme.success : theme.danger} fontSize="lg">
|
||||||
|
{lastClose?.toFixed(2) || '-'}
|
||||||
|
</StatNumber>
|
||||||
|
<StatHelpText fontSize="xs">
|
||||||
|
<StatArrow type={isUp ? 'increase' : 'decrease'} />
|
||||||
|
{changePercent.toFixed(2)}%
|
||||||
|
</StatHelpText>
|
||||||
|
</Stat>
|
||||||
|
|
||||||
|
<Stat>
|
||||||
|
<StatLabel color={theme.textMuted} fontSize="sm">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={ChevronUpIcon} boxSize={3} />
|
||||||
|
<Text>最高价</Text>
|
||||||
|
</HStack>
|
||||||
|
</StatLabel>
|
||||||
|
<StatNumber color={theme.success} fontSize="lg">
|
||||||
|
{highPrice.toFixed(2)}
|
||||||
|
</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
|
||||||
|
<Stat>
|
||||||
|
<StatLabel color={theme.textMuted} fontSize="sm">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={ChevronDownIcon} boxSize={3} />
|
||||||
|
<Text>最低价</Text>
|
||||||
|
</HStack>
|
||||||
|
</StatLabel>
|
||||||
|
<StatNumber color={theme.danger} fontSize="lg">
|
||||||
|
{lowPrice.toFixed(2)}
|
||||||
|
</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MinuteStats;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/TradeAnalysis.tsx
|
||||||
|
// 成交数据分析组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text, HStack, Badge, Grid } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { formatNumber } from '../../../../utils/formatUtils';
|
||||||
|
import type { Theme, MinuteDataPoint } from '../../../../types';
|
||||||
|
|
||||||
|
export interface TradeAnalysisProps {
|
||||||
|
theme: Theme;
|
||||||
|
data: MinuteDataPoint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TradeAnalysis: React.FC<TradeAnalysisProps> = ({ theme, data }) => {
|
||||||
|
if (data.length === 0) return null;
|
||||||
|
|
||||||
|
const totalVolume = data.reduce((sum, item) => sum + item.volume, 0);
|
||||||
|
const totalAmount = data.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
const avgPrice = data.reduce((sum, item) => sum + item.close, 0) / data.length;
|
||||||
|
const maxVolume = Math.max(...data.map((item) => item.volume));
|
||||||
|
const activeTime = data.find((item) => item.volume === maxVolume);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg={theme.bgDark}
|
||||||
|
borderRadius="lg"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={theme.border}
|
||||||
|
>
|
||||||
|
<HStack justify="space-between" mb={4}>
|
||||||
|
<Text fontWeight="bold" color={theme.textSecondary}>
|
||||||
|
成交数据分析
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Badge colorScheme="purple" fontSize="xs">
|
||||||
|
总成交量: {formatNumber(totalVolume, 0)}
|
||||||
|
</Badge>
|
||||||
|
<Badge colorScheme="orange" fontSize="xs">
|
||||||
|
总成交额: {formatNumber(totalAmount)}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Grid templateColumns="repeat(3, 1fr)" gap={4}>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color={theme.textMuted} mb={2}>
|
||||||
|
活跃时段
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={theme.textPrimary}>
|
||||||
|
{activeTime ? `${activeTime.time} (${formatNumber(maxVolume, 0)})` : '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color={theme.textMuted} mb={2}>
|
||||||
|
平均价格
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={theme.textPrimary}>
|
||||||
|
{avgPrice.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color={theme.textMuted} mb={2}>
|
||||||
|
数据点数
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={theme.textPrimary}>
|
||||||
|
{data.length} 个分钟
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TradeAnalysis;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/index.ts
|
||||||
|
// 原子组件统一导出
|
||||||
|
|
||||||
|
export { default as MinuteStats } from './MinuteStats';
|
||||||
|
export { default as TradeAnalysis } from './TradeAnalysis';
|
||||||
|
export { default as EmptyState } from './EmptyState';
|
||||||
|
|
||||||
|
export type { MinuteStatsProps } from './MinuteStats';
|
||||||
|
export type { TradeAnalysisProps } from './TradeAnalysis';
|
||||||
|
export type { EmptyStateProps } from './EmptyState';
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx
|
||||||
|
// 交易数据面板 - K线图、分钟图、交易明细表格
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { VStack } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import KLineChart from './KLineChart';
|
||||||
|
import MinuteKLineSection from './MinuteKLineSection';
|
||||||
|
import TradeTable from './TradeTable';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TradeDataPanel: React.FC<TradeDataPanelProps> = ({
|
||||||
|
theme,
|
||||||
|
tradeData,
|
||||||
|
minuteData,
|
||||||
|
minuteLoading,
|
||||||
|
analysisMap,
|
||||||
|
onLoadMinuteData,
|
||||||
|
onChartClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
<KLineChart
|
||||||
|
theme={theme}
|
||||||
|
tradeData={tradeData}
|
||||||
|
analysisMap={analysisMap}
|
||||||
|
onChartClick={onChartClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MinuteKLineSection
|
||||||
|
theme={theme}
|
||||||
|
minuteData={minuteData}
|
||||||
|
loading={minuteLoading}
|
||||||
|
onLoadMinuteData={onLoadMinuteData}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TradeTable theme={theme} tradeData={tradeData} />
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TradeDataPanel;
|
||||||
|
|
||||||
|
// 导出子组件供外部按需使用
|
||||||
|
export { KLineChart, MinuteKLineSection, TradeTable };
|
||||||
|
export type { KLineChartProps } from './KLineChart';
|
||||||
|
export type { MinuteKLineSectionProps } from './MinuteKLineSection';
|
||||||
|
export type { TradeTableProps } from './TradeTable';
|
||||||
@@ -13,3 +13,15 @@ export type { FundingPanelProps } from './FundingPanel';
|
|||||||
export type { BigDealPanelProps } from './BigDealPanel';
|
export type { BigDealPanelProps } from './BigDealPanel';
|
||||||
export type { UnusualPanelProps } from './UnusualPanel';
|
export type { UnusualPanelProps } from './UnusualPanel';
|
||||||
export type { PledgePanelProps } from './PledgePanel';
|
export type { PledgePanelProps } from './PledgePanel';
|
||||||
|
|
||||||
|
// 导出 TradeDataPanel 子组件
|
||||||
|
export {
|
||||||
|
KLineChart,
|
||||||
|
MinuteKLineSection,
|
||||||
|
TradeTable,
|
||||||
|
} from './TradeDataPanel';
|
||||||
|
export type {
|
||||||
|
KLineChartProps,
|
||||||
|
MinuteKLineSectionProps,
|
||||||
|
TradeTableProps,
|
||||||
|
} from './TradeDataPanel';
|
||||||
|
|||||||
Reference in New Issue
Block a user