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 { import {
Box, Box,
Text, Text,
@@ -12,24 +12,11 @@ import {
Center, Center,
Tooltip, Tooltip,
Badge, Badge,
Tabs, } from "@chakra-ui/react";
TabList, import { motion, useAnimationControls } from "framer-motion";
Tab, import { getApiBase } from "@utils/apiConfig";
TabPanels,
TabPanel, const MotionBox = motion.create(Box);
Input,
Flex,
} from '@chakra-ui/react';
import {
FireOutlined,
RiseOutlined,
ThunderboltOutlined,
TrophyOutlined,
StockOutlined,
CalendarOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { getApiBase } from '@utils/apiConfig';
/** /**
* 生成事件详情页 URL * 生成事件详情页 URL
@@ -43,275 +30,31 @@ const getEventDetailUrl = (eventId) => {
* 格式化涨跌幅 * 格式化涨跌幅
*/ */
const formatChg = (val) => { const formatChg = (val) => {
if (val === null || val === undefined) return '-'; if (val === null || val === undefined) return "-";
const num = parseFloat(val); const num = parseFloat(val);
if (isNaN(num)) return '-'; if (isNaN(num)) return "-";
return (num >= 0 ? '+' : '') + num.toFixed(2) + '%'; return (num >= 0 ? "+" : "") + num.toFixed(2) + "%";
}; };
/** /**
* 获取涨跌幅颜色 * 获取涨跌幅颜色
*/ */
const getChgColor = (val) => { const getChgColor = (val) => {
if (val === null || val === undefined) return 'gray.400'; if (val === null || val === undefined) return "gray.400";
const num = parseFloat(val); const num = parseFloat(val);
if (isNaN(num)) return 'gray.400'; if (isNaN(num)) return "gray.400";
if (num > 0) return '#FF4D4F'; if (num > 0) return "#FF4D4F";
if (num < 0) return '#52C41A'; if (num < 0) return "#52C41A";
return 'gray.400'; 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事件列表项 * TOP事件列表项
*/ */
const TopEventItem = ({ event, rank }) => { const TopEventItem = ({ event, rank }) => {
const handleClick = () => { const handleClick = () => {
if (event.id) { 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} px={2}
bg="rgba(0,0,0,0.2)" bg="rgba(0,0,0,0.2)"
borderRadius="md" 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" transition="all 0.15s"
onClick={handleClick} onClick={handleClick}
> >
<Badge <Badge
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'} colorScheme={rank === 1 ? "yellow" : rank === 2 ? "gray" : "orange"}
fontSize="2xs" fontSize="2xs"
px={1.5} px={1.5}
borderRadius="full" borderRadius="full"
@@ -342,7 +85,7 @@ const TopEventItem = ({ event, rank }) => {
color="gray.300" color="gray.300"
flex="1" flex="1"
noOfLines={1} noOfLines={1}
_hover={{ color: '#FFD700' }} _hover={{ color: "#FFD700" }}
> >
{event.title} {event.title}
</Text> </Text>
@@ -354,52 +97,19 @@ const TopEventItem = ({ event, rank }) => {
); );
}; };
/** // 单个事件项高度py=1 约 8px * 2 + 内容约 20px + spacing 4px
* TOP股票列表项 const ITEM_HEIGHT = 32;
*/ const VISIBLE_COUNT = 8;
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>
);
};
const EventDailyStats = () => { const EventDailyStats = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [, setRefreshing] = useState(false);
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState(0); const [isPaused, setIsPaused] = useState(false);
const [selectedDate, setSelectedDate] = useState(''); const controls = useAnimationControls();
const fetchStats = useCallback(async (dateStr = '', isRefresh = false) => { const fetchStats = useCallback(async (isRefresh = false) => {
if (isRefresh) { if (isRefresh) {
setRefreshing(true); setRefreshing(true);
} else { } else {
@@ -408,17 +118,18 @@ const EventDailyStats = () => {
setError(null); setError(null);
try { try {
const apiBase = getApiBase(); const apiBase = getApiBase();
const dateParam = dateStr ? `&date=${dateStr}` : ''; const response = await fetch(
const response = await fetch(`${apiBase}/api/v1/events/effectiveness-stats?days=1${dateParam}`); `${apiBase}/api/v1/events/effectiveness-stats?days=1`
if (!response.ok) throw new Error('获取数据失败'); );
if (!response.ok) throw new Error("获取数据失败");
const data = await response.json(); const data = await response.json();
if (data.success || data.code === 200) { if (data.success || data.code === 200) {
setStats(data.data); setStats(data.data);
} else { } else {
throw new Error(data.message || '数据格式错误'); throw new Error(data.message || "数据格式错误");
} }
} catch (err) { } catch (err) {
console.error('获取事件统计失败:', err); console.error("获取事件统计失败:", err);
setError(err.message); setError(err.message);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -427,29 +138,50 @@ const EventDailyStats = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchStats(selectedDate); fetchStats();
}, [fetchStats, selectedDate]); }, [fetchStats]);
// 自动刷新(仅当选择今天时,每60秒刷新一次 // 自动刷新每60秒刷新一次
useEffect(() => { useEffect(() => {
if (!selectedDate) { const interval = setInterval(() => fetchStats(true), 60 * 1000);
const interval = setInterval(() => fetchStats('', true), 60 * 1000); return () => clearInterval(interval);
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) => { const startAnimation = async () => {
setSelectedDate(e.target.value); await controls.start({
}; y: -totalScrollHeight,
transition: {
duration: originalCount * 2, // 每个item约2秒
ease: "linear",
repeat: Infinity,
repeatType: "loop",
},
});
};
// 手动刷新 startAnimation();
const handleRefresh = () => { }, [needScroll, isPaused, controls, totalScrollHeight, originalCount]);
if (!refreshing) {
fetchStats(selectedDate, true);
}
};
const isToday = !selectedDate;
if (loading) { if (loading) {
return ( return (
@@ -468,34 +200,14 @@ const EventDailyStats = () => {
); );
} }
if (error || !stats) { const hasData = stats && displayList.length > 0;
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;
return ( return (
<Box <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%)" 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)" backdropFilter="blur(20px)"
borderRadius="2xl" borderRadius="xl"
p={4} p={3}
border="1px solid" border="1px solid"
borderColor="rgba(255, 255, 255, 0.08)" borderColor="rgba(255, 255, 255, 0.08)"
position="relative" position="relative"
@@ -528,266 +240,54 @@ const EventDailyStats = () => {
/> />
{/* 标题行 */} {/* 标题行 */}
<Flex justify="space-between" align="center" mb={4}> <HStack spacing={2} mb={2}>
<HStack spacing={3}> <Box
<Box w="3px"
w="4px" h="14px"
h="20px" bg="linear-gradient(180deg, #7C3AED, #06B6D4)"
bg="linear-gradient(180deg, #7C3AED, #06B6D4)" borderRadius="full"
borderRadius="full" boxShadow="0 0 8px rgba(124, 58, 237, 0.5)"
boxShadow="0 0 10px rgba(124, 58, 237, 0.5)" />
/> <Text fontSize="sm" fontWeight="bold" color="white">
<Text fontSize="md" fontWeight="bold" color="white" letterSpacing="wide"> 事件 TOP 排行
{isToday ? '今日统计' : '历史统计'} </Text>
</Text> </HStack>
{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>
{/* 内容区域 - 固定高度滚动 */} {/* 内容区域 - 固定高度显示8个向上滚动轮播 */}
<Box <Box
flex="1" h={`${ITEM_HEIGHT * VISIBLE_COUNT}px`}
overflowY="auto" maxH={`${ITEM_HEIGHT * VISIBLE_COUNT}px`}
pr={1} overflow="hidden"
css={{ position="relative"
'&::-webkit-scrollbar': { width: '4px' }, onMouseEnter={() => setIsPaused(true)}
'&::-webkit-scrollbar-track': { background: 'rgba(255,255,255,0.02)', borderRadius: '2px' }, onMouseLeave={() => setIsPaused(false)}
'&::-webkit-scrollbar-thumb': { background: 'rgba(124, 58, 237, 0.3)', borderRadius: '2px' },
'&::-webkit-scrollbar-thumb:hover': { background: 'rgba(124, 58, 237, 0.5)' },
}}
> >
<VStack spacing={4} align="stretch"> {hasData ? (
{/* 胜率对比仪表盘 */} <MotionBox animate={controls} initial={{ y: 0 }}>
<WinRateGauge <VStack spacing={1} align="stretch">
eventRate={summary?.positiveRate || 0} {displayList.map((event, idx) => (
marketRate={marketStats?.risingRate || 0} <TopEventItem
marketStats={marketStats} key={`${event.id || idx}-${idx}`}
/> event={event}
rank={(idx % originalCount) + 1}
{/* 核心指标 - 2x2 网格 */} />
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={3}> ))}
<CompactStatCard </VStack>
label="事件数" </MotionBox>
value={summary?.totalEvents || 0} ) : (
icon={<FireOutlined />} <Center h="100%">
color="#F59E0B" <VStack spacing={1}>
/> <Text color="gray.500" fontSize="sm">
<CompactStatCard 暂无数据
label="关联股票" </Text>
value={summary?.totalStocks || 0} {error && (
icon={<StockOutlined />} <Text fontSize="xs" color="gray.600">
color="#06B6D4" {error}
/> </Text>
<CompactStatCard )}
label="平均超额" </VStack>
value={formatChg(summary?.avgChg)} </Center>
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>
</Box> </Box>
</Box> </Box>
); );