refactor(MarketDataView): 提取共享组件,简化 Panel 结构
- 新增 shared 目录,提取重复组件: - DarkGoldCard: 黑金卡片容器 - DarkGoldBadge: 黑金徽章组件 - EmptyState: 空状态组件 - styles.ts: 共享样式定义 - 简化各 Panel 组件,移除重复代码 - 优化 index.tsx componentProps 传递 - 调整 hooks/services 数据获取逻辑 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/BigDealPanel.tsx
|
||||
// 大宗交易面板 - 黑金主题
|
||||
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
@@ -12,73 +12,23 @@ import {
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Center,
|
||||
VStack,
|
||||
HStack,
|
||||
Tooltip,
|
||||
Heading,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { formatNumber } from '../../utils/formatUtils';
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
import type { Theme, BigDealData } from '../../types';
|
||||
import { DarkGoldCard, DarkGoldBadge, EmptyState } from '../shared';
|
||||
import type { BigDealData } from '../../types';
|
||||
|
||||
export interface BigDealPanelProps {
|
||||
theme: Theme;
|
||||
bigDealData: BigDealData;
|
||||
}
|
||||
|
||||
// 黑金卡片样式
|
||||
const darkGoldCardStyle = {
|
||||
bg: darkGoldTheme.bgCard,
|
||||
border: '1px solid',
|
||||
borderColor: darkGoldTheme.border,
|
||||
borderRadius: 'xl',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
borderColor: darkGoldTheme.borderHover,
|
||||
boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)',
|
||||
},
|
||||
};
|
||||
|
||||
// 黑金徽章样式
|
||||
const DarkGoldBadge: React.FC<{ children: React.ReactNode; variant?: 'gold' | 'orange' | 'green' | 'purple' }> = ({
|
||||
children,
|
||||
variant = 'gold',
|
||||
}) => {
|
||||
const colors = {
|
||||
gold: { bg: 'rgba(212, 175, 55, 0.15)', color: darkGoldTheme.gold },
|
||||
orange: { bg: 'rgba(255, 149, 0, 0.15)', color: darkGoldTheme.orange },
|
||||
green: { bg: 'rgba(0, 200, 81, 0.15)', color: darkGoldTheme.green },
|
||||
purple: { bg: 'rgba(160, 120, 220, 0.15)', color: '#A078DC' },
|
||||
};
|
||||
const style = colors[variant];
|
||||
|
||||
return (
|
||||
<Box
|
||||
px={2}
|
||||
py={1}
|
||||
bg={style.bg}
|
||||
color={style.color}
|
||||
borderRadius="md"
|
||||
fontSize="xs"
|
||||
fontWeight="medium"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const BigDealPanel: React.FC<BigDealPanelProps> = ({ bigDealData }) => {
|
||||
return (
|
||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<Heading size="md" color={darkGoldTheme.gold}>
|
||||
大宗交易记录
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
<DarkGoldCard title="大宗交易记录">
|
||||
{bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? (
|
||||
<VStack spacing={4} align="stretch">
|
||||
{bigDealData.daily_stats.map((dayStats, idx) => (
|
||||
@@ -175,13 +125,10 @@ const BigDealPanel: React.FC<BigDealPanelProps> = ({ bigDealData }) => {
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<Text color={darkGoldTheme.textMuted}>暂无大宗交易数据</Text>
|
||||
</Center>
|
||||
<EmptyState message="暂无大宗交易数据" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DarkGoldCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default BigDealPanel;
|
||||
export default memo(BigDealPanel);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx
|
||||
// 融资融券面板 - 黑金主题
|
||||
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
@@ -10,41 +10,27 @@ import {
|
||||
Grid,
|
||||
Heading,
|
||||
} from '@chakra-ui/react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import ECharts from '@components/Charts/ECharts';
|
||||
|
||||
import { formatNumber } from '../../utils/formatUtils';
|
||||
import { getFundingDarkGoldOption } from '../../utils/chartOptions';
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
import type { Theme, FundingDayData } from '../../types';
|
||||
import { DarkGoldCard } from '../shared';
|
||||
import { darkGoldCardFullStyle } from '../shared/styles';
|
||||
import type { FundingDayData } from '../../types';
|
||||
|
||||
export interface FundingPanelProps {
|
||||
theme: Theme;
|
||||
fundingData: FundingDayData[];
|
||||
}
|
||||
|
||||
// 黑金卡片样式
|
||||
const darkGoldCardStyle = {
|
||||
bg: darkGoldTheme.bgCard,
|
||||
border: '1px solid',
|
||||
borderColor: darkGoldTheme.border,
|
||||
borderRadius: 'xl',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
borderColor: darkGoldTheme.borderHover,
|
||||
boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
};
|
||||
|
||||
const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
||||
return (
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 图表卡片 */}
|
||||
<Box {...darkGoldCardStyle} p={6}>
|
||||
<Box sx={darkGoldCardFullStyle} p={6}>
|
||||
{fundingData.length > 0 && (
|
||||
<Box h="400px">
|
||||
<ReactECharts
|
||||
<ECharts
|
||||
option={getFundingDarkGoldOption(fundingData)}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
theme="dark"
|
||||
@@ -55,13 +41,7 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
||||
|
||||
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
|
||||
{/* 融资数据 */}
|
||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<Heading size="md" color={darkGoldTheme.gold}>
|
||||
融资数据
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
<DarkGoldCard title="融资数据" titleColor={darkGoldTheme.gold}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{fundingData
|
||||
.slice(-5)
|
||||
@@ -97,17 +77,10 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
</DarkGoldCard>
|
||||
|
||||
{/* 融券数据 */}
|
||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<Heading size="md" color={darkGoldTheme.orange}>
|
||||
融券数据
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
<DarkGoldCard title="融券数据" titleColor={darkGoldTheme.orange}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{fundingData
|
||||
.slice(-5)
|
||||
@@ -143,11 +116,10 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
</DarkGoldCard>
|
||||
</Grid>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default FundingPanel;
|
||||
export default memo(FundingPanel);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx
|
||||
// 股权质押面板 - 黑金主题
|
||||
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
@@ -13,42 +13,28 @@ import {
|
||||
Td,
|
||||
TableContainer,
|
||||
VStack,
|
||||
Heading,
|
||||
} from '@chakra-ui/react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import ECharts from '@components/Charts/ECharts';
|
||||
|
||||
import { formatNumber, formatPercent } from '../../utils/formatUtils';
|
||||
import { getPledgeDarkGoldOption } from '../../utils/chartOptions';
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
import type { Theme, PledgeData } from '../../types';
|
||||
import { DarkGoldCard } from '../shared';
|
||||
import { darkGoldCardFullStyle } from '../shared/styles';
|
||||
import type { PledgeData } from '../../types';
|
||||
|
||||
export interface PledgePanelProps {
|
||||
theme: Theme;
|
||||
pledgeData: PledgeData[];
|
||||
}
|
||||
|
||||
// 黑金卡片样式
|
||||
const darkGoldCardStyle = {
|
||||
bg: darkGoldTheme.bgCard,
|
||||
border: '1px solid',
|
||||
borderColor: darkGoldTheme.border,
|
||||
borderRadius: 'xl',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
borderColor: darkGoldTheme.borderHover,
|
||||
boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)',
|
||||
},
|
||||
};
|
||||
|
||||
const PledgePanel: React.FC<PledgePanelProps> = ({ pledgeData }) => {
|
||||
return (
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 图表卡片 */}
|
||||
<Box {...darkGoldCardStyle} p={6}>
|
||||
<Box sx={darkGoldCardFullStyle} p={6}>
|
||||
{pledgeData.length > 0 && (
|
||||
<Box h="400px">
|
||||
<ReactECharts
|
||||
<ECharts
|
||||
option={getPledgeDarkGoldOption(pledgeData)}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
theme="dark"
|
||||
@@ -58,13 +44,7 @@ const PledgePanel: React.FC<PledgePanelProps> = ({ pledgeData }) => {
|
||||
</Box>
|
||||
|
||||
{/* 质押明细表格 */}
|
||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<Heading size="md" color={darkGoldTheme.gold}>
|
||||
质押明细
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
<DarkGoldCard title="质押明细">
|
||||
<TableContainer>
|
||||
<Table variant="unstyled" size="sm">
|
||||
<Thead>
|
||||
@@ -132,10 +112,9 @@ const PledgePanel: React.FC<PledgePanelProps> = ({ pledgeData }) => {
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
</DarkGoldCard>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PledgePanel;
|
||||
export default memo(PledgePanel);
|
||||
|
||||
@@ -1,76 +1,27 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx
|
||||
// 龙虎榜面板 - 黑金主题
|
||||
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Center,
|
||||
VStack,
|
||||
HStack,
|
||||
Grid,
|
||||
Heading,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { formatNumber } from '../../utils/formatUtils';
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
import type { Theme, UnusualData } from '../../types';
|
||||
import { DarkGoldCard, DarkGoldBadge, EmptyState } from '../shared';
|
||||
import type { UnusualData } from '../../types';
|
||||
|
||||
export interface UnusualPanelProps {
|
||||
theme: Theme;
|
||||
unusualData: UnusualData;
|
||||
}
|
||||
|
||||
// 黑金卡片样式
|
||||
const darkGoldCardStyle = {
|
||||
bg: darkGoldTheme.bgCard,
|
||||
border: '1px solid',
|
||||
borderColor: darkGoldTheme.border,
|
||||
borderRadius: 'xl',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
borderColor: darkGoldTheme.borderHover,
|
||||
boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)',
|
||||
},
|
||||
};
|
||||
|
||||
// 黑金徽章样式
|
||||
const DarkGoldBadge: React.FC<{ children: React.ReactNode; variant?: 'red' | 'green' | 'gold' }> = ({
|
||||
children,
|
||||
variant = 'gold',
|
||||
}) => {
|
||||
const colors = {
|
||||
red: { bg: 'rgba(255, 68, 68, 0.15)', color: darkGoldTheme.red },
|
||||
green: { bg: 'rgba(0, 200, 81, 0.15)', color: darkGoldTheme.green },
|
||||
gold: { bg: 'rgba(212, 175, 55, 0.15)', color: darkGoldTheme.gold },
|
||||
};
|
||||
const style = colors[variant];
|
||||
|
||||
return (
|
||||
<Box
|
||||
px={2}
|
||||
py={1}
|
||||
bg={style.bg}
|
||||
color={style.color}
|
||||
borderRadius="md"
|
||||
fontSize="xs"
|
||||
fontWeight="medium"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
|
||||
return (
|
||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<Heading size="md" color={darkGoldTheme.gold}>
|
||||
龙虎榜数据
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
<DarkGoldCard title="龙虎榜数据">
|
||||
{unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? (
|
||||
<VStack spacing={4} align="stretch">
|
||||
{unusualData.grouped_data.map((dayData, idx) => (
|
||||
@@ -208,13 +159,10 @@ const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<Text color={darkGoldTheme.textMuted}>暂无龙虎榜数据</Text>
|
||||
</Center>
|
||||
<EmptyState message="暂无龙虎榜数据" />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DarkGoldCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnusualPanel;
|
||||
export default memo(UnusualPanel);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// src/views/Company/components/MarketDataView/components/shared/DarkGoldBadge.tsx
|
||||
// 黑金主题徽章组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
|
||||
export type DarkGoldBadgeVariant = 'gold' | 'orange' | 'green' | 'red' | 'purple';
|
||||
|
||||
export interface DarkGoldBadgeProps extends Omit<BoxProps, 'children'> {
|
||||
children: React.ReactNode;
|
||||
variant?: DarkGoldBadgeVariant;
|
||||
}
|
||||
|
||||
// 徽章颜色配置
|
||||
const BADGE_COLORS: Record<DarkGoldBadgeVariant, { bg: string; color: string }> = {
|
||||
gold: { bg: 'rgba(212, 175, 55, 0.15)', color: darkGoldTheme.gold },
|
||||
orange: { bg: 'rgba(255, 149, 0, 0.15)', color: darkGoldTheme.orange },
|
||||
green: { bg: 'rgba(0, 200, 81, 0.15)', color: darkGoldTheme.green },
|
||||
red: { bg: 'rgba(255, 68, 68, 0.15)', color: darkGoldTheme.red },
|
||||
purple: { bg: 'rgba(160, 120, 220, 0.15)', color: '#A078DC' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 黑金主题徽章组件
|
||||
* 用于显示标签、状态等信息
|
||||
*/
|
||||
const DarkGoldBadge: React.FC<DarkGoldBadgeProps> = ({
|
||||
children,
|
||||
variant = 'gold',
|
||||
...boxProps
|
||||
}) => {
|
||||
const colors = BADGE_COLORS[variant];
|
||||
|
||||
return (
|
||||
<Box
|
||||
px={2}
|
||||
py={1}
|
||||
bg={colors.bg}
|
||||
color={colors.color}
|
||||
borderRadius="md"
|
||||
fontSize="xs"
|
||||
fontWeight="medium"
|
||||
{...boxProps}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DarkGoldBadge);
|
||||
@@ -0,0 +1,47 @@
|
||||
// src/views/Company/components/MarketDataView/components/shared/DarkGoldCard.tsx
|
||||
// 黑金主题卡片组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Box, Heading, BoxProps } from '@chakra-ui/react';
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
import { darkGoldCardFullStyle } from './styles';
|
||||
|
||||
export interface DarkGoldCardProps extends Omit<BoxProps, 'title'> {
|
||||
/** 卡片标题 */
|
||||
title?: string;
|
||||
/** 标题颜色 */
|
||||
titleColor?: string;
|
||||
/** 是否显示标题区域 */
|
||||
showHeader?: boolean;
|
||||
/** 内容区 padding */
|
||||
contentPadding?: number | string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑金主题卡片组件
|
||||
* 统一的卡片样式,包含标题区和内容区
|
||||
*/
|
||||
const DarkGoldCard: React.FC<DarkGoldCardProps> = ({
|
||||
title,
|
||||
titleColor = darkGoldTheme.gold,
|
||||
showHeader = true,
|
||||
contentPadding = 4,
|
||||
children,
|
||||
...boxProps
|
||||
}) => {
|
||||
return (
|
||||
<Box sx={darkGoldCardFullStyle} overflow="hidden" {...boxProps}>
|
||||
{showHeader && title && (
|
||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<Heading size="md" color={titleColor}>
|
||||
{title}
|
||||
</Heading>
|
||||
</Box>
|
||||
)}
|
||||
<Box p={contentPadding}>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DarkGoldCard);
|
||||
@@ -0,0 +1,28 @@
|
||||
// src/views/Company/components/MarketDataView/components/shared/EmptyState.tsx
|
||||
// 空状态组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Center, Text, CenterProps } from '@chakra-ui/react';
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
|
||||
export interface EmptyStateProps extends Omit<CenterProps, 'children'> {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 空状态组件
|
||||
* 数据为空时显示的占位组件
|
||||
*/
|
||||
const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
message = '暂无数据',
|
||||
h = '200px',
|
||||
...centerProps
|
||||
}) => {
|
||||
return (
|
||||
<Center h={h} {...centerProps}>
|
||||
<Text color={darkGoldTheme.textMuted}>{message}</Text>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EmptyState);
|
||||
@@ -0,0 +1,8 @@
|
||||
// src/views/Company/components/MarketDataView/components/shared/index.ts
|
||||
// 共享组件和样式导出
|
||||
|
||||
export { default as DarkGoldCard } from './DarkGoldCard';
|
||||
export { default as DarkGoldBadge } from './DarkGoldBadge';
|
||||
export { default as EmptyState } from './EmptyState';
|
||||
export { darkGoldCardStyle, darkGoldCardHoverStyle } from './styles';
|
||||
export type { DarkGoldBadgeVariant } from './DarkGoldBadge';
|
||||
@@ -0,0 +1,59 @@
|
||||
// src/views/Company/components/MarketDataView/components/shared/styles.ts
|
||||
// 共享样式常量
|
||||
|
||||
import { darkGoldTheme } from '../../constants';
|
||||
import type { SystemStyleObject } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* 黑金卡片基础样式
|
||||
*/
|
||||
export const darkGoldCardStyle: SystemStyleObject = {
|
||||
bg: darkGoldTheme.bgCard,
|
||||
border: '1px solid',
|
||||
borderColor: darkGoldTheme.border,
|
||||
borderRadius: 'xl',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
};
|
||||
|
||||
/**
|
||||
* 黑金卡片悬停样式
|
||||
*/
|
||||
export const darkGoldCardHoverStyle: SystemStyleObject = {
|
||||
borderColor: darkGoldTheme.borderHover,
|
||||
boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)',
|
||||
transform: 'translateY(-2px)',
|
||||
};
|
||||
|
||||
/**
|
||||
* 黑金卡片完整样式(包含 hover)
|
||||
*/
|
||||
export const darkGoldCardFullStyle: SystemStyleObject = {
|
||||
...darkGoldCardStyle,
|
||||
_hover: darkGoldCardHoverStyle,
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据项行样式
|
||||
*/
|
||||
export const dataRowStyle: SystemStyleObject = {
|
||||
p: 3,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
transition: 'all 0.2s',
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格行悬停样式
|
||||
*/
|
||||
export const tableRowHoverStyle: SystemStyleObject = {
|
||||
bg: 'rgba(212, 175, 55, 0.08)',
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格边框样式
|
||||
*/
|
||||
export const tableBorderStyle: SystemStyleObject = {
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'rgba(212, 175, 55, 0.1)',
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
// MarketDataView 数据获取 Hook
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { logger } from '@utils/logger';
|
||||
import { marketService } from '../services/marketService';
|
||||
import { DEFAULT_PERIOD } from '../constants';
|
||||
@@ -17,6 +18,11 @@ import type {
|
||||
UseMarketDataReturn,
|
||||
} from '../types';
|
||||
|
||||
// 判断是否为取消请求的错误
|
||||
const isCancelError = (error: unknown): boolean => {
|
||||
return axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError');
|
||||
};
|
||||
|
||||
/**
|
||||
* 市场数据获取 Hook
|
||||
* @param stockCode 股票代码
|
||||
@@ -51,6 +57,11 @@ export const useMarketData = (
|
||||
// 记录上一次的 period,用于判断是否需要刷新交易数据
|
||||
const prevPeriodRef = useRef(period);
|
||||
|
||||
// AbortController refs - 用于取消请求
|
||||
const coreDataControllerRef = useRef<AbortController | null>(null);
|
||||
const tabDataControllerRef = useRef<AbortController | null>(null);
|
||||
const minuteDataControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
/**
|
||||
* 加载涨幅分析数据(懒加载)
|
||||
* 需要 tradeData 来建立日期索引映射
|
||||
@@ -100,14 +111,20 @@ export const useMarketData = (
|
||||
const loadCoreData = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
// 取消之前的核心数据请求
|
||||
coreDataControllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
coreDataControllerRef.current = controller;
|
||||
const options = { signal: controller.signal };
|
||||
|
||||
logger.debug('useMarketData', '开始加载核心市场数据', { stockCode, period });
|
||||
setLoading(true);
|
||||
setAnalysisMap({}); // 清空旧的分析数据
|
||||
|
||||
try {
|
||||
const [summaryRes, tradeRes] = await Promise.all([
|
||||
marketService.getMarketSummary(stockCode),
|
||||
marketService.getTradeData(stockCode, period),
|
||||
marketService.getMarketSummary(stockCode, options),
|
||||
marketService.getTradeData(stockCode, period, options),
|
||||
]);
|
||||
|
||||
// 设置概览数据
|
||||
@@ -131,10 +148,15 @@ export const useMarketData = (
|
||||
loadRiseAnalysis(loadedTradeData);
|
||||
}
|
||||
} catch (error) {
|
||||
// 取消请求不作为错误处理
|
||||
if (isCancelError(error)) return;
|
||||
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
|
||||
} finally {
|
||||
// 只有当前请求没有被取消时才设置 loading 状态
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [stockCode, period, loadRiseAnalysis]);
|
||||
|
||||
/**
|
||||
@@ -144,12 +166,18 @@ export const useMarketData = (
|
||||
if (!stockCode) return;
|
||||
if (loadedDataRef.current[dataType]) return; // 已加载则跳过
|
||||
|
||||
// 取消之前的 Tab 数据请求
|
||||
tabDataControllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
tabDataControllerRef.current = controller;
|
||||
const options = { signal: controller.signal };
|
||||
|
||||
logger.debug('useMarketData', `按需加载 ${dataType} 数据`, { stockCode });
|
||||
|
||||
try {
|
||||
switch (dataType) {
|
||||
case 'funding': {
|
||||
const res = await marketService.getFundingData(stockCode, 30);
|
||||
const res = await marketService.getFundingData(stockCode, 30, options);
|
||||
if (res.success) {
|
||||
setFundingData(res.data);
|
||||
loadedDataRef.current.funding = true;
|
||||
@@ -157,7 +185,7 @@ export const useMarketData = (
|
||||
break;
|
||||
}
|
||||
case 'bigDeal': {
|
||||
const res = await marketService.getBigDealData(stockCode, 30);
|
||||
const res = await marketService.getBigDealData(stockCode, 30, options);
|
||||
if (res.success) {
|
||||
setBigDealData(res);
|
||||
loadedDataRef.current.bigDeal = true;
|
||||
@@ -165,7 +193,7 @@ export const useMarketData = (
|
||||
break;
|
||||
}
|
||||
case 'unusual': {
|
||||
const res = await marketService.getUnusualData(stockCode, 30);
|
||||
const res = await marketService.getUnusualData(stockCode, 30, options);
|
||||
if (res.success) {
|
||||
setUnusualData(res);
|
||||
loadedDataRef.current.unusual = true;
|
||||
@@ -173,7 +201,7 @@ export const useMarketData = (
|
||||
break;
|
||||
}
|
||||
case 'pledge': {
|
||||
const res = await marketService.getPledgeData(stockCode);
|
||||
const res = await marketService.getPledgeData(stockCode, options);
|
||||
if (res.success) {
|
||||
setPledgeData(res.data);
|
||||
loadedDataRef.current.pledge = true;
|
||||
@@ -183,6 +211,8 @@ export const useMarketData = (
|
||||
}
|
||||
logger.info('useMarketData', `${dataType} 数据加载成功`, { stockCode });
|
||||
} catch (error) {
|
||||
// 取消请求不作为错误处理
|
||||
if (isCancelError(error)) return;
|
||||
logger.error('useMarketData', `loadDataByType:${dataType}`, error, { stockCode });
|
||||
}
|
||||
}, [stockCode]);
|
||||
@@ -200,11 +230,17 @@ export const useMarketData = (
|
||||
const loadMinuteData = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
// 取消之前的分钟数据请求
|
||||
minuteDataControllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
minuteDataControllerRef.current = controller;
|
||||
const options = { signal: controller.signal };
|
||||
|
||||
logger.debug('useMarketData', '开始加载分钟频数据', { stockCode });
|
||||
setMinuteLoading(true);
|
||||
|
||||
try {
|
||||
const data = await marketService.getMinuteData(stockCode);
|
||||
const data = await marketService.getMinuteData(stockCode, options);
|
||||
setMinuteData(data);
|
||||
|
||||
if (data.data && data.data.length > 0) {
|
||||
@@ -216,6 +252,8 @@ export const useMarketData = (
|
||||
logger.warn('useMarketData', '分钟频数据为空', { stockCode });
|
||||
}
|
||||
} catch (error) {
|
||||
// 取消请求不作为错误处理
|
||||
if (isCancelError(error)) return;
|
||||
logger.error('useMarketData', 'loadMinuteData', error, { stockCode });
|
||||
setMinuteData({
|
||||
data: [],
|
||||
@@ -225,8 +263,11 @@ export const useMarketData = (
|
||||
type: 'minute',
|
||||
});
|
||||
} finally {
|
||||
// 只有当前请求没有被取消时才设置 loading 状态
|
||||
if (!controller.signal.aborted) {
|
||||
setMinuteLoading(false);
|
||||
}
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
/**
|
||||
@@ -303,6 +344,15 @@ export const useMarketData = (
|
||||
}
|
||||
}, [period, refreshTradeData, stockCode]);
|
||||
|
||||
// 组件卸载时取消所有进行中的请求
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
coreDataControllerRef.current?.abort();
|
||||
tabDataControllerRef.current?.abort();
|
||||
minuteDataControllerRef.current?.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
tradeLoading,
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
// src/views/Company/components/MarketDataView/index.tsx
|
||||
// MarketDataView 主组件 - 股票市场数据综合展示
|
||||
|
||||
import React, { useState, useEffect, ReactNode, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, ReactNode, useMemo, useCallback, memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
VStack,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
Unlock,
|
||||
ArrowUp,
|
||||
Star,
|
||||
Lock,
|
||||
} from 'lucide-react';
|
||||
import { Unlock, ArrowUp, Star, Lock } from 'lucide-react';
|
||||
|
||||
// 通用组件
|
||||
import SubTabContainer from '@components/SubTabContainer';
|
||||
@@ -36,7 +31,7 @@ import {
|
||||
PledgePanel,
|
||||
} from './components/panels';
|
||||
import LoadingState from '../LoadingState';
|
||||
import type { MarketDataViewProps, RiseAnalysis } from './types';
|
||||
import type { MarketDataViewProps } from './types';
|
||||
|
||||
/**
|
||||
* MarketDataView 主组件
|
||||
@@ -118,37 +113,16 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
||||
{ key: 'pledge', name: '股权质押', icon: Lock, component: PledgePanel },
|
||||
];
|
||||
|
||||
// 传递给 Tab 组件的 props
|
||||
// 传递给 Tab 组件的 props - 只传递各 Tab 需要的数据
|
||||
const componentProps = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
tradeData,
|
||||
minuteData,
|
||||
minuteLoading,
|
||||
analysisMap,
|
||||
onLoadMinuteData: loadMinuteData,
|
||||
onChartClick: handleChartClick,
|
||||
selectedPeriod,
|
||||
onPeriodChange: setSelectedPeriod,
|
||||
// 各 Tab 只使用自己需要的数据
|
||||
fundingData,
|
||||
bigDealData,
|
||||
unusualData,
|
||||
pledgeData,
|
||||
}),
|
||||
[
|
||||
theme,
|
||||
tradeData,
|
||||
minuteData,
|
||||
minuteLoading,
|
||||
analysisMap,
|
||||
loadMinuteData,
|
||||
handleChartClick,
|
||||
selectedPeriod,
|
||||
fundingData,
|
||||
bigDealData,
|
||||
unusualData,
|
||||
pledgeData,
|
||||
]
|
||||
[fundingData, bigDealData, unusualData, pledgeData]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -203,4 +177,4 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketDataView;
|
||||
export default memo(MarketDataView);
|
||||
|
||||
@@ -23,6 +23,13 @@ interface ApiResponse<T> {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求选项
|
||||
*/
|
||||
interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 市场数据服务
|
||||
*/
|
||||
@@ -30,9 +37,13 @@ export const marketService = {
|
||||
/**
|
||||
* 获取市场概览数据
|
||||
* @param stockCode 股票代码
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getMarketSummary(stockCode: string): Promise<ApiResponse<MarketSummary>> {
|
||||
const { data } = await axios.get<ApiResponse<MarketSummary>>(`/api/market/summary/${stockCode}`);
|
||||
async getMarketSummary(stockCode: string, options?: RequestOptions): Promise<ApiResponse<MarketSummary>> {
|
||||
const { data } = await axios.get<ApiResponse<MarketSummary>>(
|
||||
`/api/market/summary/${stockCode}`,
|
||||
{ signal: options?.signal }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -40,9 +51,13 @@ export const marketService = {
|
||||
* 获取交易日数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 60 天
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getTradeData(stockCode: string, days: number = 60): Promise<ApiResponse<TradeDayData[]>> {
|
||||
const { data } = await axios.get<ApiResponse<TradeDayData[]>>(`/api/market/trade/${stockCode}?days=${days}`);
|
||||
async getTradeData(stockCode: string, days: number = 60, options?: RequestOptions): Promise<ApiResponse<TradeDayData[]>> {
|
||||
const { data } = await axios.get<ApiResponse<TradeDayData[]>>(
|
||||
`/api/market/trade/${stockCode}?days=${days}`,
|
||||
{ signal: options?.signal }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -50,9 +65,13 @@ export const marketService = {
|
||||
* 获取融资融券数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 30 天
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getFundingData(stockCode: string, days: number = 30): Promise<ApiResponse<FundingDayData[]>> {
|
||||
const { data } = await axios.get<ApiResponse<FundingDayData[]>>(`/api/market/funding/${stockCode}?days=${days}`);
|
||||
async getFundingData(stockCode: string, days: number = 30, options?: RequestOptions): Promise<ApiResponse<FundingDayData[]>> {
|
||||
const { data } = await axios.get<ApiResponse<FundingDayData[]>>(
|
||||
`/api/market/funding/${stockCode}?days=${days}`,
|
||||
{ signal: options?.signal }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -60,9 +79,13 @@ export const marketService = {
|
||||
* 获取大宗交易数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 30 天
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getBigDealData(stockCode: string, days: number = 30): Promise<BigDealData> {
|
||||
const { data } = await axios.get<BigDealData>(`/api/market/bigdeal/${stockCode}?days=${days}`);
|
||||
async getBigDealData(stockCode: string, days: number = 30, options?: RequestOptions): Promise<BigDealData> {
|
||||
const { data } = await axios.get<BigDealData>(
|
||||
`/api/market/bigdeal/${stockCode}?days=${days}`,
|
||||
{ signal: options?.signal }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -70,18 +93,26 @@ export const marketService = {
|
||||
* 获取龙虎榜数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 30 天
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getUnusualData(stockCode: string, days: number = 30): Promise<UnusualData> {
|
||||
const { data } = await axios.get<UnusualData>(`/api/market/unusual/${stockCode}?days=${days}`);
|
||||
async getUnusualData(stockCode: string, days: number = 30, options?: RequestOptions): Promise<UnusualData> {
|
||||
const { data } = await axios.get<UnusualData>(
|
||||
`/api/market/unusual/${stockCode}?days=${days}`,
|
||||
{ signal: options?.signal }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取股权质押数据
|
||||
* @param stockCode 股票代码
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getPledgeData(stockCode: string): Promise<ApiResponse<PledgeData[]>> {
|
||||
const { data } = await axios.get<ApiResponse<PledgeData[]>>(`/api/market/pledge/${stockCode}`);
|
||||
async getPledgeData(stockCode: string, options?: RequestOptions): Promise<ApiResponse<PledgeData[]>> {
|
||||
const { data } = await axios.get<ApiResponse<PledgeData[]>>(
|
||||
`/api/market/pledge/${stockCode}`,
|
||||
{ signal: options?.signal }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -90,27 +121,33 @@ export const marketService = {
|
||||
* @param stockCode 股票代码
|
||||
* @param startDate 开始日期(可选)
|
||||
* @param endDate 结束日期(可选)
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getRiseAnalysis(
|
||||
stockCode: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
endDate?: string,
|
||||
options?: RequestOptions
|
||||
): Promise<ApiResponse<RiseAnalysis[]>> {
|
||||
let url = `/api/market/rise-analysis/${stockCode}`;
|
||||
if (startDate && endDate) {
|
||||
url += `?start_date=${startDate}&end_date=${endDate}`;
|
||||
}
|
||||
const { data } = await axios.get<ApiResponse<RiseAnalysis[]>>(url);
|
||||
const { data } = await axios.get<ApiResponse<RiseAnalysis[]>>(url, { signal: options?.signal });
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取分钟K线数据
|
||||
* @param stockCode 股票代码
|
||||
* @param options 请求选项
|
||||
*/
|
||||
async getMinuteData(stockCode: string): Promise<MinuteData> {
|
||||
async getMinuteData(stockCode: string, options?: RequestOptions): Promise<MinuteData> {
|
||||
try {
|
||||
const { data } = await axios.get<MinuteData>(`/api/stock/${stockCode}/latest-minute`);
|
||||
const { data } = await axios.get<MinuteData>(
|
||||
`/api/stock/${stockCode}/latest-minute`,
|
||||
{ signal: options?.signal }
|
||||
);
|
||||
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
return data;
|
||||
@@ -125,6 +162,10 @@ export const marketService = {
|
||||
type: 'minute',
|
||||
};
|
||||
} catch (error) {
|
||||
// 取消请求不作为错误处理
|
||||
if (axios.isCancel(error)) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('marketService', 'getMinuteData', error, { stockCode });
|
||||
// 返回空数据结构
|
||||
return {
|
||||
|
||||
@@ -927,7 +927,8 @@ export const getKLineDarkGoldOption = (
|
||||
|
||||
// 布局配置(优化比例)
|
||||
// 主图: 55%, 成交量: 12%, 副图指标: 18%(如有)
|
||||
const grids: EChartsOption['grid'] = [
|
||||
// 注意:使用 object[] 而非 EChartsOption['grid'],因为后者可能是单个对象或数组
|
||||
const grids: object[] = [
|
||||
{
|
||||
left: '3%',
|
||||
right: '3%',
|
||||
@@ -956,7 +957,7 @@ export const getKLineDarkGoldOption = (
|
||||
}
|
||||
|
||||
// X轴配置(使用 boundaryGap: true 确保柱状图对齐)
|
||||
const xAxes: EChartsOption['xAxis'] = [
|
||||
const xAxes: object[] = [
|
||||
{
|
||||
type: 'category',
|
||||
data: dates,
|
||||
@@ -978,7 +979,7 @@ export const getKLineDarkGoldOption = (
|
||||
];
|
||||
|
||||
// Y轴配置
|
||||
const yAxes: EChartsOption['yAxis'] = [
|
||||
const yAxes: object[] = [
|
||||
{
|
||||
scale: true,
|
||||
splitLine: {
|
||||
@@ -1144,7 +1145,7 @@ export const getKLineDarkGoldOption = (
|
||||
}
|
||||
|
||||
// 构建系列数据
|
||||
const series: EChartsOption['series'] = [
|
||||
const series: object[] = [
|
||||
{
|
||||
name: 'K线',
|
||||
type: 'candlestick',
|
||||
|
||||
Reference in New Issue
Block a user