feat(community): 事件TOP10添加无限滚动动画
- 默认展示8个事件,列表向上无限轮播 - 使用 framer-motion useAnimationControls 实现 - 鼠标悬停时暂停动画
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user