diff --git a/src/components/StockChangeIndicators.js b/src/components/StockChangeIndicators.js
index f96bf09b..7da56125 100644
--- a/src/components/StockChangeIndicators.js
+++ b/src/components/StockChangeIndicators.js
@@ -1,13 +1,13 @@
// src/components/StockChangeIndicators.js
// 股票涨跌幅指标组件(通用)
-import React from 'react';
+import React, { useState } from 'react';
import { Flex, Box, Text, useColorModeValue } from '@chakra-ui/react';
import { TriangleUpIcon, TriangleDownIcon } from '@chakra-ui/icons';
import { getChangeColor } from '../utils/colorUtils';
/**
- * 股票涨跌幅指标组件(3分天下布局)
+ * 股票涨跌幅指标组件(2个指标:超额涨幅可切换 + 超预期得分)
* @param {Object} props
* @param {number} props.avgChange - 平均超额涨幅
* @param {number} props.maxChange - 最大超额涨幅
@@ -20,6 +20,9 @@ const StockChangeIndicators = ({
expectationScore,
size = 'default',
}) => {
+ // 点击切换显示最大超额/平均超额
+ const [showMax, setShowMax] = useState(true);
+
const isLarge = size === 'large';
const isComfortable = size === 'comfortable';
const isDefault = size === 'default';
@@ -68,8 +71,11 @@ const StockChangeIndicators = ({
: useColorModeValue('green.200', 'green.700');
};
- // 渲染单个指标
- const renderIndicator = (label, value) => {
+ // 渲染可切换的超额指标(最大超额/平均超额)
+ const renderToggleIndicator = () => {
+ const value = showMax ? maxChange : avgChange;
+ const label = showMax ? '最大超额' : '平均超额';
+
if (value == null) return null;
const sign = value > 0 ? '+' : '-';
@@ -95,11 +101,18 @@ const StockChangeIndicators = ({
maxW={isLarge ? "200px" : "none"}
flex="0 1 auto"
minW="0"
+ cursor="pointer"
+ onClick={(e) => {
+ e.stopPropagation();
+ setShowMax(!showMax);
+ }}
+ _hover={{ opacity: 0.85 }}
+ title={`点击切换${showMax ? '平均超额' : '最大超额'}`}
>
{/* Large 和 Default 模式:标签单独一行 */}
{(isLarge || isDefault) && (
- {label.trim()}
+ {label}
)}
@@ -135,7 +148,7 @@ const StockChangeIndicators = ({
{/* Comfortable 模式:标签和数字在同一行 */}
{!isLarge && !isDefault && (
- {label}
+ {label}{' '}
)}
{sign}{numStr}
@@ -229,8 +242,9 @@ const StockChangeIndicators = ({
return (
- {renderIndicator('最大超额', maxChange)}
- {renderIndicator('平均超额', avgChange)}
+ {/* 可切换的超额指标(最大超额/平均超额) */}
+ {renderToggleIndicator()}
+ {/* 超预期得分 */}
{renderScoreIndicator('超预期', expectationScore)}
);
diff --git a/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js b/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js
index 862ba8d8..e517188d 100644
--- a/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js
+++ b/src/views/Community/components/StockDetailPanel/components/MiniTimelineChart.js
@@ -40,19 +40,19 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve
// 从缓存或API获取数据的函数
const loadData = useCallback(() => {
- if (!stockCode || !mountedRef.current) return;
+ if (!stockCode || !mountedRef.current) return false;
// 检查缓存
const cacheKey = getCacheKey(stockCode, stableEventTime);
const cachedData = klineDataCache.get(cacheKey);
- // 如果有缓存数据,直接使用
- if (cachedData && cachedData.length > 0) {
- setData(cachedData);
+ // 如果有缓存数据(包括空数组,表示已请求过但无数据),直接使用
+ if (cachedData !== undefined) {
+ setData(cachedData || []);
setLoading(false);
loadedRef.current = true;
dataFetchedRef.current = true;
- return true; // 表示数据已加载
+ return true; // 表示数据已加载(或确认无数据)
}
return false; // 表示需要请求
}, [stockCode, stableEventTime]);
@@ -75,48 +75,81 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve
return;
}
- // 检查是否有正在进行的批量请求
- const batchKey = `${stableEventTime || 'today'}|timeline`;
- const pendingBatch = batchPendingRequests.get(batchKey);
+ // 检查批量请求的函数
+ const checkBatchAndLoad = () => {
+ // 再次检查缓存(批量请求可能已完成)
+ const cacheKey = getCacheKey(stockCode, stableEventTime);
+ const cachedData = klineDataCache.get(cacheKey);
+ if (cachedData !== undefined) {
+ setData(cachedData || []);
+ setLoading(false);
+ loadedRef.current = true;
+ dataFetchedRef.current = true;
+ return true; // 从缓存加载成功
+ }
- if (pendingBatch) {
- // 等待批量请求完成后再从缓存读取
- setLoading(true);
- dataFetchedRef.current = true;
- pendingBatch.then(() => {
- if (mountedRef.current) {
- loadData();
- setLoading(false);
- }
- }).catch(() => {
- if (mountedRef.current) {
- setData([]);
- setLoading(false);
- }
- });
+ const batchKey = `${stableEventTime || 'today'}|timeline`;
+ const pendingBatch = batchPendingRequests.get(batchKey);
+
+ if (pendingBatch) {
+ // 等待批量请求完成后再从缓存读取
+ setLoading(true);
+ dataFetchedRef.current = true;
+ pendingBatch.then(() => {
+ if (mountedRef.current) {
+ const newCachedData = klineDataCache.get(cacheKey);
+ setData(newCachedData || []);
+ setLoading(false);
+ loadedRef.current = true;
+ }
+ }).catch(() => {
+ if (mountedRef.current) {
+ setData([]);
+ setLoading(false);
+ }
+ });
+ return true; // 找到批量请求
+ }
+ return false; // 没有批量请求
+ };
+
+ // 先立即检查一次
+ if (checkBatchAndLoad()) {
return;
}
- // 没有批量请求,发起单独请求
- dataFetchedRef.current = true;
+ // 延迟一小段时间再检查(等待批量请求启动)
+ // 因为 StockTable 的 useEffect 可能还没执行
setLoading(true);
+ const timeoutId = setTimeout(() => {
+ if (!mountedRef.current || dataFetchedRef.current) return;
- // 使用全局的fetchKlineData函数
- fetchKlineData(stockCode, stableEventTime)
- .then((result) => {
- if (mountedRef.current) {
- setData(result);
- setLoading(false);
- loadedRef.current = true;
- }
- })
- .catch(() => {
- if (mountedRef.current) {
- setData([]);
- setLoading(false);
- loadedRef.current = true;
- }
- });
+ // 再次检查批量请求
+ if (checkBatchAndLoad()) {
+ return;
+ }
+
+ // 仍然没有批量请求,发起单独请求
+ dataFetchedRef.current = true;
+
+ fetchKlineData(stockCode, stableEventTime)
+ .then((result) => {
+ if (mountedRef.current) {
+ setData(result);
+ setLoading(false);
+ loadedRef.current = true;
+ }
+ })
+ .catch(() => {
+ if (mountedRef.current) {
+ setData([]);
+ setLoading(false);
+ loadedRef.current = true;
+ }
+ });
+ }, 50); // 延迟 50ms 等待批量请求启动
+
+ return () => clearTimeout(timeoutId);
}, [stockCode, stableEventTime, loadData]); // 注意这里使用 stableEventTime
const chartOption = useMemo(() => {
diff --git a/src/views/Community/components/StockDetailPanel/components/StockTable.js b/src/views/Community/components/StockDetailPanel/components/StockTable.js
index c5319160..d272ea2f 100644
--- a/src/views/Community/components/StockDetailPanel/components/StockTable.js
+++ b/src/views/Community/components/StockDetailPanel/components/StockTable.js
@@ -1,5 +1,5 @@
// src/views/Community/components/StockDetailPanel/components/StockTable.js
-import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
+import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { Table, Button } from 'antd';
import { StarFilled, StarOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
@@ -29,7 +29,6 @@ const StockTable = ({
}) => {
// 展开/收缩的行
const [expandedRows, setExpandedRows] = useState(new Set());
- const preloadedRef = useRef(false); // 标记是否已预加载
// 稳定的事件时间,避免重复渲染
const stableEventTime = useMemo(() => {
@@ -37,22 +36,21 @@ const StockTable = ({
}, [eventTime]);
// 批量预加载K线数据
+ // 使用 stocks 的 JSON 字符串作为依赖项的 key,避免引用变化导致重复预加载
+ const stocksKey = useMemo(() => {
+ return stocks.map(s => s.stock_code).sort().join(',');
+ }, [stocks]);
+
useEffect(() => {
- if (stocks.length > 0 && !preloadedRef.current) {
+ if (stocks.length > 0) {
const stockCodes = stocks.map(s => s.stock_code);
logger.debug('StockTable', '批量预加载K线数据', {
stockCount: stockCodes.length,
eventTime: stableEventTime
});
preloadBatchKlineData(stockCodes, stableEventTime, 'timeline');
- preloadedRef.current = true;
}
- }, [stocks, stableEventTime]);
-
- // 当股票列表变化时重置预加载标记
- useEffect(() => {
- preloadedRef.current = false;
- }, [stocks.length]);
+ }, [stocksKey, stableEventTime]); // 使用 stocksKey 而非 stocks 对象引用
// 切换行展开状态
const toggleRowExpand = useCallback((stockCode) => {