Files
vf_react/src/views/Community/components/EventDailyStats.js
2026-01-08 18:13:24 +08:00

357 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* EventDailyStats - 当日事件统计面板
* 展示当前交易日的事件统计数据,证明系统推荐的胜率和市场热度
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
Box,
Text,
VStack,
HStack,
Spinner,
Center,
Tooltip,
Badge,
Progress,
} from '@chakra-ui/react';
import {
FireOutlined,
RiseOutlined,
CheckCircleOutlined,
ThunderboltOutlined,
TrophyOutlined,
StarOutlined,
} from '@ant-design/icons';
import { getApiBase } from '@utils/apiConfig';
/**
* 格式化涨跌幅
*/
const formatChg = (val) => {
if (val === null || val === undefined) return '-';
const num = parseFloat(val);
if (isNaN(num)) return '-';
return (num >= 0 ? '+' : '') + num.toFixed(2) + '%';
};
/**
* 获取涨跌幅颜色
*/
const getChgColor = (val) => {
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';
};
/**
* 获取胜率颜色
*/
const getWinRateColor = (rate) => {
if (rate >= 70) return '#52C41A'; // 绿色-优秀
if (rate >= 50) return '#FFD700'; // 金色-良好
return '#FF4D4F'; // 红色-待提升
};
/**
* 紧凑数据卡片
*/
const CompactStatCard = ({ label, value, icon, color = '#FFD700', subText, progress }) => (
<Box
bg="rgba(0,0,0,0.25)"
borderRadius="md"
p={2.5}
border="1px solid"
borderColor="rgba(255,215,0,0.12)"
_hover={{ borderColor: 'rgba(255,215,0,0.25)' }}
transition="all 0.2s"
>
<HStack spacing={1.5} mb={1}>
<Box color={color} fontSize="xs">
{icon}
</Box>
<Text fontSize="xs" color="gray.500" fontWeight="medium">
{label}
</Text>
</HStack>
<Text fontSize="lg" fontWeight="bold" color={color} lineHeight="1.2">
{value}
</Text>
{subText && (
<Text fontSize="2xs" color="gray.600" mt={0.5}>
{subText}
</Text>
)}
{progress !== undefined && (
<Progress
value={progress}
size="xs"
colorScheme={progress >= 60 ? 'green' : progress >= 40 ? 'yellow' : 'red'}
mt={1.5}
borderRadius="full"
bg="rgba(255,255,255,0.08)"
h="3px"
/>
)}
</Box>
);
/**
* TOP事件列表项
*/
const TopEventItem = ({ event, rank }) => (
<HStack
spacing={2}
py={1.5}
px={2}
bg="rgba(0,0,0,0.2)"
borderRadius="md"
_hover={{ bg: 'rgba(255,215,0,0.08)' }}
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>
<Tooltip label={event.title} placement="top" hasArrow>
<Text
fontSize="xs"
color="gray.300"
flex="1"
noOfLines={1}
cursor="default"
>
{event.title}
</Text>
</Tooltip>
<Text
fontSize="xs"
fontWeight="bold"
color={getChgColor(event.maxChg)}
>
{formatChg(event.maxChg)}
</Text>
</HStack>
);
const EventDailyStats = () => {
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState(null);
const [error, setError] = useState(null);
const fetchStats = useCallback(async () => {
setLoading(true);
setError(null);
try {
const apiBase = getApiBase();
// 获取当日数据days=1 表示当前交易日)
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 || '数据格式错误');
}
} catch (err) {
console.error('获取当日事件统计失败:', err);
setError(err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchStats();
// 每5分钟刷新一次
const interval = setInterval(fetchStats, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [fetchStats]);
if (loading) {
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%"
minH="380px"
>
<Center h="100%">
<Spinner size="md" color="yellow.400" thickness="3px" />
</Center>
</Box>
);
}
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%"
minH="380px"
>
<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, topPerformers = [], dailyStats = [] } = stats;
// 获取当日TOP事件
const todayTopEvents = dailyStats[0]?.topEvents || topPerformers.slice(0, 3);
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)"
position="relative"
overflow="hidden"
h="100%"
>
{/* 背景装饰 */}
<Box
position="absolute"
top="-30%"
right="-20%"
w="150px"
h="150px"
bg="radial-gradient(circle, rgba(255,215,0,0.06) 0%, transparent 70%)"
pointerEvents="none"
/>
{/* 标题 */}
<HStack spacing={2} mb={3}>
<Box
w="3px"
h="16px"
bg="linear-gradient(180deg, #FFD700, #FFA500)"
borderRadius="full"
/>
<Text
fontSize="sm"
fontWeight="bold"
color="white"
letterSpacing="wide"
>
今日统计
</Text>
<Badge
colorScheme="yellow"
variant="subtle"
fontSize="2xs"
px={1.5}
>
实时
</Badge>
</HStack>
<VStack spacing={3} align="stretch">
{/* 核心指标 - 2x2 网格 */}
<Box
display="grid"
gridTemplateColumns="repeat(2, 1fr)"
gap={2}
>
<CompactStatCard
label="事件数"
value={summary?.totalEvents || 0}
icon={<FireOutlined />}
color="#FFD700"
subText="今日追踪"
/>
<CompactStatCard
label="胜率"
value={`${(summary?.positiveRate || 0).toFixed(0)}%`}
icon={<CheckCircleOutlined />}
color={getWinRateColor(summary?.positiveRate || 0)}
progress={summary?.positiveRate || 0}
/>
<CompactStatCard
label="平均超额"
value={formatChg(summary?.avgChg)}
icon={<RiseOutlined />}
color={getChgColor(summary?.avgChg)}
subText="关联股票"
/>
<CompactStatCard
label="最大超额"
value={formatChg(summary?.maxChg)}
icon={<ThunderboltOutlined />}
color="#FF4D4F"
subText="单事件最佳"
/>
</Box>
{/* 评分指标 */}
<Box
display="grid"
gridTemplateColumns="repeat(2, 1fr)"
gap={2}
>
<CompactStatCard
label="投资价值"
value={(summary?.avgInvestScore || 0).toFixed(0)}
icon={<StarOutlined />}
color="#F59E0B"
progress={summary?.avgInvestScore || 0}
/>
<CompactStatCard
label="超预期分"
value={(summary?.avgSurpriseScore || 0).toFixed(0)}
icon={<TrophyOutlined />}
color="#8B5CF6"
progress={summary?.avgSurpriseScore || 0}
/>
</Box>
{/* 分割线 */}
<Box h="1px" bg="rgba(255,215,0,0.1)" />
{/* TOP表现事件 */}
<Box>
<HStack spacing={1.5} mb={2}>
<TrophyOutlined style={{ color: '#FFD700', fontSize: '12px' }} />
<Text fontSize="xs" fontWeight="bold" color="gray.400">
今日 TOP 表现
</Text>
</HStack>
<VStack spacing={1.5} align="stretch">
{todayTopEvents.slice(0, 3).map((event, idx) => (
<TopEventItem key={event.id || idx} event={event} rank={idx + 1} />
))}
{todayTopEvents.length === 0 && (
<Text fontSize="xs" color="gray.600" textAlign="center" py={2}>
暂无数据
</Text>
)}
</VStack>
</Box>
</VStack>
</Box>
);
};
export default EventDailyStats;