feat(community): 事件TOP10添加无限滚动动画

- 默认展示8个事件,列表向上无限轮播
  - 使用 framer-motion useAnimationControls 实现
  - 鼠标悬停时暂停动画
This commit is contained in:
zdl
2026-01-13 14:55:55 +08:00
parent 098a88c5ba
commit 59da1718ae

View File

@@ -1,8 +1,8 @@
/**
* EventDailyStats - 当日事件统计面板
* 展示当前交易日的事件统计数据,证明系统推荐的胜率和市场热度
* EventDailyStats - 事件 TOP 排行面板
* 展示当日事件的表现排行
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from "react";
import {
Box,
Text,
@@ -12,24 +12,11 @@ import {
Center,
Tooltip,
Badge,
Tabs,
TabList,
Tab,
TabPanels,
TabPanel,
Input,
Flex,
} from '@chakra-ui/react';
import {
FireOutlined,
RiseOutlined,
ThunderboltOutlined,
TrophyOutlined,
StockOutlined,
CalendarOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { getApiBase } from '@utils/apiConfig';
} from "@chakra-ui/react";
import { motion, useAnimationControls } from "framer-motion";
import { getApiBase } from "@utils/apiConfig";
const MotionBox = motion.create(Box);
/**
* 生成事件详情页 URL
@@ -43,275 +30,31 @@ const getEventDetailUrl = (eventId) => {
* 格式化涨跌幅
*/
const formatChg = (val) => {
if (val === null || val === undefined) return '-';
if (val === null || val === undefined) return "-";
const num = parseFloat(val);
if (isNaN(num)) return '-';
return (num >= 0 ? '+' : '') + num.toFixed(2) + '%';
if (isNaN(num)) return "-";
return (num >= 0 ? "+" : "") + num.toFixed(2) + "%";
};
/**
* 获取涨跌幅颜色
*/
const getChgColor = (val) => {
if (val === null || val === undefined) return 'gray.400';
if (val === null || val === undefined) return "gray.400";
const num = parseFloat(val);
if (isNaN(num)) return 'gray.400';
if (num > 0) return '#FF4D4F';
if (num < 0) return '#52C41A';
return 'gray.400';
if (isNaN(num)) return "gray.400";
if (num > 0) return "#FF4D4F";
if (num < 0) return "#52C41A";
return "gray.400";
};
/**
* 获取胜率颜色(>50%红色,<50%绿色)
*/
const getRateColor = (rate) => {
if (rate >= 50) return '#F31260'; // HeroUI 红色
return '#17C964'; // HeroUI 绿色
};
/**
* HeroUI 风格圆环仪表盘
*/
const CircularGauge = ({ rate, label, icon }) => {
const validRate = Math.min(100, Math.max(0, rate || 0));
const gaugeColor = getRateColor(validRate);
const circumference = 2 * Math.PI * 42; // 半径42
const strokeDashoffset = circumference - (validRate / 100) * circumference;
return (
<Box
bg="rgba(255,255,255,0.03)"
backdropFilter="blur(20px)"
borderRadius="2xl"
p={4}
border="1px solid"
borderColor="rgba(255,255,255,0.08)"
position="relative"
overflow="hidden"
flex="1"
_before={{
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `radial-gradient(circle at 30% 20%, ${gaugeColor}15 0%, transparent 50%)`,
pointerEvents: 'none',
}}
>
{/* 圆环仪表盘 */}
<Center>
<Box position="relative" w="100px" h="100px">
<svg width="100" height="100" style={{ transform: 'rotate(-90deg)' }}>
{/* 背景圆环 */}
<circle
cx="50"
cy="50"
r="42"
fill="none"
stroke="rgba(255,255,255,0.08)"
strokeWidth="8"
/>
{/* 渐变定义 */}
<defs>
<linearGradient id={`gauge-grad-${label}`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={gaugeColor} stopOpacity="0.6" />
<stop offset="100%" stopColor={gaugeColor} />
</linearGradient>
</defs>
{/* 进度圆环 */}
<circle
cx="50"
cy="50"
r="42"
fill="none"
stroke={`url(#gauge-grad-${label})`}
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
style={{
transition: 'stroke-dashoffset 0.8s ease-out',
filter: `drop-shadow(0 0 8px ${gaugeColor}60)`,
}}
/>
</svg>
{/* 中心数值 */}
<VStack
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
spacing={0}
>
<Text
fontSize="2xl"
fontWeight="bold"
color={gaugeColor}
lineHeight="1"
textShadow={`0 0 20px ${gaugeColor}40`}
>
{validRate.toFixed(1)}
</Text>
<Text fontSize="xs" color="whiteAlpha.600">%</Text>
</VStack>
</Box>
</Center>
{/* 标签 */}
<HStack justify="center" mt={2} spacing={2}>
<Box color={gaugeColor} fontSize="sm">{icon}</Box>
<Text fontSize="sm" color="whiteAlpha.800" fontWeight="medium">
{label}
</Text>
</HStack>
</Box>
);
};
/**
* HeroUI 风格胜率对比面板
*/
const WinRateGauge = ({ eventRate, marketRate, marketStats }) => {
const eventRateVal = eventRate || 0;
const marketRateVal = marketRate || 0;
return (
<Box>
{/* 双仪表盘对比 - HeroUI 毛玻璃卡片 */}
<HStack spacing={4} mb={4}>
<CircularGauge
rate={eventRateVal}
label="事件胜率"
icon={<TrophyOutlined />}
/>
<CircularGauge
rate={marketRateVal}
label="大盘上涨率"
icon={<RiseOutlined />}
/>
</HStack>
{/* 市场统计 - 毛玻璃条 */}
{marketStats && marketStats.totalCount > 0 && (
<Box
bg="rgba(255,255,255,0.03)"
backdropFilter="blur(10px)"
borderRadius="xl"
p={3}
border="1px solid rgba(255,255,255,0.06)"
>
<HStack justify="space-between" mb={2}>
<Text fontSize="xs" color="whiteAlpha.500">沪深两市实时</Text>
<Text fontSize="xs" color="whiteAlpha.400">{marketStats.totalCount} </Text>
</HStack>
{/* 进度条 */}
<Box position="relative" h="6px" borderRadius="full" overflow="hidden" bg="rgba(255,255,255,0.05)">
<Box
position="absolute"
left="0"
top="0"
h="100%"
w={`${(marketStats.risingCount / marketStats.totalCount) * 100}%`}
bg="linear-gradient(90deg, #F31260, #FF6B9D)"
borderRadius="full"
/>
<Box
position="absolute"
left={`${(marketStats.risingCount / marketStats.totalCount) * 100}%`}
top="0"
h="100%"
w={`${(marketStats.flatCount / marketStats.totalCount) * 100}%`}
bg="rgba(255,255,255,0.3)"
/>
</Box>
{/* 数字统计 */}
<HStack justify="space-between" mt={2}>
<HStack spacing={1}>
<Box w="8px" h="8px" borderRadius="full" bg="#F31260" boxShadow="0 0 8px #F3126060" />
<Text fontSize="sm" color="#FF6B9D" fontWeight="bold">{marketStats.risingCount}</Text>
<Text fontSize="xs" color="whiteAlpha.500"></Text>
</HStack>
<HStack spacing={1}>
<Box w="8px" h="8px" borderRadius="full" bg="whiteAlpha.400" />
<Text fontSize="sm" color="whiteAlpha.700" fontWeight="bold">{marketStats.flatCount}</Text>
<Text fontSize="xs" color="whiteAlpha.500"></Text>
</HStack>
<HStack spacing={1}>
<Box w="8px" h="8px" borderRadius="full" bg="#17C964" boxShadow="0 0 8px #17C96460" />
<Text fontSize="sm" color="#17C964" fontWeight="bold">{marketStats.fallingCount}</Text>
<Text fontSize="xs" color="whiteAlpha.500"></Text>
</HStack>
</HStack>
</Box>
)}
</Box>
);
};
/**
* HeroUI 风格紧凑数据卡片
*/
const CompactStatCard = ({ label, value, icon, color = '#7C3AED', subText }) => (
<Box
bg="rgba(255,255,255,0.03)"
backdropFilter="blur(10px)"
borderRadius="xl"
p={3}
border="1px solid"
borderColor="rgba(255,255,255,0.06)"
_hover={{
borderColor: 'rgba(255,255,255,0.12)',
bg: 'rgba(255,255,255,0.05)',
transform: 'translateY(-2px)',
}}
transition="all 0.3s ease"
position="relative"
overflow="hidden"
_before={{
content: '""',
position: 'absolute',
top: 0,
right: 0,
w: '60px',
h: '60px',
background: `radial-gradient(circle, ${color}15 0%, transparent 70%)`,
pointerEvents: 'none',
}}
>
<HStack spacing={2} mb={1}>
<Box
color={color}
fontSize="sm"
p={1.5}
borderRadius="lg"
bg={`${color}15`}
>
{icon}
</Box>
<Text fontSize="xs" color="whiteAlpha.600" fontWeight="medium">
{label}
</Text>
</HStack>
<Text fontSize="lg" fontWeight="bold" color={color} lineHeight="1.2" textShadow={`0 0 20px ${color}30`}>
{value}
</Text>
{subText && (
<Text fontSize="2xs" color="gray.600" mt={0.5}>
{subText}
</Text>
)}
</Box>
);
/**
* TOP事件列表项
*/
const TopEventItem = ({ event, rank }) => {
const handleClick = () => {
if (event.id) {
window.open(getEventDetailUrl(event.id), '_blank');
window.open(getEventDetailUrl(event.id), "_blank");
}
};
@@ -322,12 +65,12 @@ const TopEventItem = ({ event, rank }) => {
px={2}
bg="rgba(0,0,0,0.2)"
borderRadius="md"
_hover={{ bg: 'rgba(255,215,0,0.12)', cursor: 'pointer' }}
_hover={{ bg: "rgba(255,215,0,0.12)", cursor: "pointer" }}
transition="all 0.15s"
onClick={handleClick}
>
<Badge
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
colorScheme={rank === 1 ? "yellow" : rank === 2 ? "gray" : "orange"}
fontSize="2xs"
px={1.5}
borderRadius="full"
@@ -342,7 +85,7 @@ const TopEventItem = ({ event, rank }) => {
color="gray.300"
flex="1"
noOfLines={1}
_hover={{ color: '#FFD700' }}
_hover={{ color: "#FFD700" }}
>
{event.title}
</Text>
@@ -354,52 +97,19 @@ const TopEventItem = ({ event, rank }) => {
);
};
/**
* TOP股票列表项
*/
const TopStockItem = ({ stock, rank }) => {
return (
<HStack
spacing={2}
py={1}
px={2}
bg="rgba(0,0,0,0.2)"
borderRadius="md"
_hover={{ bg: 'rgba(255,215,0,0.12)' }}
transition="all 0.15s"
>
<Badge
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
fontSize="2xs"
px={1.5}
borderRadius="full"
minW="18px"
textAlign="center"
>
{rank}
</Badge>
<Text fontSize="xs" color="gray.400" w="55px">
{stock.stockCode?.split('.')[0] || '-'}
</Text>
<Text fontSize="xs" color="gray.300" flex="1" noOfLines={1}>
{stock.stockName || '-'}
</Text>
<Text fontSize="xs" fontWeight="bold" color={getChgColor(stock.maxChg)}>
{formatChg(stock.maxChg)}
</Text>
</HStack>
);
};
// 单个事件项高度py=1 约 8px * 2 + 内容约 20px + spacing 4px
const ITEM_HEIGHT = 32;
const VISIBLE_COUNT = 8;
const EventDailyStats = () => {
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [, setRefreshing] = useState(false);
const [stats, setStats] = useState(null);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState(0);
const [selectedDate, setSelectedDate] = useState('');
const [isPaused, setIsPaused] = useState(false);
const controls = useAnimationControls();
const fetchStats = useCallback(async (dateStr = '', isRefresh = false) => {
const fetchStats = useCallback(async (isRefresh = false) => {
if (isRefresh) {
setRefreshing(true);
} else {
@@ -408,17 +118,18 @@ const EventDailyStats = () => {
setError(null);
try {
const apiBase = getApiBase();
const dateParam = dateStr ? `&date=${dateStr}` : '';
const response = await fetch(`${apiBase}/api/v1/events/effectiveness-stats?days=1${dateParam}`);
if (!response.ok) throw new Error('获取数据失败');
const response = await fetch(
`${apiBase}/api/v1/events/effectiveness-stats?days=1`
);
if (!response.ok) throw new Error("获取数据失败");
const data = await response.json();
if (data.success || data.code === 200) {
setStats(data.data);
} else {
throw new Error(data.message || '数据格式错误');
throw new Error(data.message || "数据格式错误");
}
} catch (err) {
console.error('获取事件统计失败:', err);
console.error("获取事件统计失败:", err);
setError(err.message);
} finally {
setLoading(false);
@@ -427,29 +138,50 @@ const EventDailyStats = () => {
}, []);
useEffect(() => {
fetchStats(selectedDate);
}, [fetchStats, selectedDate]);
fetchStats();
}, [fetchStats]);
// 自动刷新(仅当选择今天时,每60秒刷新一次
// 自动刷新每60秒刷新一次
useEffect(() => {
if (!selectedDate) {
const interval = setInterval(() => fetchStats('', true), 60 * 1000);
return () => clearInterval(interval);
const interval = setInterval(() => fetchStats(true), 60 * 1000);
return () => clearInterval(interval);
}, [fetchStats]);
// 获取显示列表取前10个复制一份用于无缝循环
const displayList = useMemo(() => {
const topPerformers = stats?.topPerformers || [];
const list = topPerformers.slice(0, 10);
// 数据不足5个时不需要滚动
if (list.length <= VISIBLE_COUNT) return list;
// 复制一份用于无缝循环
return [...list, ...list];
}, [stats]);
const needScroll = displayList.length > VISIBLE_COUNT;
const originalCount = Math.min((stats?.topPerformers || []).length, 10);
const totalScrollHeight = originalCount * ITEM_HEIGHT;
// 滚动动画
useEffect(() => {
if (!needScroll || isPaused) {
controls.stop();
return;
}
}, [selectedDate, fetchStats]);
const handleDateChange = (e) => {
setSelectedDate(e.target.value);
};
const startAnimation = async () => {
await controls.start({
y: -totalScrollHeight,
transition: {
duration: originalCount * 2, // 每个item约2秒
ease: "linear",
repeat: Infinity,
repeatType: "loop",
},
});
};
// 手动刷新
const handleRefresh = () => {
if (!refreshing) {
fetchStats(selectedDate, true);
}
};
const isToday = !selectedDate;
startAnimation();
}, [needScroll, isPaused, controls, totalScrollHeight, originalCount]);
if (loading) {
return (
@@ -468,34 +200,14 @@ const EventDailyStats = () => {
);
}
if (error || !stats) {
return (
<Box
bg="linear-gradient(180deg, rgba(25, 32, 55, 0.95) 0%, rgba(17, 24, 39, 0.98) 100%)"
borderRadius="xl"
p={3}
border="1px solid"
borderColor="rgba(255, 215, 0, 0.15)"
h="100%"
>
<Center h="100%">
<VStack spacing={2}>
<Text color="gray.400" fontSize="sm">暂无数据</Text>
<Text fontSize="xs" color="gray.600">{error}</Text>
</VStack>
</Center>
</Box>
);
}
const { summary, marketStats, topPerformers = [], topStocks = [] } = stats;
const hasData = stats && displayList.length > 0;
return (
<Box
bg="linear-gradient(135deg, rgba(10, 10, 20, 0.9) 0%, rgba(20, 20, 40, 0.95) 50%, rgba(15, 15, 30, 0.9) 100%)"
backdropFilter="blur(20px)"
borderRadius="2xl"
p={4}
borderRadius="xl"
p={3}
border="1px solid"
borderColor="rgba(255, 255, 255, 0.08)"
position="relative"
@@ -528,266 +240,54 @@ const EventDailyStats = () => {
/>
{/* 标题行 */}
<Flex justify="space-between" align="center" mb={4}>
<HStack spacing={3}>
<Box
w="4px"
h="20px"
bg="linear-gradient(180deg, #7C3AED, #06B6D4)"
borderRadius="full"
boxShadow="0 0 10px rgba(124, 58, 237, 0.5)"
/>
<Text fontSize="md" fontWeight="bold" color="white" letterSpacing="wide">
{isToday ? '今日统计' : '历史统计'}
</Text>
{isToday && (
<Box
px={2}
py={0.5}
bg="rgba(23, 201, 100, 0.15)"
border="1px solid rgba(23, 201, 100, 0.3)"
borderRadius="full"
>
<HStack spacing={1}>
<Box
w="6px"
h="6px"
borderRadius="full"
bg="#17C964"
animation="pulse 2s infinite"
boxShadow="0 0 8px #17C964"
/>
<Text fontSize="xs" color="#17C964" fontWeight="medium">实时</Text>
</HStack>
</Box>
)}
</HStack>
<HStack spacing={2}>
{/* 刷新按钮 */}
<Tooltip label="刷新数据" placement="bottom" hasArrow>
<Box
p={1.5}
bg="rgba(255,255,255,0.03)"
border="1px solid rgba(255,255,255,0.08)"
borderRadius="lg"
cursor="pointer"
_hover={{ bg: 'rgba(6, 182, 212, 0.15)', borderColor: 'rgba(6, 182, 212, 0.3)', transform: 'scale(1.05)' }}
transition="all 0.2s"
onClick={handleRefresh}
>
<ReloadOutlined
style={{
color: 'rgba(6, 182, 212, 0.8)',
fontSize: '14px',
}}
spin={refreshing}
/>
</Box>
</Tooltip>
{/* 今天按钮 - 仅在查看历史时显示 */}
{!isToday && (
<Box
px={3}
py={1}
bg="rgba(124, 58, 237, 0.15)"
border="1px solid rgba(124, 58, 237, 0.3)"
borderRadius="lg"
cursor="pointer"
_hover={{ bg: 'rgba(124, 58, 237, 0.25)', transform: 'scale(1.02)' }}
transition="all 0.2s"
onClick={() => setSelectedDate('')}
>
<Text fontSize="xs" color="#A78BFA" fontWeight="bold">返回今天</Text>
</Box>
)}
<Box
as="label"
display="flex"
alignItems="center"
gap={2}
px={3}
py={1.5}
bg="rgba(255,255,255,0.03)"
border="1px solid rgba(255,255,255,0.08)"
borderRadius="lg"
cursor="pointer"
_hover={{ bg: 'rgba(255,255,255,0.06)', borderColor: 'rgba(255,255,255,0.12)' }}
transition="all 0.2s"
>
<CalendarOutlined style={{ color: 'rgba(255,255,255,0.6)', fontSize: '14px' }} />
<Input
type="date"
size="xs"
value={selectedDate}
onChange={handleDateChange}
max={new Date().toISOString().split('T')[0]}
bg="transparent"
border="none"
color="whiteAlpha.800"
fontSize="xs"
w="100px"
h="20px"
p={0}
_hover={{ border: 'none' }}
_focus={{ border: 'none', boxShadow: 'none' }}
css={{
'&::-webkit-calendar-picker-indicator': {
filter: 'invert(0.8)',
cursor: 'pointer',
opacity: 0.6,
},
}}
/>
</Box>
</HStack>
</Flex>
<HStack spacing={2} mb={2}>
<Box
w="3px"
h="14px"
bg="linear-gradient(180deg, #7C3AED, #06B6D4)"
borderRadius="full"
boxShadow="0 0 8px rgba(124, 58, 237, 0.5)"
/>
<Text fontSize="sm" fontWeight="bold" color="white">
事件 TOP 排行
</Text>
</HStack>
{/* 内容区域 - 固定高度滚动 */}
{/* 内容区域 - 固定高度显示8个向上滚动轮播 */}
<Box
flex="1"
overflowY="auto"
pr={1}
css={{
'&::-webkit-scrollbar': { width: '4px' },
'&::-webkit-scrollbar-track': { background: 'rgba(255,255,255,0.02)', borderRadius: '2px' },
'&::-webkit-scrollbar-thumb': { background: 'rgba(124, 58, 237, 0.3)', borderRadius: '2px' },
'&::-webkit-scrollbar-thumb:hover': { background: 'rgba(124, 58, 237, 0.5)' },
}}
h={`${ITEM_HEIGHT * VISIBLE_COUNT}px`}
maxH={`${ITEM_HEIGHT * VISIBLE_COUNT}px`}
overflow="hidden"
position="relative"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<VStack spacing={4} align="stretch">
{/* 胜率对比仪表盘 */}
<WinRateGauge
eventRate={summary?.positiveRate || 0}
marketRate={marketStats?.risingRate || 0}
marketStats={marketStats}
/>
{/* 核心指标 - 2x2 网格 */}
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={3}>
<CompactStatCard
label="事件数"
value={summary?.totalEvents || 0}
icon={<FireOutlined />}
color="#F59E0B"
/>
<CompactStatCard
label="关联股票"
value={summary?.totalStocks || 0}
icon={<StockOutlined />}
color="#06B6D4"
/>
<CompactStatCard
label="平均超额"
value={formatChg(summary?.avgChg)}
icon={<RiseOutlined />}
color={summary?.avgChg >= 0 ? '#F31260' : '#17C964'}
/>
<CompactStatCard
label="最大超额"
value={formatChg(summary?.maxChg)}
icon={<ThunderboltOutlined />}
color="#F31260"
/>
</Box>
{/* 分割线 */}
<Box h="1px" bg="rgba(255,255,255,0.06)" />
{/* TOP 表现 - Tab 切换 */}
<Box>
<Tabs
variant="soft-rounded"
colorScheme="yellow"
size="sm"
index={activeTab}
onChange={setActiveTab}
display="flex"
flexDirection="column"
flex="1"
>
<TabList mb={1} flexShrink={0}>
<Tab
fontSize="xs"
py={1}
px={2}
_selected={{ bg: 'rgba(255,215,0,0.2)', color: '#FFD700' }}
color="gray.500"
>
<HStack spacing={1}>
<TrophyOutlined style={{ fontSize: '10px' }} />
<Text>事件TOP10</Text>
</HStack>
</Tab>
<Tab
fontSize="xs"
py={1}
px={2}
_selected={{ bg: 'rgba(255,215,0,0.2)', color: '#FFD700' }}
color="gray.500"
>
<HStack spacing={1}>
<StockOutlined style={{ fontSize: '10px' }} />
<Text>股票TOP10</Text>
</HStack>
</Tab>
</TabList>
<TabPanels flex="1" minH={0}>
{/* 事件 TOP10 */}
<TabPanel p={0} h="100%">
<VStack
spacing={1}
align="stretch"
h="100%"
overflowY="auto"
pr={1}
css={{
'&::-webkit-scrollbar': { width: '4px' },
'&::-webkit-scrollbar-track': { background: 'rgba(255,255,255,0.05)', borderRadius: '2px' },
'&::-webkit-scrollbar-thumb': { background: 'rgba(255,215,0,0.3)', borderRadius: '2px' },
'&::-webkit-scrollbar-thumb:hover': { background: 'rgba(255,215,0,0.5)' },
}}
>
{topPerformers.slice(0, 10).map((event, idx) => (
<TopEventItem key={event.id || idx} event={event} rank={idx + 1} />
))}
{topPerformers.length === 0 && (
<Text fontSize="xs" color="gray.600" textAlign="center" py={2}>
暂无数据
</Text>
)}
</VStack>
</TabPanel>
{/* 股票 TOP10 */}
<TabPanel p={0} h="100%">
<VStack
spacing={1}
align="stretch"
h="100%"
overflowY="auto"
pr={1}
css={{
'&::-webkit-scrollbar': { width: '4px' },
'&::-webkit-scrollbar-track': { background: 'rgba(255,255,255,0.05)', borderRadius: '2px' },
'&::-webkit-scrollbar-thumb': { background: 'rgba(255,215,0,0.3)', borderRadius: '2px' },
'&::-webkit-scrollbar-thumb:hover': { background: 'rgba(255,215,0,0.5)' },
}}
>
{topStocks.slice(0, 10).map((stock, idx) => (
<TopStockItem key={stock.stockCode || idx} stock={stock} rank={idx + 1} />
))}
{topStocks.length === 0 && (
<Text fontSize="xs" color="gray.600" textAlign="center" py={2}>
暂无数据
</Text>
)}
</VStack>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
</VStack>
{hasData ? (
<MotionBox animate={controls} initial={{ y: 0 }}>
<VStack spacing={1} align="stretch">
{displayList.map((event, idx) => (
<TopEventItem
key={`${event.id || idx}-${idx}`}
event={event}
rank={(idx % originalCount) + 1}
/>
))}
</VStack>
</MotionBox>
) : (
<Center h="100%">
<VStack spacing={1}>
<Text color="gray.500" fontSize="sm">
暂无数据
</Text>
{error && (
<Text fontSize="xs" color="gray.600">
{error}
</Text>
)}
</VStack>
</Center>
)}
</Box>
</Box>
);