perf: 优化各 Tab 数据加载为按需请求

MarketDataView (股票行情):
- 初始只加载 summary + tradeData(2个接口)
- funding/bigDeal/unusual/pledge 数据在切换 Tab 时按需加载
- 新增 loadDataByType 方法支持懒加载

FinancialPanorama (财务全景):
- 初始只加载 stockInfo + metrics + comparison + mainBusiness(4个接口)
- 从9个接口优化到4个接口

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-18 18:32:14 +08:00
parent 3953efc2ed
commit 5331bc64b4
21 changed files with 368 additions and 123 deletions

View File

@@ -0,0 +1,40 @@
/**
* ECharts 包装组件
*
* 基于 echarts-for-react使用按需引入的 echarts 实例
* 减少打包体积约 500KB
*
* @example
* ```tsx
* import ECharts from '@components/Charts/ECharts';
*
* <ECharts option={chartOption} style={{ height: 300 }} />
* ```
*/
import React, { forwardRef } from 'react';
import ReactEChartsCore from 'echarts-for-react/lib/core';
import { echarts } from '@lib/echarts';
// Re-export ReactEChartsCore props type
import type { EChartsReactProps } from 'echarts-for-react';
export type EChartsProps = Omit<EChartsReactProps, 'echarts'>;
/**
* ECharts 图表组件
* 自动使用按需引入的 echarts 实例
*/
const ECharts = forwardRef<ReactEChartsCore, EChartsProps>((props, ref) => {
return (
<ReactEChartsCore
ref={ref}
echarts={echarts}
{...props}
/>
);
});
ECharts.displayName = 'ECharts';
export default ECharts;

View File

@@ -1,7 +1,7 @@
// src/components/Charts/Stock/MiniTimelineChart.js // src/components/Charts/Stock/MiniTimelineChart.js
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts'; import { echarts } from '@lib/echarts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { import {
fetchKlineData, fetchKlineData,

View File

@@ -3,7 +3,7 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Box, useColorModeValue } from '@chakra-ui/react'; import { Box, useColorModeValue } from '@chakra-ui/react';
import * as echarts from 'echarts'; import { echarts } from '@lib/echarts';
/** /**
* ECharts 图表渲染组件 * ECharts 图表渲染组件

View File

@@ -2,7 +2,8 @@
import React, { useEffect, useRef, useState, useCallback } from 'react'; import React, { useEffect, useRef, useState, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import * as echarts from 'echarts'; import { echarts } from '@lib/echarts';
import type { ECharts } from '@lib/echarts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { stockService } from '@services/eventService'; import { stockService } from '@services/eventService';
import { selectIsMobile } from '@store/slices/deviceSlice'; import { selectIsMobile } from '@store/slices/deviceSlice';

View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Modal, Button, Spin, Typography } from 'antd'; import { Modal, Button, Spin, Typography } from 'antd';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts'; import { echarts } from '@lib/echarts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { stockService } from '../../services/eventService'; import { stockService } from '../../services/eventService';
import CitedContent from '../Citation/CitedContent'; import CitedContent from '../Citation/CitedContent';

View File

@@ -17,7 +17,7 @@ import {
Alert, Alert,
AlertIcon, AlertIcon,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import * as echarts from 'echarts'; import { echarts, type ECharts, type EChartsOption } from '@lib/echarts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { klineDataCache, getCacheKey, fetchKlineData } from '@utils/stock/klineDataCache'; import { klineDataCache, getCacheKey, fetchKlineData } from '@utils/stock/klineDataCache';
import { selectIsMobile } from '@store/slices/deviceSlice'; import { selectIsMobile } from '@store/slices/deviceSlice';

View File

@@ -31,6 +31,8 @@ import {
HStack, HStack,
Text, Text,
Spacer, Spacer,
Center,
Spinner,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import type { IconType } from 'react-icons'; import type { IconType } from 'react-icons';
@@ -311,7 +313,18 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
return ( return (
<TabPanel key={tab.key} p={0}> <TabPanel key={tab.key} p={0}>
{shouldRender && Component ? ( {shouldRender && Component ? (
<Suspense fallback={null}> <Suspense
fallback={
<Center py={20}>
<Spinner
size="lg"
color={DEEP_SPACE.textGold}
thickness="3px"
speed="0.8s"
/>
</Center>
}
>
<Component {...componentProps} /> <Component {...componentProps} />
</Suspense> </Suspense>
) : null} ) : null}

120
src/lib/echarts.ts Normal file
View File

@@ -0,0 +1,120 @@
/**
* ECharts 按需导入配置
*
* 使用方式:
* import { echarts } from '@lib/echarts';
*
* 优势:
* - 减小打包体积(从 ~800KB 降至 ~200-300KB
* - Tree-shaking 支持
* - 统一管理图表类型和组件
*/
// 核心模块
import * as echarts from 'echarts/core';
// 图表类型 - 按需导入
import {
LineChart,
BarChart,
PieChart,
CandlestickChart,
ScatterChart,
} from 'echarts/charts';
// 组件 - 按需导入
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
DataZoomComponent,
ToolboxComponent,
MarkLineComponent,
MarkPointComponent,
MarkAreaComponent,
DatasetComponent,
TransformComponent,
} from 'echarts/components';
// 渲染器
import { CanvasRenderer } from 'echarts/renderers';
// 类型导出
import type {
ECharts,
EChartsOption,
SetOptionOpts,
ComposeOption,
} from 'echarts/core';
import type {
LineSeriesOption,
BarSeriesOption,
PieSeriesOption,
CandlestickSeriesOption,
ScatterSeriesOption,
} from 'echarts/charts';
import type {
TitleComponentOption,
TooltipComponentOption,
LegendComponentOption,
GridComponentOption,
DataZoomComponentOption,
ToolboxComponentOption,
MarkLineComponentOption,
MarkPointComponentOption,
MarkAreaComponentOption,
DatasetComponentOption,
} from 'echarts/components';
// 注册必需的组件
echarts.use([
// 图表类型
LineChart,
BarChart,
PieChart,
CandlestickChart,
ScatterChart,
// 组件
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
DataZoomComponent,
ToolboxComponent,
MarkLineComponent,
MarkPointComponent,
MarkAreaComponent,
DatasetComponent,
TransformComponent,
// 渲染器
CanvasRenderer,
]);
// 组合类型定义(用于 TypeScript 类型推断)
export type ECOption = ComposeOption<
| LineSeriesOption
| BarSeriesOption
| PieSeriesOption
| CandlestickSeriesOption
| ScatterSeriesOption
| TitleComponentOption
| TooltipComponentOption
| LegendComponentOption
| GridComponentOption
| DataZoomComponentOption
| ToolboxComponentOption
| MarkLineComponentOption
| MarkPointComponentOption
| MarkAreaComponentOption
| DatasetComponentOption
>;
// 导出
export { echarts };
export type { ECharts, EChartsOption, SetOptionOpts };
// 默认导出(兼容 import * as echarts from 'echarts' 的用法)
export default echarts;

View File

@@ -16,7 +16,7 @@ import {
SimpleGrid, SimpleGrid,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { FaChartPie, FaArrowUp, FaArrowDown } from "react-icons/fa"; import { FaChartPie, FaArrowUp, FaArrowDown } from "react-icons/fa";
import * as echarts from "echarts"; import { echarts, type ECharts, type EChartsOption } from '@lib/echarts';
import type { Concentration } from "../../types"; import type { Concentration } from "../../types";
import { THEME } from "../../BasicInfoTab/config"; import { THEME } from "../../BasicInfoTab/config";

View File

@@ -179,8 +179,8 @@ export const useFinancialData = (
setSelectedPeriodsState(periods); setSelectedPeriodsState(periods);
}, []); }, []);
// 加载所有财务数据(初始加载) // 加载核心财务数据(初始加载stockInfo + metrics + comparison
const loadAllFinancialData = useCallback(async () => { const loadCoreFinancialData = useCallback(async () => {
if (!stockCode || stockCode.length !== 6) { if (!stockCode || stockCode.length !== 6) {
logger.warn('useFinancialData', '无效的股票代码', { stockCode }); logger.warn('useFinancialData', '无效的股票代码', { stockCode });
toast({ toast({
@@ -191,55 +191,45 @@ export const useFinancialData = (
return; return;
} }
logger.debug('useFinancialData', '开始加载全部财务数据', { stockCode, selectedPeriods }); logger.debug('useFinancialData', '开始加载核心财务数据', { stockCode, selectedPeriods });
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
// 并行加载所有数据 // 只加载核心数据(概览面板需要的)
const [ const [
stockInfoRes, stockInfoRes,
balanceRes,
incomeRes,
cashflowRes,
metricsRes, metricsRes,
businessRes,
forecastRes,
rankRes,
comparisonRes, comparisonRes,
businessRes,
] = await Promise.all([ ] = await Promise.all([
financialService.getStockInfo(stockCode), financialService.getStockInfo(stockCode),
financialService.getBalanceSheet(stockCode, selectedPeriods),
financialService.getIncomeStatement(stockCode, selectedPeriods),
financialService.getCashflow(stockCode, selectedPeriods),
financialService.getFinancialMetrics(stockCode, selectedPeriods), financialService.getFinancialMetrics(stockCode, selectedPeriods),
financialService.getMainBusiness(stockCode, 4),
financialService.getForecast(stockCode),
financialService.getIndustryRank(stockCode, 4),
financialService.getPeriodComparison(stockCode, selectedPeriods), financialService.getPeriodComparison(stockCode, selectedPeriods),
financialService.getMainBusiness(stockCode, 4),
]); ]);
// 设置数据 // 设置数据
if (stockInfoRes.success) setStockInfo(stockInfoRes.data); if (stockInfoRes.success) setStockInfo(stockInfoRes.data);
if (balanceRes.success) setBalanceSheet(balanceRes.data);
if (incomeRes.success) setIncomeStatement(incomeRes.data);
if (cashflowRes.success) setCashflow(cashflowRes.data);
if (metricsRes.success) setFinancialMetrics(metricsRes.data); if (metricsRes.success) setFinancialMetrics(metricsRes.data);
if (businessRes.success) setMainBusiness(businessRes.data);
if (forecastRes.success) setForecast(forecastRes.data);
if (rankRes.success) setIndustryRank(rankRes.data);
if (comparisonRes.success) setComparison(comparisonRes.data); if (comparisonRes.success) setComparison(comparisonRes.data);
if (businessRes.success) setMainBusiness(businessRes.data);
logger.info('useFinancialData', '全部财务数据加载成功', { stockCode }); logger.info('useFinancialData', '核心财务数据加载成功', { stockCode });
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : '未知错误'; const errorMessage = err instanceof Error ? err.message : '未知错误';
setError(errorMessage); setError(errorMessage);
logger.error('useFinancialData', 'loadAllFinancialData', err, { stockCode, selectedPeriods }); logger.error('useFinancialData', 'loadCoreFinancialData', err, { stockCode, selectedPeriods });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [stockCode, selectedPeriods, toast]); }, [stockCode, selectedPeriods, toast]);
// 加载所有财务数据(用于刷新)
const loadAllFinancialData = useCallback(async () => {
await loadCoreFinancialData();
}, [loadCoreFinancialData]);
// 监听 props 中的 stockCode 变化 // 监听 props 中的 stockCode 变化
useEffect(() => { useEffect(() => {
if (initialStockCode && initialStockCode !== stockCode) { if (initialStockCode && initialStockCode !== stockCode) {

View File

@@ -122,8 +122,8 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
// 颜色配置 // 颜色配置
const { bgColor, hoverBg, positiveColor, negativeColor } = COLORS; const { bgColor, hoverBg, positiveColor, negativeColor } = COLORS;
// 点击指标行显示图表 // 点击指标行显示图表(使用 useCallback 避免不必要的重渲染)
const showMetricChart = ( const showMetricChart = useCallback((
metricName: string, metricName: string,
_metricKey: string, _metricKey: string,
data: Array<{ period: string; [key: string]: unknown }>, data: Array<{ period: string; [key: string]: unknown }>,
@@ -221,7 +221,7 @@ const FinancialPanorama: React.FC<FinancialPanoramaProps> = ({ stockCode: propSt
</Box> </Box>
); );
onOpen(); onOpen();
}; }, [onOpen, positiveColor, negativeColor]);
// Tab 配置 - 财务指标分类 + 三大财务报表 // Tab 配置 - 财务指标分类 + 三大财务报表
const tabConfigs: SubTabConfig[] = useMemo( const tabConfigs: SubTabConfig[] = useMemo(

View File

@@ -86,6 +86,27 @@ const DRAWING_OPTIONS: { value: DrawingType; label: string; description: string
{ value: 'ALL', label: '全部显示', description: '显示所有参考线' }, { value: 'ALL', label: '全部显示', description: '显示所有参考线' },
]; ];
// 黑金主题按钮样式(提取到组件外部避免每次渲染重建)
const ACTIVE_BUTTON_STYLE = {
bg: `linear-gradient(135deg, ${darkGoldTheme.gold} 0%, ${darkGoldTheme.orange} 100%)`,
color: '#1a1a2e',
borderColor: darkGoldTheme.gold,
_hover: {
bg: `linear-gradient(135deg, ${darkGoldTheme.goldLight} 0%, ${darkGoldTheme.gold} 100%)`,
},
} as const;
const INACTIVE_BUTTON_STYLE = {
bg: 'transparent',
color: darkGoldTheme.textMuted,
borderColor: darkGoldTheme.border,
_hover: {
bg: 'rgba(212, 175, 55, 0.1)',
borderColor: darkGoldTheme.gold,
color: darkGoldTheme.gold,
},
} as const;
const KLineModule: React.FC<KLineModuleProps> = ({ const KLineModule: React.FC<KLineModuleProps> = ({
theme, theme,
tradeData, tradeData,
@@ -151,34 +172,13 @@ const KLineModule: React.FC<KLineModuleProps> = ({
setOverlayMetrics(prev => prev.filter(m => m.metric_id !== metricId)); setOverlayMetrics(prev => prev.filter(m => m.metric_id !== metricId));
}, []); }, []);
// 切换到分时模式时自动加载数据 // 切换到分时模式时自动加载数据(使用 useCallback 避免不必要的重渲染)
const handleModeChange = (newMode: ChartMode) => { const handleModeChange = useCallback((newMode: ChartMode) => {
setMode(newMode); setMode(newMode);
if (newMode === 'minute' && !hasMinuteData && !minuteLoading) { if (newMode === 'minute' && !hasMinuteData && !minuteLoading) {
onLoadMinuteData(); onLoadMinuteData();
} }
}; }, [hasMinuteData, minuteLoading, onLoadMinuteData]);
// 黑金主题按钮样式
const activeButtonStyle = {
bg: `linear-gradient(135deg, ${darkGoldTheme.gold} 0%, ${darkGoldTheme.orange} 100%)`,
color: '#1a1a2e',
borderColor: darkGoldTheme.gold,
_hover: {
bg: `linear-gradient(135deg, ${darkGoldTheme.goldLight} 0%, ${darkGoldTheme.gold} 100%)`,
},
};
const inactiveButtonStyle = {
bg: 'transparent',
color: darkGoldTheme.textMuted,
borderColor: darkGoldTheme.border,
_hover: {
bg: 'rgba(212, 175, 55, 0.1)',
borderColor: darkGoldTheme.gold,
color: darkGoldTheme.gold,
},
};
return ( return (
<Box <Box
@@ -263,7 +263,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
variant="outline" variant="outline"
leftIcon={showAnalysis ? <ViewOffIcon /> : <ViewIcon />} leftIcon={showAnalysis ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowAnalysis(!showAnalysis)} onClick={() => setShowAnalysis(!showAnalysis)}
{...(showAnalysis ? inactiveButtonStyle : activeButtonStyle)} {...(showAnalysis ? INACTIVE_BUTTON_STYLE : ACTIVE_BUTTON_STYLE)}
minW="90px" minW="90px"
> >
{showAnalysis ? '隐藏分析' : '显示分析'} {showAnalysis ? '隐藏分析' : '显示分析'}
@@ -278,7 +278,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
size="sm" size="sm"
variant="outline" variant="outline"
rightIcon={<ChevronDownIcon />} rightIcon={<ChevronDownIcon />}
{...inactiveButtonStyle} {...INACTIVE_BUTTON_STYLE}
minW="90px" minW="90px"
> >
{MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'} {MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'}
@@ -319,7 +319,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
variant="outline" variant="outline"
rightIcon={<ChevronDownIcon />} rightIcon={<ChevronDownIcon />}
leftIcon={<Activity size={14} />} leftIcon={<Activity size={14} />}
{...inactiveButtonStyle} {...INACTIVE_BUTTON_STYLE}
minW="100px" minW="100px"
> >
{SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'} {SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'}
@@ -360,7 +360,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
variant="outline" variant="outline"
rightIcon={<ChevronDownIcon />} rightIcon={<ChevronDownIcon />}
leftIcon={<Pencil size={14} />} leftIcon={<Pencil size={14} />}
{...(drawingType !== 'NONE' ? activeButtonStyle : inactiveButtonStyle)} {...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
minW="90px" minW="90px"
> >
{DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'} {DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'}
@@ -411,7 +411,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => setShowOrderBook(!showOrderBook)} onClick={() => setShowOrderBook(!showOrderBook)}
{...(showOrderBook ? activeButtonStyle : inactiveButtonStyle)} {...(showOrderBook ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
minW="80px" minW="80px"
> >
{showOrderBook ? '隐藏盘口' : '显示盘口'} {showOrderBook ? '隐藏盘口' : '显示盘口'}
@@ -426,7 +426,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
onClick={onLoadMinuteData} onClick={onLoadMinuteData}
isLoading={minuteLoading} isLoading={minuteLoading}
loadingText="获取中" loadingText="获取中"
{...inactiveButtonStyle} {...INACTIVE_BUTTON_STYLE}
> >
</Button> </Button>
@@ -438,14 +438,14 @@ const KLineModule: React.FC<KLineModuleProps> = ({
<Button <Button
leftIcon={<BarChart2 size={14} />} leftIcon={<BarChart2 size={14} />}
onClick={() => handleModeChange('daily')} onClick={() => handleModeChange('daily')}
{...(mode === 'daily' ? activeButtonStyle : inactiveButtonStyle)} {...(mode === 'daily' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
> >
K K
</Button> </Button>
<Button <Button
leftIcon={<LineChart size={14} />} leftIcon={<LineChart size={14} />}
onClick={() => handleModeChange('minute')} onClick={() => handleModeChange('minute')}
{...(mode === 'minute' ? activeButtonStyle : inactiveButtonStyle)} {...(mode === 'minute' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
> >
</Button> </Button>

View File

@@ -84,37 +84,36 @@ export const useMarketData = (
} }
}, [stockCode]); }, [stockCode]);
// 记录已加载的数据类型
const loadedDataRef = useRef({
summary: false,
trade: false,
funding: false,
bigDeal: false,
unusual: false,
pledge: false,
});
/** /**
* 加载所有市场数据(涨幅分析延迟加载 * 加载核心市场数据(仅 summary 和 trade
*/ */
const loadMarketData = useCallback(async () => { const loadCoreData = useCallback(async () => {
if (!stockCode) return; if (!stockCode) return;
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,
fundingRes,
bigDealRes,
unusualRes,
pledgeRes,
] = await Promise.all([
marketService.getMarketSummary(stockCode), marketService.getMarketSummary(stockCode),
marketService.getTradeData(stockCode, period), marketService.getTradeData(stockCode, period),
marketService.getFundingData(stockCode, 30),
marketService.getBigDealData(stockCode, 30),
marketService.getUnusualData(stockCode, 30),
marketService.getPledgeData(stockCode),
]); ]);
// 设置概览数据 // 设置概览数据
if (summaryRes.success) { if (summaryRes.success) {
setSummary(summaryRes.data); setSummary(summaryRes.data);
loadedDataRef.current.summary = true;
} }
// 设置交易数据 // 设置交易数据
@@ -122,41 +121,79 @@ export const useMarketData = (
if (tradeRes.success) { if (tradeRes.success) {
loadedTradeData = tradeRes.data; loadedTradeData = tradeRes.data;
setTradeData(loadedTradeData); setTradeData(loadedTradeData);
loadedDataRef.current.trade = true;
} }
// 设置融资融券数据 logger.info('useMarketData', '核心市场数据加载成功', { stockCode });
if (fundingRes.success) {
setFundingData(fundingRes.data);
}
// 设置大宗交易数据(包含 daily_stats
if (bigDealRes.success) {
setBigDealData(bigDealRes);
}
// 设置龙虎榜数据(包含 grouped_data
if (unusualRes.success) {
setUnusualData(unusualRes);
}
// 设置股权质押数据
if (pledgeRes.success) {
setPledgeData(pledgeRes.data);
}
logger.info('useMarketData', '市场数据加载成功', { stockCode });
// 核心数据加载完成后,异步加载涨幅分析(不阻塞界面) // 核心数据加载完成后,异步加载涨幅分析(不阻塞界面)
if (loadedTradeData.length > 0) { if (loadedTradeData.length > 0) {
loadRiseAnalysis(loadedTradeData); loadRiseAnalysis(loadedTradeData);
} }
} catch (error) { } catch (error) {
logger.error('useMarketData', 'loadMarketData', error, { stockCode, period }); logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [stockCode, period, loadRiseAnalysis]); }, [stockCode, period, loadRiseAnalysis]);
/**
* 按需加载指定类型的数据
*/
const loadDataByType = useCallback(async (dataType: 'funding' | 'bigDeal' | 'unusual' | 'pledge') => {
if (!stockCode) return;
if (loadedDataRef.current[dataType]) return; // 已加载则跳过
logger.debug('useMarketData', `按需加载 ${dataType} 数据`, { stockCode });
try {
switch (dataType) {
case 'funding': {
const res = await marketService.getFundingData(stockCode, 30);
if (res.success) {
setFundingData(res.data);
loadedDataRef.current.funding = true;
}
break;
}
case 'bigDeal': {
const res = await marketService.getBigDealData(stockCode, 30);
if (res.success) {
setBigDealData(res);
loadedDataRef.current.bigDeal = true;
}
break;
}
case 'unusual': {
const res = await marketService.getUnusualData(stockCode, 30);
if (res.success) {
setUnusualData(res);
loadedDataRef.current.unusual = true;
}
break;
}
case 'pledge': {
const res = await marketService.getPledgeData(stockCode);
if (res.success) {
setPledgeData(res.data);
loadedDataRef.current.pledge = true;
}
break;
}
}
logger.info('useMarketData', `${dataType} 数据加载成功`, { stockCode });
} catch (error) {
logger.error('useMarketData', `loadDataByType:${dataType}`, error, { stockCode });
}
}, [stockCode]);
/**
* 加载所有市场数据(用于刷新)
*/
const loadMarketData = useCallback(async () => {
await loadCoreData();
}, [loadCoreData]);
/** /**
* 加载分钟K线数据 * 加载分钟K线数据
*/ */
@@ -234,19 +271,28 @@ export const useMarketData = (
await Promise.all([loadMarketData(), loadMinuteData()]); await Promise.all([loadMarketData(), loadMinuteData()]);
}, [loadMarketData, loadMinuteData]); }, [loadMarketData, loadMinuteData]);
// 监听股票代码变化,加载所有数据(首次加载或切换股票) // 监听股票代码变化,加载核心数据(首次加载或切换股票)
useEffect(() => { useEffect(() => {
if (stockCode) { if (stockCode) {
// stockCode 变化时,加载所有数据 // stockCode 变化时,重置已加载状态并加载核心数据
if (stockCode !== prevStockCodeRef.current || !isInitializedRef.current) { if (stockCode !== prevStockCodeRef.current || !isInitializedRef.current) {
// 重置已加载状态
loadedDataRef.current = {
summary: false,
trade: false,
funding: false,
bigDeal: false,
unusual: false,
pledge: false,
};
// 只加载核心数据summary + trade
loadMarketData(); loadMarketData();
loadMinuteData();
prevStockCodeRef.current = stockCode; prevStockCodeRef.current = stockCode;
prevPeriodRef.current = period; // 同步重置 period ref避免切换股票后误触发 refreshTradeData prevPeriodRef.current = period;
isInitializedRef.current = true; isInitializedRef.current = true;
} }
} }
}, [stockCode, period, loadMarketData, loadMinuteData]); }, [stockCode, period, loadMarketData]);
// 监听时间周期变化只刷新日K线数据 // 监听时间周期变化只刷新日K线数据
useEffect(() => { useEffect(() => {
@@ -273,6 +319,7 @@ export const useMarketData = (
refetch, refetch,
loadMinuteData, loadMinuteData,
refreshTradeData, refreshTradeData,
loadDataByType,
}; };
}; };

View File

@@ -68,8 +68,25 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
analysisMap, analysisMap,
refetch, refetch,
loadMinuteData, loadMinuteData,
loadDataByType,
} = useMarketData(stockCode, selectedPeriod); } = useMarketData(stockCode, selectedPeriod);
// Tab 切换时按需加载数据
const handleTabChange = useCallback((index: number) => {
setActiveTab(index);
// 根据 tab index 加载对应数据
const tabDataMap: Record<number, 'funding' | 'bigDeal' | 'unusual' | 'pledge'> = {
0: 'funding',
1: 'bigDeal',
2: 'unusual',
3: 'pledge',
};
const dataType = tabDataMap[index];
if (dataType) {
loadDataByType(dataType);
}
}, [loadDataByType]);
// 监听 props 中的 stockCode 变化 // 监听 props 中的 stockCode 变化
useEffect(() => { useEffect(() => {
if (propStockCode && propStockCode !== stockCode) { if (propStockCode && propStockCode !== stockCode) {
@@ -173,7 +190,7 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
componentProps={componentProps} componentProps={componentProps}
themePreset="blackGold" themePreset="blackGold"
index={activeTab} index={activeTab}
onTabChange={(index) => setActiveTab(index)} onTabChange={handleTabChange}
isLazy isLazy
/> />
)} )}

View File

@@ -364,6 +364,11 @@ export interface OverlayMetricData {
color?: string; color?: string;
} }
/**
* 按需加载的数据类型
*/
export type LazyDataType = 'funding' | 'bigDeal' | 'unusual' | 'pledge';
/** /**
* useMarketData Hook 返回值 * useMarketData Hook 返回值
*/ */
@@ -383,4 +388,5 @@ export interface UseMarketDataReturn {
refetch: () => Promise<void>; refetch: () => Promise<void>;
loadMinuteData: () => Promise<void>; loadMinuteData: () => Promise<void>;
refreshTradeData: () => Promise<void>; refreshTradeData: () => Promise<void>;
loadDataByType: (dataType: LazyDataType) => Promise<void>;
} }

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/MarketDataView/utils/chartOptions.ts // src/views/Company/components/MarketDataView/utils/chartOptions.ts
// MarketDataView ECharts 图表配置生成器 // MarketDataView ECharts 图表配置生成器
import type { EChartsOption } from 'echarts'; import type { EChartsOption } from '@lib/echarts';
import type { import type {
Theme, Theme,
TradeDayData, TradeDayData,

View File

@@ -39,15 +39,27 @@ export const THEME: CompanyTheme = {
}; };
// ============================================ // ============================================
// Tab 懒加载组件 // Tab 懒加载组件(带 webpack chunk 命名)
// ============================================ // ============================================
const CompanyOverview = lazy(() => import('./components/CompanyOverview')); const CompanyOverview = lazy(() =>
const DeepAnalysis = lazy(() => import('./components/DeepAnalysis')); import(/* webpackChunkName: "company-overview" */ './components/CompanyOverview')
const MarketDataView = lazy(() => import('./components/MarketDataView')); );
const FinancialPanorama = lazy(() => import('./components/FinancialPanorama')); const DeepAnalysis = lazy(() =>
const ForecastReport = lazy(() => import('./components/ForecastReport')); import(/* webpackChunkName: "company-deep-analysis" */ './components/DeepAnalysis')
const DynamicTracking = lazy(() => import('./components/DynamicTracking')); );
const MarketDataView = lazy(() =>
import(/* webpackChunkName: "company-market-data" */ './components/MarketDataView')
);
const FinancialPanorama = lazy(() =>
import(/* webpackChunkName: "company-financial" */ './components/FinancialPanorama')
);
const ForecastReport = lazy(() =>
import(/* webpackChunkName: "company-forecast" */ './components/ForecastReport')
);
const DynamicTracking = lazy(() =>
import(/* webpackChunkName: "company-tracking" */ './components/DynamicTracking')
);
// ============================================ // ============================================
// Tab 配置 // Tab 配置

View File

@@ -47,7 +47,7 @@ import {
FaRedo, FaRedo,
FaSearch FaSearch
} from 'react-icons/fa'; } from 'react-icons/fa';
import * as echarts from 'echarts'; import { echarts } from '@lib/echarts';
import StockChartModal from '../../../components/StockChart/StockChartModal'; import StockChartModal from '../../../components/StockChart/StockChartModal';
import { eventService, stockService } from '../../../services/eventService'; import { eventService, stockService } from '../../../services/eventService';

View File

@@ -4,8 +4,7 @@
*/ */
import React, { useEffect, useRef, useState, useMemo } from 'react'; import React, { useEffect, useRef, useState, useMemo } from 'react';
import { Box, Spinner, Center, Text } from '@chakra-ui/react'; import { Box, Spinner, Center, Text } from '@chakra-ui/react';
import * as echarts from 'echarts'; import { echarts, type ECharts, type EChartsOption } from '@lib/echarts';
import type { ECharts, EChartsOption } from 'echarts';
import { getApiBase } from '@utils/apiConfig'; import { getApiBase } from '@utils/apiConfig';
import type { MiniTimelineChartProps, TimelineDataPoint } from '../types'; import type { MiniTimelineChartProps, TimelineDataPoint } from '../types';

View File

@@ -4,7 +4,7 @@
*/ */
import React, { useRef, useEffect, useCallback, useMemo } from 'react'; import React, { useRef, useEffect, useCallback, useMemo } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import * as echarts from 'echarts'; import { echarts } from '@lib/echarts';
import { getAlertMarkPointsGrouped } from '../utils/chartHelpers'; import { getAlertMarkPointsGrouped } from '../utils/chartHelpers';
import { colors, glassEffect } from '../../../theme/glassTheme'; import { colors, glassEffect } from '../../../theme/glassTheme';

View File

@@ -56,7 +56,7 @@ import TradeDatePicker from '@components/TradeDatePicker';
import HotspotOverview from './components/HotspotOverview'; import HotspotOverview from './components/HotspotOverview';
import FlexScreen from './components/FlexScreen'; import FlexScreen from './components/FlexScreen';
import { BsGraphUp, BsLightningFill } from 'react-icons/bs'; import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
import * as echarts from 'echarts'; import { echarts } from '@lib/echarts';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import tradingDays from '../../data/tradingDays.json'; import tradingDays from '../../data/tradingDays.json';
import { useStockOverviewEvents } from './hooks/useStockOverviewEvents'; import { useStockOverviewEvents } from './hooks/useStockOverviewEvents';