From 010ed9b5bf16b834a28f8854baeaefca58111e2f Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 19 Dec 2025 13:09:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor(MarketDataView):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=AE=80=E5=8C=96?= =?UTF-8?q?=20Panel=20=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 --- .../components/panels/BigDealPanel.tsx | 255 +++++++----------- .../components/panels/FundingPanel.tsx | 188 ++++++------- .../components/panels/PledgePanel.tsx | 41 +-- .../components/panels/UnusualPanel.tsx | 68 +---- .../components/shared/DarkGoldBadge.tsx | 51 ++++ .../components/shared/DarkGoldCard.tsx | 47 ++++ .../components/shared/EmptyState.tsx | 28 ++ .../MarketDataView/components/shared/index.ts | 8 + .../components/shared/styles.ts | 59 ++++ .../MarketDataView/hooks/useMarketData.ts | 68 ++++- .../components/MarketDataView/index.tsx | 40 +-- .../MarketDataView/services/marketService.ts | 73 +++-- .../MarketDataView/utils/chartOptions.ts | 9 +- 13 files changed, 520 insertions(+), 415 deletions(-) create mode 100644 src/views/Company/components/MarketDataView/components/shared/DarkGoldBadge.tsx create mode 100644 src/views/Company/components/MarketDataView/components/shared/DarkGoldCard.tsx create mode 100644 src/views/Company/components/MarketDataView/components/shared/EmptyState.tsx create mode 100644 src/views/Company/components/MarketDataView/components/shared/index.ts create mode 100644 src/views/Company/components/MarketDataView/components/shared/styles.ts diff --git a/src/views/Company/components/MarketDataView/components/panels/BigDealPanel.tsx b/src/views/Company/components/MarketDataView/components/panels/BigDealPanel.tsx index 8cfa52b8..81ebd87a 100644 --- a/src/views/Company/components/MarketDataView/components/panels/BigDealPanel.tsx +++ b/src/views/Company/components/MarketDataView/components/panels/BigDealPanel.tsx @@ -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,176 +12,123 @@ 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 ( - - {children} - - ); -}; - const BigDealPanel: React.FC = ({ bigDealData }) => { return ( - - - - 大宗交易记录 - - - - {bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? ( - - {bigDealData.daily_stats.map((dayStats, idx) => ( - - - - {dayStats.date} - - - - 交易笔数: {dayStats.count} - - - 成交量: {formatNumber(dayStats.total_volume)}万股 - - - 成交额: {formatNumber(dayStats.total_amount)}万元 - - - 均价: {dayStats.avg_price?.toFixed(2) || '-'}元 - - + + {bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? ( + + {bigDealData.daily_stats.map((dayStats, idx) => ( + + + + {dayStats.date} + + + + 交易笔数: {dayStats.count} + + + 成交量: {formatNumber(dayStats.total_volume)}万股 + + + 成交额: {formatNumber(dayStats.total_amount)}万元 + + + 均价: {dayStats.avg_price?.toFixed(2) || '-'}元 + + - {dayStats.deals && dayStats.deals.length > 0 && ( - - - - - - - - - - - - - {dayStats.deals.map((deal, i) => ( - 0 && ( + +
买方营业部卖方营业部 - 成交价 - - 成交量(万股) - - 成交额(万元) -
+ + + + + + + + + + + {dayStats.deals.map((deal, i) => ( + + - - - - - - ))} - -
买方营业部卖方营业部 + 成交价 + + 成交量(万股) + + 成交额(万元) +
- - - {deal.buyer_dept || '-'} - - - - {deal.seller_dept || '-'} - - - {deal.price?.toFixed(2) || '-'} - - {deal.volume?.toFixed(2) || '-'} - - {deal.amount?.toFixed(2) || '-'} -
-
- )} -
- ))} -
- ) : ( -
- 暂无大宗交易数据 -
- )} -
-
+ + {deal.buyer_dept || '-'} + + + + + {deal.seller_dept || '-'} + + + + {deal.price?.toFixed(2) || '-'} + + + {deal.volume?.toFixed(2) || '-'} + + + {deal.amount?.toFixed(2) || '-'} + + + ))} + + + + )} +
+ ))} + + ) : ( + + )} + ); }; -export default BigDealPanel; +export default memo(BigDealPanel); diff --git a/src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx b/src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx index 08a13de5..a64db755 100644 --- a/src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx +++ b/src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx @@ -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 = ({ fundingData }) => { return ( {/* 图表卡片 */} - + {fundingData.length > 0 && ( - = ({ fundingData }) => { {/* 融资数据 */} - - - - 融资数据 - - - - - {fundingData - .slice(-5) - .reverse() - .map((item, idx) => ( - - - - {item.date} + + + {fundingData + .slice(-5) + .reverse() + .map((item, idx) => ( + + + + {item.date} + + + + {formatNumber(item.financing.balance)} - - - {formatNumber(item.financing.balance)} - - - 买入{formatNumber(item.financing.buy)} / 偿还 - {formatNumber(item.financing.repay)} - - - - - ))} - - - + + 买入{formatNumber(item.financing.buy)} / 偿还 + {formatNumber(item.financing.repay)} + + + + + ))} + + {/* 融券数据 */} - - - - 融券数据 - - - - - {fundingData - .slice(-5) - .reverse() - .map((item, idx) => ( - - - - {item.date} + + + {fundingData + .slice(-5) + .reverse() + .map((item, idx) => ( + + + + {item.date} + + + + {formatNumber(item.securities.balance)} - - - {formatNumber(item.securities.balance)} - - - 卖出{formatNumber(item.securities.sell)} / 偿还 - {formatNumber(item.securities.repay)} - - - - - ))} - - - + + 卖出{formatNumber(item.securities.sell)} / 偿还 + {formatNumber(item.securities.repay)} + + + + + ))} + + ); }; -export default FundingPanel; +export default memo(FundingPanel); diff --git a/src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx b/src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx index 9e189e10..99dc48f2 100644 --- a/src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx +++ b/src/views/Company/components/MarketDataView/components/panels/PledgePanel.tsx @@ -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 = ({ pledgeData }) => { return ( {/* 图表卡片 */} - + {pledgeData.length > 0 && ( - = ({ pledgeData }) => { {/* 质押明细表格 */} - - - - 质押明细 - - - + @@ -132,10 +112,9 @@ const PledgePanel: React.FC = ({ pledgeData }) => {
-
-
+
); }; -export default PledgePanel; +export default memo(PledgePanel); diff --git a/src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx b/src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx index 4a0afe76..95270f54 100644 --- a/src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx +++ b/src/views/Company/components/MarketDataView/components/panels/UnusualPanel.tsx @@ -1,77 +1,28 @@ // 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 ( - - {children} - - ); -}; - const UnusualPanel: React.FC = ({ unusualData }) => { return ( - - - - 龙虎榜数据 - - - - {unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? ( + + {unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? ( {unusualData.grouped_data.map((dayData, idx) => ( = ({ unusualData }) => { ))} ) : ( -
- 暂无龙虎榜数据 -
+ )} -
-
+ ); }; -export default UnusualPanel; +export default memo(UnusualPanel); diff --git a/src/views/Company/components/MarketDataView/components/shared/DarkGoldBadge.tsx b/src/views/Company/components/MarketDataView/components/shared/DarkGoldBadge.tsx new file mode 100644 index 00000000..9e7b9f35 --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/shared/DarkGoldBadge.tsx @@ -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 { + children: React.ReactNode; + variant?: DarkGoldBadgeVariant; +} + +// 徽章颜色配置 +const BADGE_COLORS: Record = { + 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 = ({ + children, + variant = 'gold', + ...boxProps +}) => { + const colors = BADGE_COLORS[variant]; + + return ( + + {children} + + ); +}; + +export default memo(DarkGoldBadge); diff --git a/src/views/Company/components/MarketDataView/components/shared/DarkGoldCard.tsx b/src/views/Company/components/MarketDataView/components/shared/DarkGoldCard.tsx new file mode 100644 index 00000000..4d04153a --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/shared/DarkGoldCard.tsx @@ -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 { + /** 卡片标题 */ + title?: string; + /** 标题颜色 */ + titleColor?: string; + /** 是否显示标题区域 */ + showHeader?: boolean; + /** 内容区 padding */ + contentPadding?: number | string; + children: React.ReactNode; +} + +/** + * 黑金主题卡片组件 + * 统一的卡片样式,包含标题区和内容区 + */ +const DarkGoldCard: React.FC = ({ + title, + titleColor = darkGoldTheme.gold, + showHeader = true, + contentPadding = 4, + children, + ...boxProps +}) => { + return ( + + {showHeader && title && ( + + + {title} + + + )} + {children} + + ); +}; + +export default memo(DarkGoldCard); diff --git a/src/views/Company/components/MarketDataView/components/shared/EmptyState.tsx b/src/views/Company/components/MarketDataView/components/shared/EmptyState.tsx new file mode 100644 index 00000000..bd22006a --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/shared/EmptyState.tsx @@ -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 { + message?: string; +} + +/** + * 空状态组件 + * 数据为空时显示的占位组件 + */ +const EmptyState: React.FC = ({ + message = '暂无数据', + h = '200px', + ...centerProps +}) => { + return ( +
+ {message} +
+ ); +}; + +export default memo(EmptyState); diff --git a/src/views/Company/components/MarketDataView/components/shared/index.ts b/src/views/Company/components/MarketDataView/components/shared/index.ts new file mode 100644 index 00000000..e2316013 --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/shared/index.ts @@ -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'; diff --git a/src/views/Company/components/MarketDataView/components/shared/styles.ts b/src/views/Company/components/MarketDataView/components/shared/styles.ts new file mode 100644 index 00000000..76d48e37 --- /dev/null +++ b/src/views/Company/components/MarketDataView/components/shared/styles.ts @@ -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)', +}; diff --git a/src/views/Company/components/MarketDataView/hooks/useMarketData.ts b/src/views/Company/components/MarketDataView/hooks/useMarketData.ts index 33cd6971..cfff6891 100644 --- a/src/views/Company/components/MarketDataView/hooks/useMarketData.ts +++ b/src/views/Company/components/MarketDataView/hooks/useMarketData.ts @@ -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(null); + const tabDataControllerRef = useRef(null); + const minuteDataControllerRef = useRef(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,9 +148,14 @@ export const useMarketData = ( loadRiseAnalysis(loadedTradeData); } } catch (error) { + // 取消请求不作为错误处理 + if (isCancelError(error)) return; logger.error('useMarketData', 'loadCoreData', error, { stockCode, period }); } finally { - setLoading(false); + // 只有当前请求没有被取消时才设置 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,7 +263,10 @@ export const useMarketData = ( type: 'minute', }); } finally { - setMinuteLoading(false); + // 只有当前请求没有被取消时才设置 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, diff --git a/src/views/Company/components/MarketDataView/index.tsx b/src/views/Company/components/MarketDataView/index.tsx index 3096e405..780fbade 100644 --- a/src/views/Company/components/MarketDataView/index.tsx +++ b/src/views/Company/components/MarketDataView/index.tsx @@ -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 = ({ 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 = ({ stockCode: propStockCod ); }; -export default MarketDataView; +export default memo(MarketDataView); diff --git a/src/views/Company/components/MarketDataView/services/marketService.ts b/src/views/Company/components/MarketDataView/services/marketService.ts index c0a805b0..b9e0e12e 100644 --- a/src/views/Company/components/MarketDataView/services/marketService.ts +++ b/src/views/Company/components/MarketDataView/services/marketService.ts @@ -23,6 +23,13 @@ interface ApiResponse { message?: string; } +/** + * 请求选项 + */ +interface RequestOptions { + signal?: AbortSignal; +} + /** * 市场数据服务 */ @@ -30,9 +37,13 @@ export const marketService = { /** * 获取市场概览数据 * @param stockCode 股票代码 + * @param options 请求选项 */ - async getMarketSummary(stockCode: string): Promise> { - const { data } = await axios.get>(`/api/market/summary/${stockCode}`); + async getMarketSummary(stockCode: string, options?: RequestOptions): Promise> { + const { data } = await axios.get>( + `/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> { - const { data } = await axios.get>(`/api/market/trade/${stockCode}?days=${days}`); + async getTradeData(stockCode: string, days: number = 60, options?: RequestOptions): Promise> { + const { data } = await axios.get>( + `/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> { - const { data } = await axios.get>(`/api/market/funding/${stockCode}?days=${days}`); + async getFundingData(stockCode: string, days: number = 30, options?: RequestOptions): Promise> { + const { data } = await axios.get>( + `/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 { - const { data } = await axios.get(`/api/market/bigdeal/${stockCode}?days=${days}`); + async getBigDealData(stockCode: string, days: number = 30, options?: RequestOptions): Promise { + const { data } = await axios.get( + `/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 { - const { data } = await axios.get(`/api/market/unusual/${stockCode}?days=${days}`); + async getUnusualData(stockCode: string, days: number = 30, options?: RequestOptions): Promise { + const { data } = await axios.get( + `/api/market/unusual/${stockCode}?days=${days}`, + { signal: options?.signal } + ); return data; }, /** * 获取股权质押数据 * @param stockCode 股票代码 + * @param options 请求选项 */ - async getPledgeData(stockCode: string): Promise> { - const { data } = await axios.get>(`/api/market/pledge/${stockCode}`); + async getPledgeData(stockCode: string, options?: RequestOptions): Promise> { + const { data } = await axios.get>( + `/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> { let url = `/api/market/rise-analysis/${stockCode}`; if (startDate && endDate) { url += `?start_date=${startDate}&end_date=${endDate}`; } - const { data } = await axios.get>(url); + const { data } = await axios.get>(url, { signal: options?.signal }); return data; }, /** * 获取分钟K线数据 * @param stockCode 股票代码 + * @param options 请求选项 */ - async getMinuteData(stockCode: string): Promise { + async getMinuteData(stockCode: string, options?: RequestOptions): Promise { try { - const { data } = await axios.get(`/api/stock/${stockCode}/latest-minute`); + const { data } = await axios.get( + `/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 { diff --git a/src/views/Company/components/MarketDataView/utils/chartOptions.ts b/src/views/Company/components/MarketDataView/utils/chartOptions.ts index 5efa9a66..652bf3e1 100644 --- a/src/views/Company/components/MarketDataView/utils/chartOptions.ts +++ b/src/views/Company/components/MarketDataView/utils/chartOptions.ts @@ -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',