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>
This commit is contained in:
zdl
2025-12-12 18:08:40 +08:00
parent 85a857dc19
commit adfc0bd478
2 changed files with 35 additions and 784 deletions

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,
TabPanel,
Text,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
SimpleGrid,
CardBody,
CardHeader,
Spinner,
Center,
Badge,
VStack,
HStack,
Select,
Button,
Tooltip,
Grid,
GridItem,
Icon,
Heading,
Tag,
useDisclosure,
} from '@chakra-ui/react';
import {
ChevronDownIcon,
ChevronUpIcon,
InfoIcon,
RepeatIcon,
TimeIcon,
ArrowUpIcon,
ArrowDownIcon,
StarIcon,
LockIcon,
UnlockIcon,
} from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react';
// 内部模块导入
import { themes, DEFAULT_PERIOD, PERIOD_OPTIONS } from './constants';
import { useMarketData } from './hooks/useMarketData';
import {
formatNumber,
formatPercent,
} from './utils/formatUtils';
import {
getKLineOption,
getMinuteKLineOption,
getFundingOption,
getPledgeOption,
} from './utils/chartOptions';
import {
ThemedCard,
StockSummaryCard,
AnalysisModal,
AnalysisContent,
} from './components';
import {
TradeDataPanel,
FundingPanel,
BigDealPanel,
UnusualPanel,
PledgePanel,
} from './components/panels';
import type { MarketDataViewProps, RiseAnalysis } from './types';
/**
@@ -268,771 +240,35 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
<TabPanels>
{/* 交易数据 Tab */}
<TabPanel px={0}>
<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: handleChartClick }}
<TradeDataPanel
theme={theme}
tradeData={tradeData}
minuteData={minuteData}
minuteLoading={minuteLoading}
analysisMap={analysisMap}
onLoadMinuteData={loadMinuteData}
onChartClick={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>
{/* 融资融券 Tab */}
<TabPanel px={0}>
<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>
<FundingPanel theme={theme} fundingData={fundingData} />
</TabPanel>
{/* 大宗交易 Tab */}
<TabPanel px={0}>
<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>
<BigDealPanel theme={theme} bigDealData={bigDealData} />
</TabPanel>
{/* 龙虎榜 Tab */}
<TabPanel px={0}>
<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>
<UnusualPanel theme={theme} unusualData={unusualData} />
</TabPanel>
{/* 股权质押 Tab */}
<TabPanel px={0}>
<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>
<PledgePanel theme={theme} pledgeData={pledgeData} />
</TabPanel>
</TabPanels>
</Tabs>