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