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:
@@ -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();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +156,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 主要内容区域 - Tab */}
|
{/* 主要内容区域 - Tab */}
|
||||||
|
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||||
|
<CardBody p={0}>
|
||||||
<SubTabContainer
|
<SubTabContainer
|
||||||
tabs={tabConfigs}
|
tabs={tabConfigs}
|
||||||
componentProps={componentProps}
|
componentProps={componentProps}
|
||||||
@@ -155,6 +167,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
|
|||||||
isLazy
|
isLazy
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user