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,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 { UnusualPanelProps } from './UnusualPanel';
|
||||
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