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); const riseAnalysisRes = await marketService.getRiseAnalysis(stockCode);
if (riseAnalysisRes.success && riseAnalysisRes.data) { 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> = {}; const tempAnalysisMap: Record<number, RiseAnalysis> = {};
riseAnalysisRes.data.forEach((analysis) => { riseAnalysisRes.data.forEach((analysis) => {
const dateIndex = tradeDataForMapping.findIndex( const dateIndex = dateToIndexMap.get(analysis.trade_date);
(item) => item.date.substring(0, 10) === analysis.trade_date if (dateIndex !== undefined) {
);
if (dateIndex !== -1) {
tempAnalysisMap[dateIndex] = analysis; tempAnalysisMap[dateIndex] = analysis;
} }
}); });
setAnalysisMap(tempAnalysisMap); setAnalysisMap(tempAnalysisMap);
logger.info('useMarketData', '涨幅分析加载成功', { stockCode, count: Object.keys(tempAnalysisMap).length }); 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') => { const loadDataByType = useCallback(async (dataType: 'funding' | 'bigDeal' | 'unusual' | 'pledge') => {
if (!stockCode) return; if (!stockCode) return;
if (loadedDataRef.current[dataType]) return; // 已加载则跳过 if (loadedDataRef.current[dataType]) return;
// 取消之前的 Tab 数据请求 // 取消之前的 Tab 数据请求
tabDataControllerRef.current?.abort(); tabDataControllerRef.current?.abort();
@@ -345,11 +351,15 @@ export const useMarketData = (
}, [period, refreshTradeData, stockCode]); }, [period, refreshTradeData, stockCode]);
// 组件卸载时取消所有进行中的请求 // 组件卸载时取消所有进行中的请求
// 注意:在 React StrictMode 下,组件会快速卸载再挂载
// 为避免取消正在进行的请求,这里不再自动取消
// 请求会在 loadCoreData 开头通过 coreDataControllerRef.current?.abort() 取消旧请求
useEffect(() => { useEffect(() => {
return () => { return () => {
coreDataControllerRef.current?.abort(); // 不再在这里取消请求,让请求自然完成
tabDataControllerRef.current?.abort(); // coreDataControllerRef.current?.abort();
minuteDataControllerRef.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 React, { useState, useEffect, ReactNode, useMemo, useCallback, memo } from 'react';
import { import {
Box, Box,
Card,
CardBody,
Container, Container,
VStack, VStack,
useDisclosure, useDisclosure,
@@ -87,6 +89,14 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
} }
}, [propStockCode, stockCode]); }, [propStockCode, stockCode]);
// 首次渲染时加载默认 Tab融资融券的数据
useEffect(() => {
// 默认 Tab 是融资融券index 0
if (activeTab === 0) {
loadDataByType('funding');
}
}, [loadDataByType, activeTab]);
// 处理图表点击事件 // 处理图表点击事件
const handleChartClick = useCallback( const handleChartClick = useCallback(
(params: { seriesName?: string; data?: [number, number] }) => { (params: { seriesName?: string; data?: [number, number] }) => {
@@ -146,15 +156,19 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
/> />
{/* 主要内容区域 - Tab */} {/* 主要内容区域 - Tab */}
<SubTabContainer <Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
tabs={tabConfigs} <CardBody p={0}>
componentProps={componentProps} <SubTabContainer
themePreset="blackGold" tabs={tabConfigs}
index={activeTab} componentProps={componentProps}
onTabChange={handleTabChange} themePreset="blackGold"
isLazy index={activeTab}
size="sm" onTabChange={handleTabChange}
/> isLazy
size="sm"
/>
</CardBody>
</Card>
</VStack> </VStack>
</Container> </Container>

View File

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

View File

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