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
|
// src/views/Company/components/MarketDataView/components/panels/BigDealPanel.tsx
|
||||||
// 大宗交易面板 - 黑金主题
|
// 大宗交易面板 - 黑金主题
|
||||||
|
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@@ -12,73 +12,23 @@ import {
|
|||||||
Th,
|
Th,
|
||||||
Td,
|
Td,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
Center,
|
|
||||||
VStack,
|
VStack,
|
||||||
HStack,
|
HStack,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Heading,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { formatNumber } from '../../utils/formatUtils';
|
import { formatNumber } from '../../utils/formatUtils';
|
||||||
import { darkGoldTheme } from '../../constants';
|
import { darkGoldTheme } from '../../constants';
|
||||||
import type { Theme, BigDealData } from '../../types';
|
import { DarkGoldCard, DarkGoldBadge, EmptyState } from '../shared';
|
||||||
|
import type { BigDealData } from '../../types';
|
||||||
|
|
||||||
export interface BigDealPanelProps {
|
export interface BigDealPanelProps {
|
||||||
theme: Theme;
|
|
||||||
bigDealData: BigDealData;
|
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 }) => {
|
const BigDealPanel: React.FC<BigDealPanelProps> = ({ bigDealData }) => {
|
||||||
return (
|
return (
|
||||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
<DarkGoldCard title="大宗交易记录">
|
||||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
|
||||||
<Heading size="md" color={darkGoldTheme.gold}>
|
|
||||||
大宗交易记录
|
|
||||||
</Heading>
|
|
||||||
</Box>
|
|
||||||
<Box p={4}>
|
|
||||||
{bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? (
|
{bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? (
|
||||||
<VStack spacing={4} align="stretch">
|
<VStack spacing={4} align="stretch">
|
||||||
{bigDealData.daily_stats.map((dayStats, idx) => (
|
{bigDealData.daily_stats.map((dayStats, idx) => (
|
||||||
@@ -175,13 +125,10 @@ const BigDealPanel: React.FC<BigDealPanelProps> = ({ bigDealData }) => {
|
|||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
<Center h="200px">
|
<EmptyState message="暂无大宗交易数据" />
|
||||||
<Text color={darkGoldTheme.textMuted}>暂无大宗交易数据</Text>
|
|
||||||
</Center>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</DarkGoldCard>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BigDealPanel;
|
export default memo(BigDealPanel);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx
|
// src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx
|
||||||
// 融资融券面板 - 黑金主题
|
// 融资融券面板 - 黑金主题
|
||||||
|
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@@ -10,41 +10,27 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Heading,
|
Heading,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ECharts from '@components/Charts/ECharts';
|
||||||
|
|
||||||
import { formatNumber } from '../../utils/formatUtils';
|
import { formatNumber } from '../../utils/formatUtils';
|
||||||
import { getFundingDarkGoldOption } from '../../utils/chartOptions';
|
import { getFundingDarkGoldOption } from '../../utils/chartOptions';
|
||||||
import { darkGoldTheme } from '../../constants';
|
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 {
|
export interface FundingPanelProps {
|
||||||
theme: Theme;
|
|
||||||
fundingData: FundingDayData[];
|
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 }) => {
|
const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
||||||
return (
|
return (
|
||||||
<VStack spacing={6} align="stretch">
|
<VStack spacing={6} align="stretch">
|
||||||
{/* 图表卡片 */}
|
{/* 图表卡片 */}
|
||||||
<Box {...darkGoldCardStyle} p={6}>
|
<Box sx={darkGoldCardFullStyle} p={6}>
|
||||||
{fundingData.length > 0 && (
|
{fundingData.length > 0 && (
|
||||||
<Box h="400px">
|
<Box h="400px">
|
||||||
<ReactECharts
|
<ECharts
|
||||||
option={getFundingDarkGoldOption(fundingData)}
|
option={getFundingDarkGoldOption(fundingData)}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
@@ -55,13 +41,7 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
|||||||
|
|
||||||
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
|
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
|
||||||
{/* 融资数据 */}
|
{/* 融资数据 */}
|
||||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
<DarkGoldCard title="融资数据" titleColor={darkGoldTheme.gold}>
|
||||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
|
||||||
<Heading size="md" color={darkGoldTheme.gold}>
|
|
||||||
融资数据
|
|
||||||
</Heading>
|
|
||||||
</Box>
|
|
||||||
<Box p={4}>
|
|
||||||
<VStack spacing={3} align="stretch">
|
<VStack spacing={3} align="stretch">
|
||||||
{fundingData
|
{fundingData
|
||||||
.slice(-5)
|
.slice(-5)
|
||||||
@@ -97,17 +77,10 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
</Box>
|
</DarkGoldCard>
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 融券数据 */}
|
{/* 融券数据 */}
|
||||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
<DarkGoldCard title="融券数据" titleColor={darkGoldTheme.orange}>
|
||||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
|
||||||
<Heading size="md" color={darkGoldTheme.orange}>
|
|
||||||
融券数据
|
|
||||||
</Heading>
|
|
||||||
</Box>
|
|
||||||
<Box p={4}>
|
|
||||||
<VStack spacing={3} align="stretch">
|
<VStack spacing={3} align="stretch">
|
||||||
{fundingData
|
{fundingData
|
||||||
.slice(-5)
|
.slice(-5)
|
||||||
@@ -143,11 +116,10 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
</Box>
|
</DarkGoldCard>
|
||||||
</Box>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FundingPanel;
|
export default memo(FundingPanel);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx
|
// src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx
|
||||||
// 股权质押面板 - 黑金主题
|
// 股权质押面板 - 黑金主题
|
||||||
|
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@@ -13,42 +13,28 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
VStack,
|
VStack,
|
||||||
Heading,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ECharts from '@components/Charts/ECharts';
|
||||||
|
|
||||||
import { formatNumber, formatPercent } from '../../utils/formatUtils';
|
import { formatNumber, formatPercent } from '../../utils/formatUtils';
|
||||||
import { getPledgeDarkGoldOption } from '../../utils/chartOptions';
|
import { getPledgeDarkGoldOption } from '../../utils/chartOptions';
|
||||||
import { darkGoldTheme } from '../../constants';
|
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 {
|
export interface PledgePanelProps {
|
||||||
theme: Theme;
|
|
||||||
pledgeData: PledgeData[];
|
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 }) => {
|
const PledgePanel: React.FC<PledgePanelProps> = ({ pledgeData }) => {
|
||||||
return (
|
return (
|
||||||
<VStack spacing={6} align="stretch">
|
<VStack spacing={6} align="stretch">
|
||||||
{/* 图表卡片 */}
|
{/* 图表卡片 */}
|
||||||
<Box {...darkGoldCardStyle} p={6}>
|
<Box sx={darkGoldCardFullStyle} p={6}>
|
||||||
{pledgeData.length > 0 && (
|
{pledgeData.length > 0 && (
|
||||||
<Box h="400px">
|
<Box h="400px">
|
||||||
<ReactECharts
|
<ECharts
|
||||||
option={getPledgeDarkGoldOption(pledgeData)}
|
option={getPledgeDarkGoldOption(pledgeData)}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
@@ -58,13 +44,7 @@ const PledgePanel: React.FC<PledgePanelProps> = ({ pledgeData }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 质押明细表格 */}
|
{/* 质押明细表格 */}
|
||||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
<DarkGoldCard title="质押明细">
|
||||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
|
||||||
<Heading size="md" color={darkGoldTheme.gold}>
|
|
||||||
质押明细
|
|
||||||
</Heading>
|
|
||||||
</Box>
|
|
||||||
<Box p={4}>
|
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table variant="unstyled" size="sm">
|
<Table variant="unstyled" size="sm">
|
||||||
<Thead>
|
<Thead>
|
||||||
@@ -132,10 +112,9 @@ const PledgePanel: React.FC<PledgePanelProps> = ({ pledgeData }) => {
|
|||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Box>
|
</DarkGoldCard>
|
||||||
</Box>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PledgePanel;
|
export default memo(PledgePanel);
|
||||||
|
|||||||
@@ -1,76 +1,27 @@
|
|||||||
// src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx
|
// src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx
|
||||||
// 龙虎榜面板 - 黑金主题
|
// 龙虎榜面板 - 黑金主题
|
||||||
|
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
Center,
|
|
||||||
VStack,
|
VStack,
|
||||||
HStack,
|
HStack,
|
||||||
Grid,
|
Grid,
|
||||||
Heading,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { formatNumber } from '../../utils/formatUtils';
|
import { formatNumber } from '../../utils/formatUtils';
|
||||||
import { darkGoldTheme } from '../../constants';
|
import { darkGoldTheme } from '../../constants';
|
||||||
import type { Theme, UnusualData } from '../../types';
|
import { DarkGoldCard, DarkGoldBadge, EmptyState } from '../shared';
|
||||||
|
import type { UnusualData } from '../../types';
|
||||||
|
|
||||||
export interface UnusualPanelProps {
|
export interface UnusualPanelProps {
|
||||||
theme: Theme;
|
|
||||||
unusualData: UnusualData;
|
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 }) => {
|
const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
|
||||||
return (
|
return (
|
||||||
<Box {...darkGoldCardStyle} overflow="hidden">
|
<DarkGoldCard title="龙虎榜数据">
|
||||||
<Box p={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
|
||||||
<Heading size="md" color={darkGoldTheme.gold}>
|
|
||||||
龙虎榜数据
|
|
||||||
</Heading>
|
|
||||||
</Box>
|
|
||||||
<Box p={4}>
|
|
||||||
{unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? (
|
{unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? (
|
||||||
<VStack spacing={4} align="stretch">
|
<VStack spacing={4} align="stretch">
|
||||||
{unusualData.grouped_data.map((dayData, idx) => (
|
{unusualData.grouped_data.map((dayData, idx) => (
|
||||||
@@ -208,13 +159,10 @@ const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
|
|||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
<Center h="200px">
|
<EmptyState message="暂无龙虎榜数据" />
|
||||||
<Text color={darkGoldTheme.textMuted}>暂无龙虎榜数据</Text>
|
|
||||||
</Center>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</DarkGoldCard>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
// MarketDataView 数据获取 Hook
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { marketService } from '../services/marketService';
|
import { marketService } from '../services/marketService';
|
||||||
import { DEFAULT_PERIOD } from '../constants';
|
import { DEFAULT_PERIOD } from '../constants';
|
||||||
@@ -17,6 +18,11 @@ import type {
|
|||||||
UseMarketDataReturn,
|
UseMarketDataReturn,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
|
// 判断是否为取消请求的错误
|
||||||
|
const isCancelError = (error: unknown): boolean => {
|
||||||
|
return axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 市场数据获取 Hook
|
* 市场数据获取 Hook
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
@@ -51,6 +57,11 @@ export const useMarketData = (
|
|||||||
// 记录上一次的 period,用于判断是否需要刷新交易数据
|
// 记录上一次的 period,用于判断是否需要刷新交易数据
|
||||||
const prevPeriodRef = useRef(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 来建立日期索引映射
|
* 需要 tradeData 来建立日期索引映射
|
||||||
@@ -100,14 +111,20 @@ export const useMarketData = (
|
|||||||
const loadCoreData = useCallback(async () => {
|
const loadCoreData = useCallback(async () => {
|
||||||
if (!stockCode) return;
|
if (!stockCode) return;
|
||||||
|
|
||||||
|
// 取消之前的核心数据请求
|
||||||
|
coreDataControllerRef.current?.abort();
|
||||||
|
const controller = new AbortController();
|
||||||
|
coreDataControllerRef.current = controller;
|
||||||
|
const options = { signal: controller.signal };
|
||||||
|
|
||||||
logger.debug('useMarketData', '开始加载核心市场数据', { stockCode, period });
|
logger.debug('useMarketData', '开始加载核心市场数据', { stockCode, period });
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setAnalysisMap({}); // 清空旧的分析数据
|
setAnalysisMap({}); // 清空旧的分析数据
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [summaryRes, tradeRes] = await Promise.all([
|
const [summaryRes, tradeRes] = await Promise.all([
|
||||||
marketService.getMarketSummary(stockCode),
|
marketService.getMarketSummary(stockCode, options),
|
||||||
marketService.getTradeData(stockCode, period),
|
marketService.getTradeData(stockCode, period, options),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 设置概览数据
|
// 设置概览数据
|
||||||
@@ -131,10 +148,15 @@ export const useMarketData = (
|
|||||||
loadRiseAnalysis(loadedTradeData);
|
loadRiseAnalysis(loadedTradeData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 取消请求不作为错误处理
|
||||||
|
if (isCancelError(error)) return;
|
||||||
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
|
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
|
||||||
} finally {
|
} finally {
|
||||||
|
// 只有当前请求没有被取消时才设置 loading 状态
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [stockCode, period, loadRiseAnalysis]);
|
}, [stockCode, period, loadRiseAnalysis]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,12 +166,18 @@ export const useMarketData = (
|
|||||||
if (!stockCode) return;
|
if (!stockCode) return;
|
||||||
if (loadedDataRef.current[dataType]) 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 });
|
logger.debug('useMarketData', `按需加载 ${dataType} 数据`, { stockCode });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (dataType) {
|
switch (dataType) {
|
||||||
case 'funding': {
|
case 'funding': {
|
||||||
const res = await marketService.getFundingData(stockCode, 30);
|
const res = await marketService.getFundingData(stockCode, 30, options);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setFundingData(res.data);
|
setFundingData(res.data);
|
||||||
loadedDataRef.current.funding = true;
|
loadedDataRef.current.funding = true;
|
||||||
@@ -157,7 +185,7 @@ export const useMarketData = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'bigDeal': {
|
case 'bigDeal': {
|
||||||
const res = await marketService.getBigDealData(stockCode, 30);
|
const res = await marketService.getBigDealData(stockCode, 30, options);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setBigDealData(res);
|
setBigDealData(res);
|
||||||
loadedDataRef.current.bigDeal = true;
|
loadedDataRef.current.bigDeal = true;
|
||||||
@@ -165,7 +193,7 @@ export const useMarketData = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'unusual': {
|
case 'unusual': {
|
||||||
const res = await marketService.getUnusualData(stockCode, 30);
|
const res = await marketService.getUnusualData(stockCode, 30, options);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setUnusualData(res);
|
setUnusualData(res);
|
||||||
loadedDataRef.current.unusual = true;
|
loadedDataRef.current.unusual = true;
|
||||||
@@ -173,7 +201,7 @@ export const useMarketData = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'pledge': {
|
case 'pledge': {
|
||||||
const res = await marketService.getPledgeData(stockCode);
|
const res = await marketService.getPledgeData(stockCode, options);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setPledgeData(res.data);
|
setPledgeData(res.data);
|
||||||
loadedDataRef.current.pledge = true;
|
loadedDataRef.current.pledge = true;
|
||||||
@@ -183,6 +211,8 @@ export const useMarketData = (
|
|||||||
}
|
}
|
||||||
logger.info('useMarketData', `${dataType} 数据加载成功`, { stockCode });
|
logger.info('useMarketData', `${dataType} 数据加载成功`, { stockCode });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 取消请求不作为错误处理
|
||||||
|
if (isCancelError(error)) return;
|
||||||
logger.error('useMarketData', `loadDataByType:${dataType}`, error, { stockCode });
|
logger.error('useMarketData', `loadDataByType:${dataType}`, error, { stockCode });
|
||||||
}
|
}
|
||||||
}, [stockCode]);
|
}, [stockCode]);
|
||||||
@@ -200,11 +230,17 @@ export const useMarketData = (
|
|||||||
const loadMinuteData = useCallback(async () => {
|
const loadMinuteData = useCallback(async () => {
|
||||||
if (!stockCode) return;
|
if (!stockCode) return;
|
||||||
|
|
||||||
|
// 取消之前的分钟数据请求
|
||||||
|
minuteDataControllerRef.current?.abort();
|
||||||
|
const controller = new AbortController();
|
||||||
|
minuteDataControllerRef.current = controller;
|
||||||
|
const options = { signal: controller.signal };
|
||||||
|
|
||||||
logger.debug('useMarketData', '开始加载分钟频数据', { stockCode });
|
logger.debug('useMarketData', '开始加载分钟频数据', { stockCode });
|
||||||
setMinuteLoading(true);
|
setMinuteLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await marketService.getMinuteData(stockCode);
|
const data = await marketService.getMinuteData(stockCode, options);
|
||||||
setMinuteData(data);
|
setMinuteData(data);
|
||||||
|
|
||||||
if (data.data && data.data.length > 0) {
|
if (data.data && data.data.length > 0) {
|
||||||
@@ -216,6 +252,8 @@ export const useMarketData = (
|
|||||||
logger.warn('useMarketData', '分钟频数据为空', { stockCode });
|
logger.warn('useMarketData', '分钟频数据为空', { stockCode });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 取消请求不作为错误处理
|
||||||
|
if (isCancelError(error)) return;
|
||||||
logger.error('useMarketData', 'loadMinuteData', error, { stockCode });
|
logger.error('useMarketData', 'loadMinuteData', error, { stockCode });
|
||||||
setMinuteData({
|
setMinuteData({
|
||||||
data: [],
|
data: [],
|
||||||
@@ -225,8 +263,11 @@ export const useMarketData = (
|
|||||||
type: 'minute',
|
type: 'minute',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
// 只有当前请求没有被取消时才设置 loading 状态
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
setMinuteLoading(false);
|
setMinuteLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [stockCode]);
|
}, [stockCode]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -303,6 +344,15 @@ export const useMarketData = (
|
|||||||
}
|
}
|
||||||
}, [period, refreshTradeData, stockCode]);
|
}, [period, refreshTradeData, stockCode]);
|
||||||
|
|
||||||
|
// 组件卸载时取消所有进行中的请求
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
coreDataControllerRef.current?.abort();
|
||||||
|
tabDataControllerRef.current?.abort();
|
||||||
|
minuteDataControllerRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
tradeLoading,
|
tradeLoading,
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
// src/views/Company/components/MarketDataView/index.tsx
|
// src/views/Company/components/MarketDataView/index.tsx
|
||||||
// MarketDataView 主组件 - 股票市场数据综合展示
|
// MarketDataView 主组件 - 股票市场数据综合展示
|
||||||
|
|
||||||
import React, { useState, useEffect, ReactNode, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, ReactNode, useMemo, useCallback, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
VStack,
|
VStack,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import { Unlock, ArrowUp, Star, Lock } from 'lucide-react';
|
||||||
Unlock,
|
|
||||||
ArrowUp,
|
|
||||||
Star,
|
|
||||||
Lock,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
// 通用组件
|
// 通用组件
|
||||||
import SubTabContainer from '@components/SubTabContainer';
|
import SubTabContainer from '@components/SubTabContainer';
|
||||||
@@ -36,7 +31,7 @@ import {
|
|||||||
PledgePanel,
|
PledgePanel,
|
||||||
} from './components/panels';
|
} from './components/panels';
|
||||||
import LoadingState from '../LoadingState';
|
import LoadingState from '../LoadingState';
|
||||||
import type { MarketDataViewProps, RiseAnalysis } from './types';
|
import type { MarketDataViewProps } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MarketDataView 主组件
|
* MarketDataView 主组件
|
||||||
@@ -118,37 +113,16 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
|||||||
{ key: 'pledge', name: '股权质押', icon: Lock, component: PledgePanel },
|
{ key: 'pledge', name: '股权质押', icon: Lock, component: PledgePanel },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 传递给 Tab 组件的 props
|
// 传递给 Tab 组件的 props - 只传递各 Tab 需要的数据
|
||||||
const componentProps = useMemo(
|
const componentProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
theme,
|
// 各 Tab 只使用自己需要的数据
|
||||||
tradeData,
|
|
||||||
minuteData,
|
|
||||||
minuteLoading,
|
|
||||||
analysisMap,
|
|
||||||
onLoadMinuteData: loadMinuteData,
|
|
||||||
onChartClick: handleChartClick,
|
|
||||||
selectedPeriod,
|
|
||||||
onPeriodChange: setSelectedPeriod,
|
|
||||||
fundingData,
|
fundingData,
|
||||||
bigDealData,
|
bigDealData,
|
||||||
unusualData,
|
unusualData,
|
||||||
pledgeData,
|
pledgeData,
|
||||||
}),
|
}),
|
||||||
[
|
[fundingData, bigDealData, unusualData, pledgeData]
|
||||||
theme,
|
|
||||||
tradeData,
|
|
||||||
minuteData,
|
|
||||||
minuteLoading,
|
|
||||||
analysisMap,
|
|
||||||
loadMinuteData,
|
|
||||||
handleChartClick,
|
|
||||||
selectedPeriod,
|
|
||||||
fundingData,
|
|
||||||
bigDealData,
|
|
||||||
unusualData,
|
|
||||||
pledgeData,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求选项
|
||||||
|
*/
|
||||||
|
interface RequestOptions {
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 市场数据服务
|
* 市场数据服务
|
||||||
*/
|
*/
|
||||||
@@ -30,9 +37,13 @@ export const marketService = {
|
|||||||
/**
|
/**
|
||||||
* 获取市场概览数据
|
* 获取市场概览数据
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getMarketSummary(stockCode: string): Promise<ApiResponse<MarketSummary>> {
|
async getMarketSummary(stockCode: string, options?: RequestOptions): Promise<ApiResponse<MarketSummary>> {
|
||||||
const { data } = await axios.get<ApiResponse<MarketSummary>>(`/api/market/summary/${stockCode}`);
|
const { data } = await axios.get<ApiResponse<MarketSummary>>(
|
||||||
|
`/api/market/summary/${stockCode}`,
|
||||||
|
{ signal: options?.signal }
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -40,9 +51,13 @@ export const marketService = {
|
|||||||
* 获取交易日数据
|
* 获取交易日数据
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
* @param days 天数,默认 60 天
|
* @param days 天数,默认 60 天
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getTradeData(stockCode: string, days: number = 60): Promise<ApiResponse<TradeDayData[]>> {
|
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}`);
|
const { data } = await axios.get<ApiResponse<TradeDayData[]>>(
|
||||||
|
`/api/market/trade/${stockCode}?days=${days}`,
|
||||||
|
{ signal: options?.signal }
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -50,9 +65,13 @@ export const marketService = {
|
|||||||
* 获取融资融券数据
|
* 获取融资融券数据
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
* @param days 天数,默认 30 天
|
* @param days 天数,默认 30 天
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getFundingData(stockCode: string, days: number = 30): Promise<ApiResponse<FundingDayData[]>> {
|
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}`);
|
const { data } = await axios.get<ApiResponse<FundingDayData[]>>(
|
||||||
|
`/api/market/funding/${stockCode}?days=${days}`,
|
||||||
|
{ signal: options?.signal }
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,9 +79,13 @@ export const marketService = {
|
|||||||
* 获取大宗交易数据
|
* 获取大宗交易数据
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
* @param days 天数,默认 30 天
|
* @param days 天数,默认 30 天
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getBigDealData(stockCode: string, days: number = 30): Promise<BigDealData> {
|
async getBigDealData(stockCode: string, days: number = 30, options?: RequestOptions): Promise<BigDealData> {
|
||||||
const { data } = await axios.get<BigDealData>(`/api/market/bigdeal/${stockCode}?days=${days}`);
|
const { data } = await axios.get<BigDealData>(
|
||||||
|
`/api/market/bigdeal/${stockCode}?days=${days}`,
|
||||||
|
{ signal: options?.signal }
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -70,18 +93,26 @@ export const marketService = {
|
|||||||
* 获取龙虎榜数据
|
* 获取龙虎榜数据
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
* @param days 天数,默认 30 天
|
* @param days 天数,默认 30 天
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getUnusualData(stockCode: string, days: number = 30): Promise<UnusualData> {
|
async getUnusualData(stockCode: string, days: number = 30, options?: RequestOptions): Promise<UnusualData> {
|
||||||
const { data } = await axios.get<UnusualData>(`/api/market/unusual/${stockCode}?days=${days}`);
|
const { data } = await axios.get<UnusualData>(
|
||||||
|
`/api/market/unusual/${stockCode}?days=${days}`,
|
||||||
|
{ signal: options?.signal }
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取股权质押数据
|
* 获取股权质押数据
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getPledgeData(stockCode: string): Promise<ApiResponse<PledgeData[]>> {
|
async getPledgeData(stockCode: string, options?: RequestOptions): Promise<ApiResponse<PledgeData[]>> {
|
||||||
const { data } = await axios.get<ApiResponse<PledgeData[]>>(`/api/market/pledge/${stockCode}`);
|
const { data } = await axios.get<ApiResponse<PledgeData[]>>(
|
||||||
|
`/api/market/pledge/${stockCode}`,
|
||||||
|
{ signal: options?.signal }
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -90,27 +121,33 @@ export const marketService = {
|
|||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
* @param startDate 开始日期(可选)
|
* @param startDate 开始日期(可选)
|
||||||
* @param endDate 结束日期(可选)
|
* @param endDate 结束日期(可选)
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getRiseAnalysis(
|
async getRiseAnalysis(
|
||||||
stockCode: string,
|
stockCode: string,
|
||||||
startDate?: string,
|
startDate?: string,
|
||||||
endDate?: string
|
endDate?: string,
|
||||||
|
options?: RequestOptions
|
||||||
): Promise<ApiResponse<RiseAnalysis[]>> {
|
): Promise<ApiResponse<RiseAnalysis[]>> {
|
||||||
let url = `/api/market/rise-analysis/${stockCode}`;
|
let url = `/api/market/rise-analysis/${stockCode}`;
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
url += `?start_date=${startDate}&end_date=${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;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取分钟K线数据
|
* 获取分钟K线数据
|
||||||
* @param stockCode 股票代码
|
* @param stockCode 股票代码
|
||||||
|
* @param options 请求选项
|
||||||
*/
|
*/
|
||||||
async getMinuteData(stockCode: string): Promise<MinuteData> {
|
async getMinuteData(stockCode: string, options?: RequestOptions): Promise<MinuteData> {
|
||||||
try {
|
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)) {
|
if (data.data && Array.isArray(data.data)) {
|
||||||
return data;
|
return data;
|
||||||
@@ -125,6 +162,10 @@ export const marketService = {
|
|||||||
type: 'minute',
|
type: 'minute',
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 取消请求不作为错误处理
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
logger.error('marketService', 'getMinuteData', error, { stockCode });
|
logger.error('marketService', 'getMinuteData', error, { stockCode });
|
||||||
// 返回空数据结构
|
// 返回空数据结构
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -927,7 +927,8 @@ export const getKLineDarkGoldOption = (
|
|||||||
|
|
||||||
// 布局配置(优化比例)
|
// 布局配置(优化比例)
|
||||||
// 主图: 55%, 成交量: 12%, 副图指标: 18%(如有)
|
// 主图: 55%, 成交量: 12%, 副图指标: 18%(如有)
|
||||||
const grids: EChartsOption['grid'] = [
|
// 注意:使用 object[] 而非 EChartsOption['grid'],因为后者可能是单个对象或数组
|
||||||
|
const grids: object[] = [
|
||||||
{
|
{
|
||||||
left: '3%',
|
left: '3%',
|
||||||
right: '3%',
|
right: '3%',
|
||||||
@@ -956,7 +957,7 @@ export const getKLineDarkGoldOption = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// X轴配置(使用 boundaryGap: true 确保柱状图对齐)
|
// X轴配置(使用 boundaryGap: true 确保柱状图对齐)
|
||||||
const xAxes: EChartsOption['xAxis'] = [
|
const xAxes: object[] = [
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: dates,
|
data: dates,
|
||||||
@@ -978,7 +979,7 @@ export const getKLineDarkGoldOption = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Y轴配置
|
// Y轴配置
|
||||||
const yAxes: EChartsOption['yAxis'] = [
|
const yAxes: object[] = [
|
||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@@ -1144,7 +1145,7 @@ export const getKLineDarkGoldOption = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建系列数据
|
// 构建系列数据
|
||||||
const series: EChartsOption['series'] = [
|
const series: object[] = [
|
||||||
{
|
{
|
||||||
name: 'K线',
|
name: 'K线',
|
||||||
type: 'candlestick',
|
type: 'candlestick',
|
||||||
|
|||||||
Reference in New Issue
Block a user