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>
This commit is contained in:
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user