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) => {