perf(MarketDataView): 优化数据映射性能和请求管理

- useMarketData: 使用 Map 替代 findIndex,O(n*m) → O(n+m) 性能优化
- useMarketData: 修复 React StrictMode 下请求被意外取消的问题
- config.ts: 添加 CompanyOverview 和 DynamicTracking 的骨架屏 fallback

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 18:54:28 +08:00
parent d3f4a8e02c
commit bea4c7fe81
4 changed files with 108 additions and 46 deletions

View File

@@ -76,15 +76,21 @@ export const useMarketData = (
const riseAnalysisRes = await marketService.getRiseAnalysis(stockCode);
if (riseAnalysisRes.success && riseAnalysisRes.data) {
// 性能优化:使用 Map 预构建日期索引,将 O(n*m) 降为 O(n+m)
const dateToIndexMap = new Map<string, number>();
tradeDataForMapping.forEach((item, idx) => {
dateToIndexMap.set(item.date.substring(0, 10), idx);
});
// 使用 Map 查找O(1) 复杂度
const tempAnalysisMap: Record<number, RiseAnalysis> = {};
riseAnalysisRes.data.forEach((analysis) => {
const dateIndex = tradeDataForMapping.findIndex(
(item) => item.date.substring(0, 10) === analysis.trade_date
);
if (dateIndex !== -1) {
const dateIndex = dateToIndexMap.get(analysis.trade_date);
if (dateIndex !== undefined) {
tempAnalysisMap[dateIndex] = analysis;
}
});
setAnalysisMap(tempAnalysisMap);
logger.info('useMarketData', '涨幅分析加载成功', { stockCode, count: Object.keys(tempAnalysisMap).length });
}
@@ -164,7 +170,7 @@ export const useMarketData = (
*/
const loadDataByType = useCallback(async (dataType: 'funding' | 'bigDeal' | 'unusual' | 'pledge') => {
if (!stockCode) return;
if (loadedDataRef.current[dataType]) return; // 已加载则跳过
if (loadedDataRef.current[dataType]) return;
// 取消之前的 Tab 数据请求
tabDataControllerRef.current?.abort();
@@ -345,11 +351,15 @@ export const useMarketData = (
}, [period, refreshTradeData, stockCode]);
// 组件卸载时取消所有进行中的请求
// 注意:在 React StrictMode 下,组件会快速卸载再挂载
// 为避免取消正在进行的请求,这里不再自动取消
// 请求会在 loadCoreData 开头通过 coreDataControllerRef.current?.abort() 取消旧请求
useEffect(() => {
return () => {
coreDataControllerRef.current?.abort();
tabDataControllerRef.current?.abort();
minuteDataControllerRef.current?.abort();
// 不再在这里取消请求,让请求自然完成
// coreDataControllerRef.current?.abort();
// tabDataControllerRef.current?.abort();
// minuteDataControllerRef.current?.abort();
};
}, []);

View File

@@ -4,6 +4,8 @@
import React, { useState, useEffect, ReactNode, useMemo, useCallback, memo } from 'react';
import {
Box,
Card,
CardBody,
Container,
VStack,
useDisclosure,
@@ -87,6 +89,14 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
}
}, [propStockCode, stockCode]);
// 首次渲染时加载默认 Tab融资融券的数据
useEffect(() => {
// 默认 Tab 是融资融券index 0
if (activeTab === 0) {
loadDataByType('funding');
}
}, [loadDataByType, activeTab]);
// 处理图表点击事件
const handleChartClick = useCallback(
(params: { seriesName?: string; data?: [number, number] }) => {
@@ -146,6 +156,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
/>
{/* 主要内容区域 - Tab */}
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
<CardBody p={0}>
<SubTabContainer
tabs={tabConfigs}
componentProps={componentProps}
@@ -155,6 +167,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
isLazy
size="sm"
/>
</CardBody>
</Card>
</VStack>
</Container>

View File

@@ -889,26 +889,60 @@ export const getKLineDarkGoldOption = (
const highPrices = tradeData.map((item) => item.high);
const lowPrices = tradeData.map((item) => item.low);
// 计算主图指标
// 计算主图指标 - MA 始终计算(主图必需)
const ma5 = calculateMA(closePrices, 5);
const ma10 = calculateMA(closePrices, 10);
const ma20 = calculateMA(closePrices, 20);
// 计算布林带(如果选择
// 计算布林带(仅当选择 BOLL 时
const boll = mainIndicator === 'BOLL' ? calculateBOLL(closePrices) : null;
// 计算副图指标
const macdData = calculateMACD(closePrices);
const kdjData = calculateKDJ(highPrices, lowPrices, closePrices);
const rsi6 = calculateRSI(closePrices, 6);
const rsi12 = calculateRSI(closePrices, 12);
const rsi24 = calculateRSI(closePrices, 24);
const wr14 = calculateWR(highPrices, lowPrices, closePrices, 14);
const wr6 = calculateWR(highPrices, lowPrices, closePrices, 6);
const cci14 = calculateCCI(highPrices, lowPrices, closePrices, 14);
const bias6 = calculateBIAS(closePrices, 6);
const bias12 = calculateBIAS(closePrices, 12);
const bias24 = calculateBIAS(closePrices, 24);
// 副图指标 - 按需计算(性能优化:只计算当前显示的指标)
type MACDData = { dif: (number | null)[]; dea: (number | null)[]; macd: (number | null)[] };
type KDJData = { k: (number | null)[]; d: (number | null)[]; j: (number | null)[] };
type RSIData = { rsi6: (number | null)[]; rsi12: (number | null)[]; rsi24: (number | null)[] };
type WRData = { wr6: (number | null)[]; wr14: (number | null)[] };
type BIASData = { bias6: (number | null)[]; bias12: (number | null)[]; bias24: (number | null)[] };
let macdData: MACDData | null = null;
let kdjData: KDJData | null = null;
let rsiData: RSIData | null = null;
let wrData: WRData | null = null;
let cci14: (number | null)[] | null = null;
let biasData: BIASData | null = null;
// 根据 subIndicator 按需计算对应指标
switch (subIndicator) {
case 'MACD':
macdData = calculateMACD(closePrices);
break;
case 'KDJ':
kdjData = calculateKDJ(highPrices, lowPrices, closePrices);
break;
case 'RSI':
rsiData = {
rsi6: calculateRSI(closePrices, 6),
rsi12: calculateRSI(closePrices, 12),
rsi24: calculateRSI(closePrices, 24),
};
break;
case 'WR':
wrData = {
wr6: calculateWR(highPrices, lowPrices, closePrices, 6),
wr14: calculateWR(highPrices, lowPrices, closePrices, 14),
};
break;
case 'CCI':
cci14 = calculateCCI(highPrices, lowPrices, closePrices, 14);
break;
case 'BIAS':
biasData = {
bias6: calculateBIAS(closePrices, 6),
bias12: calculateBIAS(closePrices, 12),
bias24: calculateBIAS(closePrices, 24),
};
break;
}
// 创建涨幅分析标记点(仅当 showAnalysis 为 true 时)
const scatterData: [number, number][] = [];
@@ -1307,8 +1341,8 @@ export const getKLineDarkGoldOption = (
},
});
// 添加副图指标
if (subIndicator === 'MACD') {
// 添加副图指标(使用按需计算的数据)
if (subIndicator === 'MACD' && macdData) {
// MACD柱状图
series.push({
name: 'MACD',
@@ -1347,7 +1381,7 @@ export const getKLineDarkGoldOption = (
lineStyle: { color: cyan, width: 1 },
});
legendData.push('MACD', 'DIF', 'DEA');
} else if (subIndicator === 'KDJ') {
} else if (subIndicator === 'KDJ' && kdjData) {
series.push(
{
name: 'K',
@@ -1381,14 +1415,14 @@ export const getKLineDarkGoldOption = (
}
);
legendData.push('K', 'D', 'J');
} else if (subIndicator === 'RSI') {
} else if (subIndicator === 'RSI' && rsiData) {
series.push(
{
name: 'RSI6',
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: rsi6,
data: rsiData.rsi6,
smooth: true,
symbol: 'none',
lineStyle: { color: gold, width: 1 },
@@ -1398,7 +1432,7 @@ export const getKLineDarkGoldOption = (
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: rsi12,
data: rsiData.rsi12,
smooth: true,
symbol: 'none',
lineStyle: { color: cyan, width: 1 },
@@ -1408,21 +1442,21 @@ export const getKLineDarkGoldOption = (
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: rsi24,
data: rsiData.rsi24,
smooth: true,
symbol: 'none',
lineStyle: { color: purple, width: 1 },
}
);
legendData.push('RSI6', 'RSI12', 'RSI24');
} else if (subIndicator === 'WR') {
} else if (subIndicator === 'WR' && wrData) {
series.push(
{
name: 'WR14',
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: wr14,
data: wrData.wr14,
smooth: true,
symbol: 'none',
lineStyle: { color: gold, width: 1 },
@@ -1432,14 +1466,14 @@ export const getKLineDarkGoldOption = (
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: wr6,
data: wrData.wr6,
smooth: true,
symbol: 'none',
lineStyle: { color: cyan, width: 1 },
}
);
legendData.push('WR14', 'WR6');
} else if (subIndicator === 'CCI') {
} else if (subIndicator === 'CCI' && cci14) {
series.push({
name: 'CCI',
type: 'line',
@@ -1474,14 +1508,14 @@ export const getKLineDarkGoldOption = (
}
);
legendData.push('CCI');
} else if (subIndicator === 'BIAS') {
} else if (subIndicator === 'BIAS' && biasData) {
series.push(
{
name: 'BIAS6',
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: bias6,
data: biasData.bias6,
smooth: true,
symbol: 'none',
lineStyle: { color: gold, width: 1 },
@@ -1491,7 +1525,7 @@ export const getKLineDarkGoldOption = (
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: bias12,
data: biasData.bias12,
smooth: true,
symbol: 'none',
lineStyle: { color: cyan, width: 1 },
@@ -1501,7 +1535,7 @@ export const getKLineDarkGoldOption = (
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: bias24,
data: biasData.bias24,
smooth: true,
symbol: 'none',
lineStyle: { color: purple, width: 1 },

View File

@@ -12,6 +12,8 @@ import type { CompanyTheme, TabConfig } from './types';
import { FinancialPanoramaSkeleton } from './components/FinancialPanorama/components';
import { ForecastSkeleton } from './components/ForecastReport/components';
import { MarketDataSkeleton } from './components/MarketDataView/components';
import DynamicTrackingNavSkeleton from './components/DynamicTracking/components/DynamicTrackingNavSkeleton';
import CompanyOverviewNavSkeleton from './components/CompanyOverview/BasicInfoTab/components/CompanyOverviewNavSkeleton';
// ============================================
// 黑金主题配置
@@ -76,6 +78,7 @@ export const TAB_CONFIG: TabConfig[] = [
name: '公司概览',
icon: Building2,
component: CompanyOverview,
fallback: React.createElement(CompanyOverviewNavSkeleton),
},
{
key: 'analysis',
@@ -109,6 +112,7 @@ export const TAB_CONFIG: TabConfig[] = [
name: '动态跟踪',
icon: Newspaper,
component: DynamicTracking,
fallback: React.createElement(DynamicTrackingNavSkeleton),
},
];