Compare commits

..

3 Commits

Author SHA1 Message Date
zdl
276b280cb9 docs: 更新 STRUCTURE.md 和 mock 数据
- STRUCTURE.md 添加 MarketDataView Panel 拆分记录
- 更新目录结构说明,包含 panels/ 子目录
- 更新 company.js 和 market.js mock 数据

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 18:11:03 +08:00
zdl
adfc0bd478 refactor(MarketDataView): 使用 Panel 组件重构主组件
- 主组件从 1049 行精简至 285 行(减少 73%)
- 添加 panels/index.ts 统一导出
- Tab 容器和状态管理保留在主组件
- 各面板内容拆分到独立组件

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 18:08:40 +08:00
zdl
85a857dc19 feat(MarketDataView): 新增 5 个 Panel 组件
- TradeDataPanel: 交易数据面板(K线图、分钟图、表格)
- FundingPanel: 融资融券面板
- BigDealPanel: 大宗交易面板
- UnusualPanel: 龙虎榜面板
- PledgePanel: 股权质押面板

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 18:07:58 +08:00
10 changed files with 1167 additions and 831 deletions

View File

@@ -42,6 +42,72 @@ export const PINGAN_BANK_DATA = {
employees: 42099, employees: 42099,
}, },
// 市场概览数据 - StockSummaryCard 使用
marketSummary: {
stock_code: '000001',
stock_name: '平安银行',
latest_trade: {
close: 11.28,
change_percent: 2.35,
volume: 58623400,
amount: 659800000,
turnover_rate: 0.30,
pe_ratio: 4.92
},
latest_funding: {
financing_balance: 5823000000,
securities_balance: 125600000
},
latest_pledge: {
pledge_ratio: 8.25
}
},
// 当日分钟K线数据 - MinuteKLineChart 使用
minuteData: {
code: '000001',
name: '平安银行',
trade_date: '2024-12-12',
type: '1min',
data: [
// 上午交易时段 9:30 - 11:30
{ time: '09:30', open: 11.02, close: 11.05, high: 11.06, low: 11.01, volume: 1856000, amount: 20458000 },
{ time: '09:31', open: 11.05, close: 11.08, high: 11.09, low: 11.04, volume: 1423000, amount: 15782000 },
{ time: '09:32', open: 11.08, close: 11.06, high: 11.10, low: 11.05, volume: 1125000, amount: 12468000 },
{ time: '09:33', open: 11.06, close: 11.10, high: 11.11, low: 11.05, volume: 1678000, amount: 18623000 },
{ time: '09:34', open: 11.10, close: 11.12, high: 11.14, low: 11.09, volume: 2134000, amount: 23725000 },
{ time: '09:35', open: 11.12, close: 11.15, high: 11.16, low: 11.11, volume: 1892000, amount: 21082000 },
{ time: '09:40', open: 11.15, close: 11.18, high: 11.20, low: 11.14, volume: 1567000, amount: 17523000 },
{ time: '09:45', open: 11.18, close: 11.16, high: 11.19, low: 11.15, volume: 1234000, amount: 13782000 },
{ time: '09:50', open: 11.16, close: 11.20, high: 11.21, low: 11.15, volume: 1456000, amount: 16298000 },
{ time: '09:55', open: 11.20, close: 11.22, high: 11.24, low: 11.19, volume: 1789000, amount: 20068000 },
{ time: '10:00', open: 11.22, close: 11.25, high: 11.26, low: 11.21, volume: 2012000, amount: 22635000 },
{ time: '10:10', open: 11.25, close: 11.23, high: 11.26, low: 11.22, volume: 1345000, amount: 15123000 },
{ time: '10:20', open: 11.23, close: 11.26, high: 11.28, low: 11.22, volume: 1678000, amount: 18912000 },
{ time: '10:30', open: 11.26, close: 11.24, high: 11.27, low: 11.23, volume: 1123000, amount: 12645000 },
{ time: '10:40', open: 11.24, close: 11.27, high: 11.28, low: 11.23, volume: 1456000, amount: 16412000 },
{ time: '10:50', open: 11.27, close: 11.25, high: 11.28, low: 11.24, volume: 1234000, amount: 13902000 },
{ time: '11:00', open: 11.25, close: 11.28, high: 11.30, low: 11.24, volume: 1567000, amount: 17689000 },
{ time: '11:10', open: 11.28, close: 11.26, high: 11.29, low: 11.25, volume: 1089000, amount: 12278000 },
{ time: '11:20', open: 11.26, close: 11.28, high: 11.29, low: 11.25, volume: 1234000, amount: 13912000 },
{ time: '11:30', open: 11.28, close: 11.27, high: 11.29, low: 11.26, volume: 987000, amount: 11134000 },
// 下午交易时段 13:00 - 15:00
{ time: '13:00', open: 11.27, close: 11.30, high: 11.31, low: 11.26, volume: 1456000, amount: 16456000 },
{ time: '13:10', open: 11.30, close: 11.28, high: 11.31, low: 11.27, volume: 1123000, amount: 12689000 },
{ time: '13:20', open: 11.28, close: 11.32, high: 11.33, low: 11.27, volume: 1789000, amount: 20245000 },
{ time: '13:30', open: 11.32, close: 11.30, high: 11.33, low: 11.29, volume: 1345000, amount: 15212000 },
{ time: '13:40', open: 11.30, close: 11.33, high: 11.35, low: 11.29, volume: 1678000, amount: 18978000 },
{ time: '13:50', open: 11.33, close: 11.31, high: 11.34, low: 11.30, volume: 1234000, amount: 13956000 },
{ time: '14:00', open: 11.31, close: 11.34, high: 11.36, low: 11.30, volume: 1567000, amount: 17789000 },
{ time: '14:10', open: 11.34, close: 11.32, high: 11.35, low: 11.31, volume: 1123000, amount: 12712000 },
{ time: '14:20', open: 11.32, close: 11.30, high: 11.33, low: 11.29, volume: 1456000, amount: 16478000 },
{ time: '14:30', open: 11.30, close: 11.28, high: 11.31, low: 11.27, volume: 1678000, amount: 18956000 },
{ time: '14:40', open: 11.28, close: 11.26, high: 11.29, low: 11.25, volume: 1345000, amount: 15167000 },
{ time: '14:50', open: 11.26, close: 11.28, high: 11.30, low: 11.25, volume: 1892000, amount: 21345000 },
{ time: '15:00', open: 11.28, close: 11.28, high: 11.29, low: 11.27, volume: 2345000, amount: 26478000 }
]
},
// 实际控制人信息(数组格式) // 实际控制人信息(数组格式)
actualControl: [ actualControl: [
{ {

View File

@@ -24,8 +24,9 @@ export const generateMarketData = (stockCode) => {
low: parseFloat(low.toFixed(2)), low: parseFloat(low.toFixed(2)),
volume: Math.floor(Math.random() * 500000000) + 100000000, // 1-6亿股 volume: Math.floor(Math.random() * 500000000) + 100000000, // 1-6亿股
amount: Math.floor(Math.random() * 7000000000) + 1300000000, // 13-80亿元 amount: Math.floor(Math.random() * 7000000000) + 1300000000, // 13-80亿元
turnover_rate: (Math.random() * 2 + 0.5).toFixed(2), // 0.5-2.5% turnover_rate: parseFloat((Math.random() * 2 + 0.5).toFixed(2)), // 0.5-2.5%
change_pct: (Math.random() * 6 - 3).toFixed(2) // -3% to +3% change_percent: parseFloat((Math.random() * 6 - 3).toFixed(2)), // -3% to +3%
pe_ratio: parseFloat((Math.random() * 3 + 4).toFixed(2)) // 4-7
}; };
}) })
}, },
@@ -78,36 +79,45 @@ export const generateMarketData = (stockCode) => {
})) }))
}, },
// 股权质押 // 股权质押 - 匹配 PledgeData[] 类型
pledgeData: { pledgeData: {
success: true, success: true,
data: { data: Array(12).fill(null).map((_, i) => {
total_pledged: 25.6, // 质押比例% const date = new Date();
major_shareholders: [ date.setMonth(date.getMonth() - (11 - i));
{ name: '中国平安保险集团', pledged_shares: 0, total_shares: 10168542300, pledge_ratio: 0 }, return {
{ name: '深圳市投资控股', pledged_shares: 50000000, total_shares: 382456100, pledge_ratio: 13.08 } end_date: date.toISOString().split('T')[0].slice(0, 7) + '-01',
], unrestricted_pledge: Math.floor(Math.random() * 1000000000) + 500000000,
update_date: '2024-09-30' restricted_pledge: Math.floor(Math.random() * 200000000) + 50000000,
} total_pledge: Math.floor(Math.random() * 1200000000) + 550000000,
total_shares: 19405918198,
pledge_ratio: parseFloat((Math.random() * 3 + 6).toFixed(2)), // 6-9%
pledge_count: Math.floor(Math.random() * 50) + 100 // 100-150
};
})
}, },
// 市场摘要 // 市场摘要 - 匹配 MarketSummary 类型
summaryData: { summaryData: {
success: true, success: true,
data: { data: {
current_price: basePrice, stock_code: stockCode,
change: 0.25, stock_name: stockCode === '000001' ? '平安银行' : '示例股票',
change_pct: 1.89, latest_trade: {
open: 13.35, close: basePrice,
high: 13.68, change_percent: 1.89,
low: 13.28, volume: 345678900,
volume: 345678900, amount: 4678900000,
amount: 4678900000, turnover_rate: 1.78,
turnover_rate: 1.78, pe_ratio: 4.96
pe_ratio: 4.96, },
pb_ratio: 0.72, latest_funding: {
total_market_cap: 262300000000, financing_balance: 5823000000,
circulating_market_cap: 262300000000 securities_balance: 125600000
},
latest_pledge: {
pledge_ratio: 8.25
}
} }
}, },
@@ -131,26 +141,57 @@ export const generateMarketData = (stockCode) => {
}) })
}, },
// 最新分时数据 // 最新分时数据 - 匹配 MinuteData 类型
latestMinuteData: { latestMinuteData: {
success: true, success: true,
data: Array(240).fill(null).map((_, i) => { data: (() => {
const minute = 9 * 60 + 30 + i; // 从9:30开始 const minuteData = [];
const hour = Math.floor(minute / 60); // 上午 9:30-11:30 (120分钟)
const min = minute % 60; for (let i = 0; i < 120; i++) {
const time = `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}`; const hour = 9 + Math.floor((30 + i) / 60);
const randomChange = (Math.random() - 0.5) * 0.1; const min = (30 + i) % 60;
return { const time = `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}`;
time, const randomChange = (Math.random() - 0.5) * 0.1;
price: (basePrice + randomChange).toFixed(2), const open = parseFloat((basePrice + randomChange).toFixed(2));
volume: Math.floor(Math.random() * 2000000) + 500000, const close = parseFloat((basePrice + randomChange + (Math.random() - 0.5) * 0.05).toFixed(2));
avg_price: (basePrice + randomChange * 0.8).toFixed(2) const high = parseFloat(Math.max(open, close, open + Math.random() * 0.05).toFixed(2));
}; const low = parseFloat(Math.min(open, close, close - Math.random() * 0.05).toFixed(2));
}), minuteData.push({
time,
open,
close,
high,
low,
volume: Math.floor(Math.random() * 2000000) + 500000,
amount: Math.floor(Math.random() * 30000000) + 5000000
});
}
// 下午 13:00-15:00 (120分钟)
for (let i = 0; i < 120; i++) {
const hour = 13 + Math.floor(i / 60);
const min = i % 60;
const time = `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}`;
const randomChange = (Math.random() - 0.5) * 0.1;
const open = parseFloat((basePrice + randomChange).toFixed(2));
const close = parseFloat((basePrice + randomChange + (Math.random() - 0.5) * 0.05).toFixed(2));
const high = parseFloat(Math.max(open, close, open + Math.random() * 0.05).toFixed(2));
const low = parseFloat(Math.min(open, close, close - Math.random() * 0.05).toFixed(2));
minuteData.push({
time,
open,
close,
high,
low,
volume: Math.floor(Math.random() * 1500000) + 400000,
amount: Math.floor(Math.random() * 25000000) + 4000000
});
}
return minuteData;
})(),
code: stockCode, code: stockCode,
name: stockCode === '000001' ? '平安银行' : '示例股票', name: stockCode === '000001' ? '平安银行' : '示例股票',
trade_date: new Date().toISOString().split('T')[0], trade_date: new Date().toISOString().split('T')[0],
type: 'minute' type: '1min'
} }
}; };
}; };

View File

@@ -1,6 +1,6 @@
# Company 目录结构说明 # Company 目录结构说明
> 最后更新2025-12-11 > 最后更新2025-12-12
## 目录结构 ## 目录结构
@@ -67,7 +67,7 @@ src/views/Company/
│ │ └── ManagementCard.tsx # 人员卡片memo │ │ └── ManagementCard.tsx # 人员卡片memo
│ │ │ │
│ ├── MarketDataView/ # Tab: 股票行情TypeScript │ ├── MarketDataView/ # Tab: 股票行情TypeScript
│ │ ├── index.tsx # 主组件入口 │ │ ├── index.tsx # 主组件入口~285 行Tab 容器)
│ │ ├── types.ts # 类型定义 │ │ ├── types.ts # 类型定义
│ │ ├── constants.ts # 主题配置、常量 │ │ ├── constants.ts # 主题配置、常量
│ │ ├── services/ │ │ ├── services/
@@ -82,7 +82,14 @@ src/views/Company/
│ │ ├── ThemedCard.tsx # 主题化卡片 │ │ ├── ThemedCard.tsx # 主题化卡片
│ │ ├── MarkdownRenderer.tsx # Markdown 渲染 │ │ ├── MarkdownRenderer.tsx # Markdown 渲染
│ │ ├── StockSummaryCard.tsx # 股票概览卡片 │ │ ├── StockSummaryCard.tsx # 股票概览卡片
│ │ ── AnalysisModal.tsx # 涨幅分析模态框 │ │ ── AnalysisModal.tsx # 涨幅分析模态框
│ │ └── panels/ # Tab 面板组件
│ │ ├── index.ts # 面板组件统一导出
│ │ ├── TradeDataPanel.tsx # 交易数据K线图、分钟图、表格
│ │ ├── FundingPanel.tsx # 融资融券面板
│ │ ├── BigDealPanel.tsx # 大宗交易面板
│ │ ├── UnusualPanel.tsx # 龙虎榜面板
│ │ └── PledgePanel.tsx # 股权质押面板
│ │ │ │
│ ├── DeepAnalysis/ # Tab: 深度分析 │ ├── DeepAnalysis/ # Tab: 深度分析
│ │ └── index.js │ │ └── index.js
@@ -451,7 +458,7 @@ CompanyOverview/
**拆分后文件结构** **拆分后文件结构**
``` ```
MarketDataView/ MarketDataView/
├── index.tsx # 主组件入口(~1049 行 ├── index.tsx # 主组件入口(~285 行Tab 容器
├── types.ts # 类型定义(~383 行) ├── types.ts # 类型定义(~383 行)
├── constants.ts # 主题配置、常量(~49 行) ├── constants.ts # 主题配置、常量(~49 行)
├── services/ ├── services/
@@ -466,14 +473,21 @@ MarketDataView/
├── ThemedCard.tsx # 主题化卡片(~32 行) ├── ThemedCard.tsx # 主题化卡片(~32 行)
├── MarkdownRenderer.tsx # Markdown 渲染(~65 行) ├── MarkdownRenderer.tsx # Markdown 渲染(~65 行)
├── StockSummaryCard.tsx # 股票概览卡片(~133 行) ├── StockSummaryCard.tsx # 股票概览卡片(~133 行)
── AnalysisModal.tsx # 涨幅分析模态框(~188 行) ── AnalysisModal.tsx # 涨幅分析模态框(~188 行)
└── panels/ # Tab 面板组件2025-12-12 拆分)
├── index.ts # 面板组件统一导出
├── TradeDataPanel.tsx # 交易数据面板(~381 行)
├── FundingPanel.tsx # 融资融券面板(~113 行)
├── BigDealPanel.tsx # 大宗交易面板(~143 行)
├── UnusualPanel.tsx # 龙虎榜面板(~163 行)
└── PledgePanel.tsx # 股权质押面板(~124 行)
``` ```
**文件职责说明** **文件职责说明**
| 文件 | 行数 | 职责 | | 文件 | 行数 | 职责 |
|------|------|------| |------|------|------|
| `index.tsx` | ~1049 | 主组件,包含 5 个 Tab 面板(交易数据、融资融券、大宗交易、龙虎榜、股权质押) | | `index.tsx` | ~285 | 主组件,Tab 容器和状态管理,导入使用 5 个 Panel 组件 |
| `types.ts` | ~383 | 所有 TypeScript 类型定义Theme、TradeDayData、MinuteData、FundingData 等) | | `types.ts` | ~383 | 所有 TypeScript 类型定义Theme、TradeDayData、MinuteData、FundingData 等) |
| `constants.ts` | ~49 | 主题配置light/dark、周期选项常量 | | `constants.ts` | ~49 | 主题配置light/dark、周期选项常量 |
| `marketService.ts` | ~173 | API 服务封装getMarketData、getMinuteData、getBigDealData 等) | | `marketService.ts` | ~173 | API 服务封装getMarketData、getMinuteData、getBigDealData 等) |
@@ -780,4 +794,44 @@ index.tsx
- **Hook 数据层**`useFinancialData` 封装 9 个 API 并行加载 - **Hook 数据层**`useFinancialData` 封装 9 个 API 并行加载
- **组件解耦**:每个表格/分析视图独立为组件 - **组件解耦**:每个表格/分析视图独立为组件
- **常量配置化**:指标定义可维护、可扩展 - **常量配置化**:指标定义可维护、可扩展
- **工具函数复用**:计算和图表配置统一管理 - **工具函数复用**:计算和图表配置统一管理
### 2025-12-12 MarketDataView Panel 拆分
**改动概述**
- `MarketDataView/index.tsx` 从 **1,049 行** 精简至 **285 行**(减少 73%
- 将 5 个 TabPanel 拆分为独立的面板组件
- 创建 `components/panels/` 子目录
**拆分后文件结构**
```
MarketDataView/components/panels/
├── index.ts # 面板组件统一导出
├── TradeDataPanel.tsx # 交易数据面板(~381 行)
├── FundingPanel.tsx # 融资融券面板(~113 行)
├── BigDealPanel.tsx # 大宗交易面板(~143 行)
├── UnusualPanel.tsx # 龙虎榜面板(~163 行)
└── PledgePanel.tsx # 股权质押面板(~124 行)
```
**面板组件职责**
| 组件 | 行数 | 功能 |
|------|------|------|
| `TradeDataPanel` | ~381 | K线图、分钟K线图、交易明细表格 |
| `FundingPanel` | ~113 | 融资融券图表和数据卡片 |
| `BigDealPanel` | ~143 | 大宗交易记录表格 |
| `UnusualPanel` | ~163 | 龙虎榜数据(买入/卖出前五) |
| `PledgePanel` | ~124 | 股权质押图表和明细表格 |
**优化效果**
| 指标 | 优化前 | 优化后 | 改善 |
|------|--------|--------|------|
| 主文件行数 | 1,049 | 285 | -73% |
| 面板组件 | 内联 | 5 个独立文件 | 模块化 |
| 可维护性 | 低 | 高 | 每个面板独立维护 |
**设计原则**
- **职责分离**:主组件只负责 Tab 容器和状态管理
- **组件复用**:面板组件可独立测试和维护
- **类型安全**:每个面板组件有独立的 Props 类型定义

View File

@@ -0,0 +1,143 @@
// src/views/Company/components/MarketDataView/components/panels/BigDealPanel.tsx
// 大宗交易面板 - 大宗交易记录表格
import React from 'react';
import {
Box,
Text,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
CardBody,
CardHeader,
Center,
Badge,
VStack,
HStack,
Tooltip,
Heading,
} from '@chakra-ui/react';
import ThemedCard from '../ThemedCard';
import { formatNumber } from '../../utils/formatUtils';
import type { Theme, BigDealData } from '../../types';
export interface BigDealPanelProps {
theme: Theme;
bigDealData: BigDealData;
}
const BigDealPanel: React.FC<BigDealPanelProps> = ({ theme, bigDealData }) => {
return (
<ThemedCard theme={theme}>
<CardHeader>
<Heading size="md" color={theme.textSecondary}>
</Heading>
</CardHeader>
<CardBody>
{bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? (
<VStack spacing={6} align="stretch">
{bigDealData.daily_stats.map((dayStats, idx) => (
<Box
key={idx}
p={4}
bg={theme.bgDark}
borderRadius="lg"
border="1px solid"
borderColor={theme.border}
>
<HStack justify="space-between" mb={4}>
<Text fontSize="lg" fontWeight="bold" color={theme.textSecondary}>
{dayStats.date}
</Text>
<HStack spacing={4}>
<Badge colorScheme="blue" fontSize="md">
: {dayStats.count}
</Badge>
<Badge colorScheme="green" fontSize="md">
: {formatNumber(dayStats.total_volume)}
</Badge>
<Badge colorScheme="orange" fontSize="md">
: {formatNumber(dayStats.total_amount)}
</Badge>
<Badge colorScheme="purple" fontSize="md">
: {dayStats.avg_price?.toFixed(2) || '-'}
</Badge>
</HStack>
</HStack>
{dayStats.deals && dayStats.deals.length > 0 && (
<TableContainer>
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th color={theme.textSecondary}></Th>
<Th 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>
{dayStats.deals.map((deal, i) => (
<Tr key={i} _hover={{ bg: 'rgba(43, 108, 176, 0.05)' }}>
<Td
color={theme.textPrimary}
fontSize="xs"
maxW="200px"
isTruncated
>
<Tooltip label={deal.buyer_dept || '-'} placement="top">
<Text>{deal.buyer_dept || '-'}</Text>
</Tooltip>
</Td>
<Td
color={theme.textPrimary}
fontSize="xs"
maxW="200px"
isTruncated
>
<Tooltip label={deal.seller_dept || '-'} placement="top">
<Text>{deal.seller_dept || '-'}</Text>
</Tooltip>
</Td>
<Td isNumeric color={theme.textPrimary} fontWeight="bold">
{deal.price?.toFixed(2) || '-'}
</Td>
<Td isNumeric color={theme.textPrimary}>
{deal.volume?.toFixed(2) || '-'}
</Td>
<Td isNumeric color={theme.textSecondary} fontWeight="bold">
{deal.amount?.toFixed(2) || '-'}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)}
</Box>
))}
</VStack>
) : (
<Center h="200px">
<Text color={theme.textMuted}></Text>
</Center>
)}
</CardBody>
</ThemedCard>
);
};
export default BigDealPanel;

View File

@@ -0,0 +1,113 @@
// src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx
// 融资融券面板 - 融资融券数据图表和卡片
import React from 'react';
import {
Box,
Text,
CardBody,
CardHeader,
VStack,
HStack,
Grid,
Heading,
} from '@chakra-ui/react';
import ReactECharts from 'echarts-for-react';
import ThemedCard from '../ThemedCard';
import { formatNumber } from '../../utils/formatUtils';
import { getFundingOption } from '../../utils/chartOptions';
import type { Theme, FundingDayData } from '../../types';
export interface FundingPanelProps {
theme: Theme;
fundingData: FundingDayData[];
}
const FundingPanel: React.FC<FundingPanelProps> = ({ theme, fundingData }) => {
return (
<VStack spacing={6} align="stretch">
<ThemedCard theme={theme}>
<CardBody>
{fundingData.length > 0 && (
<Box h="400px">
<ReactECharts
option={getFundingOption(theme, fundingData)}
style={{ height: '100%', width: '100%' }}
theme="light"
/>
</Box>
)}
</CardBody>
</ThemedCard>
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
{/* 融资数据 */}
<ThemedCard theme={theme}>
<CardHeader>
<Heading size="md" color={theme.success}>
</Heading>
</CardHeader>
<CardBody>
<VStack spacing={3} align="stretch">
{fundingData
.slice(-5)
.reverse()
.map((item, idx) => (
<Box key={idx} p={3} bg="rgba(255, 68, 68, 0.05)" borderRadius="md">
<HStack justify="space-between">
<Text color={theme.textMuted}>{item.date}</Text>
<VStack align="end" spacing={0}>
<Text color={theme.textPrimary} fontWeight="bold">
{formatNumber(item.financing.balance)}
</Text>
<Text fontSize="xs" color={theme.textMuted}>
{formatNumber(item.financing.buy)} /
{formatNumber(item.financing.repay)}
</Text>
</VStack>
</HStack>
</Box>
))}
</VStack>
</CardBody>
</ThemedCard>
{/* 融券数据 */}
<ThemedCard theme={theme}>
<CardHeader>
<Heading size="md" color={theme.danger}>
</Heading>
</CardHeader>
<CardBody>
<VStack spacing={3} align="stretch">
{fundingData
.slice(-5)
.reverse()
.map((item, idx) => (
<Box key={idx} p={3} bg="rgba(0, 200, 81, 0.05)" borderRadius="md">
<HStack justify="space-between">
<Text color={theme.textMuted}>{item.date}</Text>
<VStack align="end" spacing={0}>
<Text color={theme.textPrimary} fontWeight="bold">
{formatNumber(item.securities.balance)}
</Text>
<Text fontSize="xs" color={theme.textMuted}>
{formatNumber(item.securities.sell)} /
{formatNumber(item.securities.repay)}
</Text>
</VStack>
</HStack>
</Box>
))}
</VStack>
</CardBody>
</ThemedCard>
</Grid>
</VStack>
);
};
export default FundingPanel;

View File

@@ -0,0 +1,124 @@
// src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx
// 股权质押面板 - 质押图表和表格
import React from 'react';
import {
Box,
Text,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
CardBody,
CardHeader,
VStack,
Heading,
} from '@chakra-ui/react';
import ReactECharts from 'echarts-for-react';
import ThemedCard from '../ThemedCard';
import { formatNumber, formatPercent } from '../../utils/formatUtils';
import { getPledgeOption } from '../../utils/chartOptions';
import type { Theme, PledgeData } from '../../types';
export interface PledgePanelProps {
theme: Theme;
pledgeData: PledgeData[];
}
const PledgePanel: React.FC<PledgePanelProps> = ({ theme, pledgeData }) => {
return (
<VStack spacing={6} align="stretch">
<ThemedCard theme={theme}>
<CardBody>
{pledgeData.length > 0 && (
<Box h="400px">
<ReactECharts
option={getPledgeOption(theme, pledgeData)}
style={{ height: '100%', width: '100%' }}
theme="light"
/>
</Box>
)}
</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>
</Tr>
</Thead>
<Tbody>
{pledgeData.length > 0 ? (
pledgeData.map((item, idx) => (
<Tr key={idx} _hover={{ bg: theme.bgDark }}>
<Td color={theme.textPrimary}>{item.end_date}</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.unrestricted_pledge, 0)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.restricted_pledge, 0)}
</Td>
<Td isNumeric color={theme.textPrimary} fontWeight="bold">
{formatNumber(item.total_pledge, 0)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.total_shares, 0)}
</Td>
<Td isNumeric color={theme.warning} fontWeight="bold">
{formatPercent(item.pledge_ratio)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{item.pledge_count}
</Td>
</Tr>
))
) : (
<Tr>
<Td colSpan={7} textAlign="center" py={8}>
<Text fontSize="sm" color={theme.textMuted}>
</Text>
</Td>
</Tr>
)}
</Tbody>
</Table>
</TableContainer>
</CardBody>
</ThemedCard>
</VStack>
);
};
export default PledgePanel;

View File

@@ -0,0 +1,381 @@
// 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;

View File

@@ -0,0 +1,163 @@
// src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx
// 龙虎榜面板 - 龙虎榜数据展示
import React from 'react';
import {
Box,
Text,
CardBody,
CardHeader,
Center,
Badge,
VStack,
HStack,
Grid,
Heading,
} from '@chakra-ui/react';
import ThemedCard from '../ThemedCard';
import { formatNumber } from '../../utils/formatUtils';
import type { Theme, UnusualData } from '../../types';
export interface UnusualPanelProps {
theme: Theme;
unusualData: UnusualData;
}
const UnusualPanel: React.FC<UnusualPanelProps> = ({ theme, unusualData }) => {
return (
<ThemedCard theme={theme}>
<CardHeader>
<Heading size="md" color={theme.textSecondary}>
</Heading>
</CardHeader>
<CardBody>
{unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? (
<VStack spacing={6} align="stretch">
{unusualData.grouped_data.map((dayData, idx) => (
<Box
key={idx}
p={4}
bg={theme.bgDark}
borderRadius="lg"
border="1px solid"
borderColor={theme.border}
>
<HStack justify="space-between" mb={4}>
<Text fontSize="lg" fontWeight="bold" color={theme.textSecondary}>
{dayData.date}
</Text>
<HStack spacing={4}>
<Badge colorScheme="red" fontSize="md">
: {formatNumber(dayData.total_buy)}
</Badge>
<Badge colorScheme="green" fontSize="md">
: {formatNumber(dayData.total_sell)}
</Badge>
<Badge
colorScheme={dayData.net_amount > 0 ? 'red' : 'green'}
fontSize="md"
>
: {formatNumber(dayData.net_amount)}
</Badge>
</HStack>
</HStack>
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
<Box>
<Text fontWeight="bold" color={theme.success} mb={2}>
</Text>
<VStack spacing={1} align="stretch">
{dayData.buyers && dayData.buyers.length > 0 ? (
dayData.buyers.slice(0, 5).map((buyer, i) => (
<HStack
key={i}
justify="space-between"
p={2}
bg="rgba(255, 68, 68, 0.05)"
borderRadius="md"
>
<Text
fontSize="sm"
color={theme.textPrimary}
isTruncated
maxW="70%"
>
{buyer.dept_name}
</Text>
<Text fontSize="sm" color={theme.success} fontWeight="bold">
{formatNumber(buyer.buy_amount)}
</Text>
</HStack>
))
) : (
<Text fontSize="sm" color={theme.textMuted}>
</Text>
)}
</VStack>
</Box>
<Box>
<Text fontWeight="bold" color={theme.danger} mb={2}>
</Text>
<VStack spacing={1} align="stretch">
{dayData.sellers && dayData.sellers.length > 0 ? (
dayData.sellers.slice(0, 5).map((seller, i) => (
<HStack
key={i}
justify="space-between"
p={2}
bg="rgba(0, 200, 81, 0.05)"
borderRadius="md"
>
<Text
fontSize="sm"
color={theme.textPrimary}
isTruncated
maxW="70%"
>
{seller.dept_name}
</Text>
<Text fontSize="sm" color={theme.danger} fontWeight="bold">
{formatNumber(seller.sell_amount)}
</Text>
</HStack>
))
) : (
<Text fontSize="sm" color={theme.textMuted}>
</Text>
)}
</VStack>
</Box>
</Grid>
{/* 信息类型标签 */}
<HStack mt={3} spacing={2}>
<Text fontSize="sm" color={theme.textMuted}>
:
</Text>
{dayData.info_types?.map((type, i) => (
<Badge key={i} colorScheme="blue" fontSize="xs">
{type}
</Badge>
))}
</HStack>
</Box>
))}
</VStack>
) : (
<Center h="200px">
<Text color={theme.textMuted}></Text>
</Center>
)}
</CardBody>
</ThemedCard>
);
};
export default UnusualPanel;

View File

@@ -0,0 +1,15 @@
// src/views/Company/components/MarketDataView/components/panels/index.ts
// Panel 组件统一导出
export { default as TradeDataPanel } from './TradeDataPanel';
export { default as FundingPanel } from './FundingPanel';
export { default as BigDealPanel } from './BigDealPanel';
export { default as UnusualPanel } from './UnusualPanel';
export { default as PledgePanel } from './PledgePanel';
// 导出类型
export type { TradeDataPanelProps } from './TradeDataPanel';
export type { FundingPanelProps } from './FundingPanel';
export type { BigDealPanelProps } from './BigDealPanel';
export type { UnusualPanelProps } from './UnusualPanel';
export type { PledgePanelProps } from './PledgePanel';

View File

@@ -11,69 +11,41 @@ import {
Tab, Tab,
TabPanel, TabPanel,
Text, Text,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
SimpleGrid,
CardBody, CardBody,
CardHeader,
Spinner, Spinner,
Center, Center,
Badge,
VStack, VStack,
HStack, HStack,
Select, Select,
Button, Button,
Tooltip,
Grid,
GridItem,
Icon, Icon,
Heading,
Tag,
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
ChevronDownIcon,
ChevronUpIcon, ChevronUpIcon,
InfoIcon,
RepeatIcon, RepeatIcon,
TimeIcon,
ArrowUpIcon, ArrowUpIcon,
ArrowDownIcon,
StarIcon, StarIcon,
LockIcon, LockIcon,
UnlockIcon, UnlockIcon,
} from '@chakra-ui/icons'; } from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react';
// 内部模块导入 // 内部模块导入
import { themes, DEFAULT_PERIOD, PERIOD_OPTIONS } from './constants'; import { themes, DEFAULT_PERIOD, PERIOD_OPTIONS } from './constants';
import { useMarketData } from './hooks/useMarketData'; import { useMarketData } from './hooks/useMarketData';
import {
formatNumber,
formatPercent,
} from './utils/formatUtils';
import {
getKLineOption,
getMinuteKLineOption,
getFundingOption,
getPledgeOption,
} from './utils/chartOptions';
import { import {
ThemedCard, ThemedCard,
StockSummaryCard, StockSummaryCard,
AnalysisModal, AnalysisModal,
AnalysisContent, AnalysisContent,
} from './components'; } from './components';
import {
TradeDataPanel,
FundingPanel,
BigDealPanel,
UnusualPanel,
PledgePanel,
} from './components/panels';
import type { MarketDataViewProps, RiseAnalysis } from './types'; import type { MarketDataViewProps, RiseAnalysis } from './types';
/** /**
@@ -268,771 +240,35 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
<TabPanels> <TabPanels>
{/* 交易数据 Tab */} {/* 交易数据 Tab */}
<TabPanel px={0}> <TabPanel px={0}>
<VStack spacing={6} align="stretch"> <TradeDataPanel
{/* K线图 */} theme={theme}
<ThemedCard theme={theme}> tradeData={tradeData}
<CardBody> minuteData={minuteData}
{tradeData.length > 0 && ( minuteLoading={minuteLoading}
<Box h="600px"> analysisMap={analysisMap}
<ReactECharts onLoadMinuteData={loadMinuteData}
option={getKLineOption(theme, tradeData, analysisMap)} onChartClick={handleChartClick}
style={{ height: '100%', width: '100%' }} />
theme="light"
onEvents={{ click: handleChartClick }}
/>
</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={loadMinuteData}
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>
</TabPanel> </TabPanel>
{/* 融资融券 Tab */} {/* 融资融券 Tab */}
<TabPanel px={0}> <TabPanel px={0}>
<VStack spacing={6} align="stretch"> <FundingPanel theme={theme} fundingData={fundingData} />
<ThemedCard theme={theme}>
<CardBody>
{fundingData.length > 0 && (
<Box h="400px">
<ReactECharts
option={getFundingOption(theme, fundingData)}
style={{ height: '100%', width: '100%' }}
theme="light"
/>
</Box>
)}
</CardBody>
</ThemedCard>
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
{/* 融资数据 */}
<ThemedCard theme={theme}>
<CardHeader>
<Heading size="md" color={theme.success}>
</Heading>
</CardHeader>
<CardBody>
<VStack spacing={3} align="stretch">
{fundingData
.slice(-5)
.reverse()
.map((item, idx) => (
<Box
key={idx}
p={3}
bg="rgba(255, 68, 68, 0.05)"
borderRadius="md"
>
<HStack justify="space-between">
<Text color={theme.textMuted}>{item.date}</Text>
<VStack align="end" spacing={0}>
<Text color={theme.textPrimary} fontWeight="bold">
{formatNumber(item.financing.balance)}
</Text>
<Text fontSize="xs" color={theme.textMuted}>
{formatNumber(item.financing.buy)} /
{formatNumber(item.financing.repay)}
</Text>
</VStack>
</HStack>
</Box>
))}
</VStack>
</CardBody>
</ThemedCard>
{/* 融券数据 */}
<ThemedCard theme={theme}>
<CardHeader>
<Heading size="md" color={theme.danger}>
</Heading>
</CardHeader>
<CardBody>
<VStack spacing={3} align="stretch">
{fundingData
.slice(-5)
.reverse()
.map((item, idx) => (
<Box
key={idx}
p={3}
bg="rgba(0, 200, 81, 0.05)"
borderRadius="md"
>
<HStack justify="space-between">
<Text color={theme.textMuted}>{item.date}</Text>
<VStack align="end" spacing={0}>
<Text color={theme.textPrimary} fontWeight="bold">
{formatNumber(item.securities.balance)}
</Text>
<Text fontSize="xs" color={theme.textMuted}>
{formatNumber(item.securities.sell)} /
{formatNumber(item.securities.repay)}
</Text>
</VStack>
</HStack>
</Box>
))}
</VStack>
</CardBody>
</ThemedCard>
</Grid>
</VStack>
</TabPanel> </TabPanel>
{/* 大宗交易 Tab */} {/* 大宗交易 Tab */}
<TabPanel px={0}> <TabPanel px={0}>
<ThemedCard theme={theme}> <BigDealPanel theme={theme} bigDealData={bigDealData} />
<CardHeader>
<Heading size="md" color={theme.textSecondary}>
</Heading>
</CardHeader>
<CardBody>
{bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? (
<VStack spacing={6} align="stretch">
{bigDealData.daily_stats.map((dayStats, idx) => (
<Box
key={idx}
p={4}
bg={theme.bgDark}
borderRadius="lg"
border="1px solid"
borderColor={theme.border}
>
<HStack justify="space-between" mb={4}>
<Text fontSize="lg" fontWeight="bold" color={theme.textSecondary}>
{dayStats.date}
</Text>
<HStack spacing={4}>
<Badge colorScheme="blue" fontSize="md">
: {dayStats.count}
</Badge>
<Badge colorScheme="green" fontSize="md">
: {formatNumber(dayStats.total_volume)}
</Badge>
<Badge colorScheme="orange" fontSize="md">
: {formatNumber(dayStats.total_amount)}
</Badge>
<Badge colorScheme="purple" fontSize="md">
: {dayStats.avg_price?.toFixed(2) || '-'}
</Badge>
</HStack>
</HStack>
{dayStats.deals && dayStats.deals.length > 0 && (
<TableContainer>
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th color={theme.textSecondary}></Th>
<Th 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>
{dayStats.deals.map((deal, i) => (
<Tr key={i} _hover={{ bg: 'rgba(43, 108, 176, 0.05)' }}>
<Td
color={theme.textPrimary}
fontSize="xs"
maxW="200px"
isTruncated
>
<Tooltip label={deal.buyer_dept || '-'} placement="top">
<Text>{deal.buyer_dept || '-'}</Text>
</Tooltip>
</Td>
<Td
color={theme.textPrimary}
fontSize="xs"
maxW="200px"
isTruncated
>
<Tooltip
label={deal.seller_dept || '-'}
placement="top"
>
<Text>{deal.seller_dept || '-'}</Text>
</Tooltip>
</Td>
<Td isNumeric color={theme.textPrimary} fontWeight="bold">
{deal.price?.toFixed(2) || '-'}
</Td>
<Td isNumeric color={theme.textPrimary}>
{deal.volume?.toFixed(2) || '-'}
</Td>
<Td
isNumeric
color={theme.textSecondary}
fontWeight="bold"
>
{deal.amount?.toFixed(2) || '-'}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)}
</Box>
))}
</VStack>
) : (
<Center h="200px">
<Text color={theme.textMuted}></Text>
</Center>
)}
</CardBody>
</ThemedCard>
</TabPanel> </TabPanel>
{/* 龙虎榜 Tab */} {/* 龙虎榜 Tab */}
<TabPanel px={0}> <TabPanel px={0}>
<ThemedCard theme={theme}> <UnusualPanel theme={theme} unusualData={unusualData} />
<CardHeader>
<Heading size="md" color={theme.textSecondary}>
</Heading>
</CardHeader>
<CardBody>
{unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? (
<VStack spacing={6} align="stretch">
{unusualData.grouped_data.map((dayData, idx) => (
<Box
key={idx}
p={4}
bg={theme.bgDark}
borderRadius="lg"
border="1px solid"
borderColor={theme.border}
>
<HStack justify="space-between" mb={4}>
<Text fontSize="lg" fontWeight="bold" color={theme.textSecondary}>
{dayData.date}
</Text>
<HStack spacing={4}>
<Badge colorScheme="red" fontSize="md">
: {formatNumber(dayData.total_buy)}
</Badge>
<Badge colorScheme="green" fontSize="md">
: {formatNumber(dayData.total_sell)}
</Badge>
<Badge
colorScheme={dayData.net_amount > 0 ? 'red' : 'green'}
fontSize="md"
>
: {formatNumber(dayData.net_amount)}
</Badge>
</HStack>
</HStack>
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
<Box>
<Text fontWeight="bold" color={theme.success} mb={2}>
</Text>
<VStack spacing={1} align="stretch">
{dayData.buyers && dayData.buyers.length > 0 ? (
dayData.buyers.slice(0, 5).map((buyer, i) => (
<HStack
key={i}
justify="space-between"
p={2}
bg="rgba(255, 68, 68, 0.05)"
borderRadius="md"
>
<Text
fontSize="sm"
color={theme.textPrimary}
isTruncated
maxW="70%"
>
{buyer.dept_name}
</Text>
<Text
fontSize="sm"
color={theme.success}
fontWeight="bold"
>
{formatNumber(buyer.buy_amount)}
</Text>
</HStack>
))
) : (
<Text fontSize="sm" color={theme.textMuted}>
</Text>
)}
</VStack>
</Box>
<Box>
<Text fontWeight="bold" color={theme.danger} mb={2}>
</Text>
<VStack spacing={1} align="stretch">
{dayData.sellers && dayData.sellers.length > 0 ? (
dayData.sellers.slice(0, 5).map((seller, i) => (
<HStack
key={i}
justify="space-between"
p={2}
bg="rgba(0, 200, 81, 0.05)"
borderRadius="md"
>
<Text
fontSize="sm"
color={theme.textPrimary}
isTruncated
maxW="70%"
>
{seller.dept_name}
</Text>
<Text
fontSize="sm"
color={theme.danger}
fontWeight="bold"
>
{formatNumber(seller.sell_amount)}
</Text>
</HStack>
))
) : (
<Text fontSize="sm" color={theme.textMuted}>
</Text>
)}
</VStack>
</Box>
</Grid>
{/* 信息类型标签 */}
<HStack mt={3} spacing={2}>
<Text fontSize="sm" color={theme.textMuted}>
:
</Text>
{dayData.info_types?.map((type, i) => (
<Badge key={i} colorScheme="blue" fontSize="xs">
{type}
</Badge>
))}
</HStack>
</Box>
))}
</VStack>
) : (
<Center h="200px">
<Text color={theme.textMuted}></Text>
</Center>
)}
</CardBody>
</ThemedCard>
</TabPanel> </TabPanel>
{/* 股权质押 Tab */} {/* 股权质押 Tab */}
<TabPanel px={0}> <TabPanel px={0}>
<VStack spacing={6} align="stretch"> <PledgePanel theme={theme} pledgeData={pledgeData} />
<ThemedCard theme={theme}>
<CardBody>
{pledgeData.length > 0 && (
<Box h="400px">
<ReactECharts
option={getPledgeOption(theme, pledgeData)}
style={{ height: '100%', width: '100%' }}
theme="light"
/>
</Box>
)}
</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>
</Tr>
</Thead>
<Tbody>
{pledgeData.length > 0 ? (
pledgeData.map((item, idx) => (
<Tr key={idx} _hover={{ bg: theme.bgDark }}>
<Td color={theme.textPrimary}>{item.end_date}</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.unrestricted_pledge, 0)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.restricted_pledge, 0)}
</Td>
<Td isNumeric color={theme.textPrimary} fontWeight="bold">
{formatNumber(item.total_pledge, 0)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.total_shares, 0)}
</Td>
<Td isNumeric color={theme.warning} fontWeight="bold">
{formatPercent(item.pledge_ratio)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{item.pledge_count}
</Td>
</Tr>
))
) : (
<Tr>
<Td colSpan={7} textAlign="center" py={8}>
<Text fontSize="sm" color={theme.textMuted}>
</Text>
</Td>
</Tr>
)}
</Tbody>
</Table>
</TableContainer>
</CardBody>
</ThemedCard>
</VStack>
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>