357 lines
9.0 KiB
JavaScript
357 lines
9.0 KiB
JavaScript
/**
|
||
* 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;
|