update pay ui

This commit is contained in:
2025-12-03 10:30:49 +08:00
parent f7f9774caa
commit 03d0a6514c
3 changed files with 104 additions and 59 deletions

View File

@@ -1,13 +1,13 @@
// src/components/StockChangeIndicators.js // src/components/StockChangeIndicators.js
// 股票涨跌幅指标组件(通用) // 股票涨跌幅指标组件(通用)
import React from 'react'; import React, { useState } from 'react';
import { Flex, Box, Text, useColorModeValue } from '@chakra-ui/react'; import { Flex, Box, Text, useColorModeValue } from '@chakra-ui/react';
import { TriangleUpIcon, TriangleDownIcon } from '@chakra-ui/icons'; import { TriangleUpIcon, TriangleDownIcon } from '@chakra-ui/icons';
import { getChangeColor } from '../utils/colorUtils'; import { getChangeColor } from '../utils/colorUtils';
/** /**
* 股票涨跌幅指标组件(3分天下布局 * 股票涨跌幅指标组件(2个指标超额涨幅可切换 + 超预期得分
* @param {Object} props * @param {Object} props
* @param {number} props.avgChange - 平均超额涨幅 * @param {number} props.avgChange - 平均超额涨幅
* @param {number} props.maxChange - 最大超额涨幅 * @param {number} props.maxChange - 最大超额涨幅
@@ -20,6 +20,9 @@ const StockChangeIndicators = ({
expectationScore, expectationScore,
size = 'default', size = 'default',
}) => { }) => {
// 点击切换显示最大超额/平均超额
const [showMax, setShowMax] = useState(true);
const isLarge = size === 'large'; const isLarge = size === 'large';
const isComfortable = size === 'comfortable'; const isComfortable = size === 'comfortable';
const isDefault = size === 'default'; const isDefault = size === 'default';
@@ -68,8 +71,11 @@ const StockChangeIndicators = ({
: useColorModeValue('green.200', 'green.700'); : useColorModeValue('green.200', 'green.700');
}; };
// 渲染单个指标 // 渲染可切换的超额指标(最大超额/平均超额)
const renderIndicator = (label, value) => { const renderToggleIndicator = () => {
const value = showMax ? maxChange : avgChange;
const label = showMax ? '最大超额' : '平均超额';
if (value == null) return null; if (value == null) return null;
const sign = value > 0 ? '+' : '-'; const sign = value > 0 ? '+' : '-';
@@ -95,11 +101,18 @@ const StockChangeIndicators = ({
maxW={isLarge ? "200px" : "none"} maxW={isLarge ? "200px" : "none"}
flex="0 1 auto" flex="0 1 auto"
minW="0" minW="0"
cursor="pointer"
onClick={(e) => {
e.stopPropagation();
setShowMax(!showMax);
}}
_hover={{ opacity: 0.85 }}
title={`点击切换${showMax ? '平均超额' : '最大超额'}`}
> >
{/* Large 和 Default 模式:标签单独一行 */} {/* Large 和 Default 模式:标签单独一行 */}
{(isLarge || isDefault) && ( {(isLarge || isDefault) && (
<Text fontSize={isLarge ? "sm" : "xs"} color={labelColor} fontWeight="medium"> <Text fontSize={isLarge ? "sm" : "xs"} color={labelColor} fontWeight="medium">
{label.trim()} {label}
</Text> </Text>
)} )}
@@ -135,7 +148,7 @@ const StockChangeIndicators = ({
{/* Comfortable 模式:标签和数字在同一行 */} {/* Comfortable 模式:标签和数字在同一行 */}
{!isLarge && !isDefault && ( {!isLarge && !isDefault && (
<Text as="span" color={labelColor} fontWeight="medium" fontSize="sm"> <Text as="span" color={labelColor} fontWeight="medium" fontSize="sm">
{label} {label}{' '}
</Text> </Text>
)} )}
{sign}{numStr} {sign}{numStr}
@@ -229,8 +242,9 @@ const StockChangeIndicators = ({
return ( return (
<Flex width="100%" justify="flex-start" align="center" gap={isLarge ? 4 : (isDefault ? 2 : 1)}> <Flex width="100%" justify="flex-start" align="center" gap={isLarge ? 4 : (isDefault ? 2 : 1)}>
{renderIndicator('最大超额', maxChange)} {/* 可切换的超额指标(最大超额/平均超额) */}
{renderIndicator('平均超额', avgChange)} {renderToggleIndicator()}
{/* 超预期得分 */}
{renderScoreIndicator('超预期', expectationScore)} {renderScoreIndicator('超预期', expectationScore)}
</Flex> </Flex>
); );

View File

@@ -40,19 +40,19 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve
// 从缓存或API获取数据的函数 // 从缓存或API获取数据的函数
const loadData = useCallback(() => { const loadData = useCallback(() => {
if (!stockCode || !mountedRef.current) return; if (!stockCode || !mountedRef.current) return false;
// 检查缓存 // 检查缓存
const cacheKey = getCacheKey(stockCode, stableEventTime); const cacheKey = getCacheKey(stockCode, stableEventTime);
const cachedData = klineDataCache.get(cacheKey); const cachedData = klineDataCache.get(cacheKey);
// 如果有缓存数据,直接使用 // 如果有缓存数据(包括空数组,表示已请求过但无数据),直接使用
if (cachedData && cachedData.length > 0) { if (cachedData !== undefined) {
setData(cachedData); setData(cachedData || []);
setLoading(false); setLoading(false);
loadedRef.current = true; loadedRef.current = true;
dataFetchedRef.current = true; dataFetchedRef.current = true;
return true; // 表示数据已加载 return true; // 表示数据已加载(或确认无数据)
} }
return false; // 表示需要请求 return false; // 表示需要请求
}, [stockCode, stableEventTime]); }, [stockCode, stableEventTime]);
@@ -75,7 +75,19 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve
return; return;
} }
// 检查是否有正在进行的批量请求 // 检查批量请求的函数
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; // 从缓存加载成功
}
const batchKey = `${stableEventTime || 'today'}|timeline`; const batchKey = `${stableEventTime || 'today'}|timeline`;
const pendingBatch = batchPendingRequests.get(batchKey); const pendingBatch = batchPendingRequests.get(batchKey);
@@ -85,8 +97,10 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve
dataFetchedRef.current = true; dataFetchedRef.current = true;
pendingBatch.then(() => { pendingBatch.then(() => {
if (mountedRef.current) { if (mountedRef.current) {
loadData(); const newCachedData = klineDataCache.get(cacheKey);
setData(newCachedData || []);
setLoading(false); setLoading(false);
loadedRef.current = true;
} }
}).catch(() => { }).catch(() => {
if (mountedRef.current) { if (mountedRef.current) {
@@ -94,14 +108,30 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve
setLoading(false); setLoading(false);
} }
}); });
return true; // 找到批量请求
}
return false; // 没有批量请求
};
// 先立即检查一次
if (checkBatchAndLoad()) {
return; return;
} }
// 没有批量请求,发起单独请求 // 延迟一小段时间再检查(等待批量请求启动)
dataFetchedRef.current = true; // 因为 StockTable 的 useEffect 可能还没执行
setLoading(true); setLoading(true);
const timeoutId = setTimeout(() => {
if (!mountedRef.current || dataFetchedRef.current) return;
// 再次检查批量请求
if (checkBatchAndLoad()) {
return;
}
// 仍然没有批量请求,发起单独请求
dataFetchedRef.current = true;
// 使用全局的fetchKlineData函数
fetchKlineData(stockCode, stableEventTime) fetchKlineData(stockCode, stableEventTime)
.then((result) => { .then((result) => {
if (mountedRef.current) { if (mountedRef.current) {
@@ -117,6 +147,9 @@ const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eve
loadedRef.current = true; loadedRef.current = true;
} }
}); });
}, 50); // 延迟 50ms 等待批量请求启动
return () => clearTimeout(timeoutId);
}, [stockCode, stableEventTime, loadData]); // 注意这里使用 stableEventTime }, [stockCode, stableEventTime, loadData]); // 注意这里使用 stableEventTime
const chartOption = useMemo(() => { const chartOption = useMemo(() => {

View File

@@ -1,5 +1,5 @@
// src/views/Community/components/StockDetailPanel/components/StockTable.js // 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 { Table, Button } from 'antd';
import { StarFilled, StarOutlined } from '@ant-design/icons'; import { StarFilled, StarOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -29,7 +29,6 @@ const StockTable = ({
}) => { }) => {
// 展开/收缩的行 // 展开/收缩的行
const [expandedRows, setExpandedRows] = useState(new Set()); const [expandedRows, setExpandedRows] = useState(new Set());
const preloadedRef = useRef(false); // 标记是否已预加载
// 稳定的事件时间,避免重复渲染 // 稳定的事件时间,避免重复渲染
const stableEventTime = useMemo(() => { const stableEventTime = useMemo(() => {
@@ -37,22 +36,21 @@ const StockTable = ({
}, [eventTime]); }, [eventTime]);
// 批量预加载K线数据 // 批量预加载K线数据
// 使用 stocks 的 JSON 字符串作为依赖项的 key避免引用变化导致重复预加载
const stocksKey = useMemo(() => {
return stocks.map(s => s.stock_code).sort().join(',');
}, [stocks]);
useEffect(() => { useEffect(() => {
if (stocks.length > 0 && !preloadedRef.current) { if (stocks.length > 0) {
const stockCodes = stocks.map(s => s.stock_code); const stockCodes = stocks.map(s => s.stock_code);
logger.debug('StockTable', '批量预加载K线数据', { logger.debug('StockTable', '批量预加载K线数据', {
stockCount: stockCodes.length, stockCount: stockCodes.length,
eventTime: stableEventTime eventTime: stableEventTime
}); });
preloadBatchKlineData(stockCodes, stableEventTime, 'timeline'); preloadBatchKlineData(stockCodes, stableEventTime, 'timeline');
preloadedRef.current = true;
} }
}, [stocks, stableEventTime]); }, [stocksKey, stableEventTime]); // 使用 stocksKey 而非 stocks 对象引用
// 当股票列表变化时重置预加载标记
useEffect(() => {
preloadedRef.current = false;
}, [stocks.length]);
// 切换行展开状态 // 切换行展开状态
const toggleRowExpand = useCallback((stockCode) => { const toggleRowExpand = useCallback((stockCode) => {