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:
@@ -86,6 +86,27 @@ const DRAWING_OPTIONS: { value: DrawingType; label: string; description: string
|
||||
{ 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> = ({
|
||||
theme,
|
||||
tradeData,
|
||||
@@ -151,34 +172,13 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
setOverlayMetrics(prev => prev.filter(m => m.metric_id !== metricId));
|
||||
}, []);
|
||||
|
||||
// 切换到分时模式时自动加载数据
|
||||
const handleModeChange = (newMode: ChartMode) => {
|
||||
// 切换到分时模式时自动加载数据(使用 useCallback 避免不必要的重渲染)
|
||||
const handleModeChange = useCallback((newMode: ChartMode) => {
|
||||
setMode(newMode);
|
||||
if (newMode === 'minute' && !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,
|
||||
},
|
||||
};
|
||||
}, [hasMinuteData, minuteLoading, onLoadMinuteData]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -263,7 +263,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
variant="outline"
|
||||
leftIcon={showAnalysis ? <ViewOffIcon /> : <ViewIcon />}
|
||||
onClick={() => setShowAnalysis(!showAnalysis)}
|
||||
{...(showAnalysis ? inactiveButtonStyle : activeButtonStyle)}
|
||||
{...(showAnalysis ? INACTIVE_BUTTON_STYLE : ACTIVE_BUTTON_STYLE)}
|
||||
minW="90px"
|
||||
>
|
||||
{showAnalysis ? '隐藏分析' : '显示分析'}
|
||||
@@ -278,7 +278,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
{...inactiveButtonStyle}
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
minW="90px"
|
||||
>
|
||||
{MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'}
|
||||
@@ -319,7 +319,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
leftIcon={<Activity size={14} />}
|
||||
{...inactiveButtonStyle}
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
minW="100px"
|
||||
>
|
||||
{SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'}
|
||||
@@ -360,7 +360,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
leftIcon={<Pencil size={14} />}
|
||||
{...(drawingType !== 'NONE' ? activeButtonStyle : inactiveButtonStyle)}
|
||||
{...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
minW="90px"
|
||||
>
|
||||
{DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'}
|
||||
@@ -411,7 +411,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowOrderBook(!showOrderBook)}
|
||||
{...(showOrderBook ? activeButtonStyle : inactiveButtonStyle)}
|
||||
{...(showOrderBook ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
minW="80px"
|
||||
>
|
||||
{showOrderBook ? '隐藏盘口' : '显示盘口'}
|
||||
@@ -426,7 +426,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
onClick={onLoadMinuteData}
|
||||
isLoading={minuteLoading}
|
||||
loadingText="获取中"
|
||||
{...inactiveButtonStyle}
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
@@ -438,14 +438,14 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
<Button
|
||||
leftIcon={<BarChart2 size={14} />}
|
||||
onClick={() => handleModeChange('daily')}
|
||||
{...(mode === 'daily' ? activeButtonStyle : inactiveButtonStyle)}
|
||||
{...(mode === 'daily' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
>
|
||||
日K
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<LineChart size={14} />}
|
||||
onClick={() => handleModeChange('minute')}
|
||||
{...(mode === 'minute' ? activeButtonStyle : inactiveButtonStyle)}
|
||||
{...(mode === 'minute' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
>
|
||||
分时
|
||||
</Button>
|
||||
|
||||
@@ -84,37 +84,36 @@ export const useMarketData = (
|
||||
}
|
||||
}, [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;
|
||||
|
||||
logger.debug('useMarketData', '开始加载市场数据', { stockCode, period });
|
||||
logger.debug('useMarketData', '开始加载核心市场数据', { stockCode, period });
|
||||
setLoading(true);
|
||||
setAnalysisMap({}); // 清空旧的分析数据
|
||||
|
||||
try {
|
||||
// 先加载核心数据(不含涨幅分析)
|
||||
const [
|
||||
summaryRes,
|
||||
tradeRes,
|
||||
fundingRes,
|
||||
bigDealRes,
|
||||
unusualRes,
|
||||
pledgeRes,
|
||||
] = await Promise.all([
|
||||
const [summaryRes, tradeRes] = await Promise.all([
|
||||
marketService.getMarketSummary(stockCode),
|
||||
marketService.getTradeData(stockCode, period),
|
||||
marketService.getFundingData(stockCode, 30),
|
||||
marketService.getBigDealData(stockCode, 30),
|
||||
marketService.getUnusualData(stockCode, 30),
|
||||
marketService.getPledgeData(stockCode),
|
||||
]);
|
||||
|
||||
// 设置概览数据
|
||||
if (summaryRes.success) {
|
||||
setSummary(summaryRes.data);
|
||||
loadedDataRef.current.summary = true;
|
||||
}
|
||||
|
||||
// 设置交易数据
|
||||
@@ -122,41 +121,79 @@ export const useMarketData = (
|
||||
if (tradeRes.success) {
|
||||
loadedTradeData = tradeRes.data;
|
||||
setTradeData(loadedTradeData);
|
||||
loadedDataRef.current.trade = true;
|
||||
}
|
||||
|
||||
// 设置融资融券数据
|
||||
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 });
|
||||
logger.info('useMarketData', '核心市场数据加载成功', { stockCode });
|
||||
|
||||
// 核心数据加载完成后,异步加载涨幅分析(不阻塞界面)
|
||||
if (loadedTradeData.length > 0) {
|
||||
loadRiseAnalysis(loadedTradeData);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('useMarketData', 'loadMarketData', error, { stockCode, period });
|
||||
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [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线数据
|
||||
*/
|
||||
@@ -234,19 +271,28 @@ export const useMarketData = (
|
||||
await Promise.all([loadMarketData(), loadMinuteData()]);
|
||||
}, [loadMarketData, loadMinuteData]);
|
||||
|
||||
// 监听股票代码变化,加载所有数据(首次加载或切换股票)
|
||||
// 监听股票代码变化,加载核心数据(首次加载或切换股票)
|
||||
useEffect(() => {
|
||||
if (stockCode) {
|
||||
// stockCode 变化时,加载所有数据
|
||||
// stockCode 变化时,重置已加载状态并加载核心数据
|
||||
if (stockCode !== prevStockCodeRef.current || !isInitializedRef.current) {
|
||||
// 重置已加载状态
|
||||
loadedDataRef.current = {
|
||||
summary: false,
|
||||
trade: false,
|
||||
funding: false,
|
||||
bigDeal: false,
|
||||
unusual: false,
|
||||
pledge: false,
|
||||
};
|
||||
// 只加载核心数据(summary + trade)
|
||||
loadMarketData();
|
||||
loadMinuteData();
|
||||
prevStockCodeRef.current = stockCode;
|
||||
prevPeriodRef.current = period; // 同步重置 period ref,避免切换股票后误触发 refreshTradeData
|
||||
prevPeriodRef.current = period;
|
||||
isInitializedRef.current = true;
|
||||
}
|
||||
}
|
||||
}, [stockCode, period, loadMarketData, loadMinuteData]);
|
||||
}, [stockCode, period, loadMarketData]);
|
||||
|
||||
// 监听时间周期变化,只刷新日K线数据
|
||||
useEffect(() => {
|
||||
@@ -273,6 +319,7 @@ export const useMarketData = (
|
||||
refetch,
|
||||
loadMinuteData,
|
||||
refreshTradeData,
|
||||
loadDataByType,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -68,8 +68,25 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
||||
analysisMap,
|
||||
refetch,
|
||||
loadMinuteData,
|
||||
loadDataByType,
|
||||
} = 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 变化
|
||||
useEffect(() => {
|
||||
if (propStockCode && propStockCode !== stockCode) {
|
||||
@@ -173,7 +190,7 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
||||
componentProps={componentProps}
|
||||
themePreset="blackGold"
|
||||
index={activeTab}
|
||||
onTabChange={(index) => setActiveTab(index)}
|
||||
onTabChange={handleTabChange}
|
||||
isLazy
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -364,6 +364,11 @@ export interface OverlayMetricData {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按需加载的数据类型
|
||||
*/
|
||||
export type LazyDataType = 'funding' | 'bigDeal' | 'unusual' | 'pledge';
|
||||
|
||||
/**
|
||||
* useMarketData Hook 返回值
|
||||
*/
|
||||
@@ -383,4 +388,5 @@ export interface UseMarketDataReturn {
|
||||
refetch: () => Promise<void>;
|
||||
loadMinuteData: () => Promise<void>;
|
||||
refreshTradeData: () => Promise<void>;
|
||||
loadDataByType: (dataType: LazyDataType) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Company/components/MarketDataView/utils/chartOptions.ts
|
||||
// MarketDataView ECharts 图表配置生成器
|
||||
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import type { EChartsOption } from '@lib/echarts';
|
||||
import type {
|
||||
Theme,
|
||||
TradeDayData,
|
||||
|
||||
Reference in New Issue
Block a user