363 lines
9.0 KiB
JavaScript
363 lines
9.0 KiB
JavaScript
/**
|
|
* EventEffectivenessStats - 事件有效性统计
|
|
* 展示事件中心的事件有效性数据,证明系统推荐价值
|
|
*/
|
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
import {
|
|
Box,
|
|
Text,
|
|
VStack,
|
|
HStack,
|
|
Spinner,
|
|
Center,
|
|
useToast,
|
|
Stat,
|
|
StatLabel,
|
|
StatNumber,
|
|
StatHelpText,
|
|
StatArrow,
|
|
Progress,
|
|
Badge,
|
|
Divider,
|
|
Tooltip,
|
|
Icon,
|
|
} from '@chakra-ui/react';
|
|
import {
|
|
TrophyOutlined,
|
|
RiseOutlined,
|
|
FireOutlined,
|
|
CheckCircleOutlined,
|
|
ThunderboltOutlined,
|
|
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 StatCard = ({ label, value, icon, color = '#FFD700', subText, trend, progress }) => (
|
|
<Box
|
|
bg="rgba(0,0,0,0.3)"
|
|
borderRadius="lg"
|
|
p={3}
|
|
border="1px solid"
|
|
borderColor="rgba(255,215,0,0.15)"
|
|
_hover={{ borderColor: 'rgba(255,215,0,0.3)', transform: 'translateY(-2px)' }}
|
|
transition="all 0.2s"
|
|
>
|
|
<HStack spacing={2} mb={1}>
|
|
<Box color={color} fontSize="md">
|
|
{icon}
|
|
</Box>
|
|
<Text fontSize="xs" color="gray.400" fontWeight="medium">
|
|
{label}
|
|
</Text>
|
|
</HStack>
|
|
<Text fontSize="xl" fontWeight="bold" color={color}>
|
|
{value}
|
|
</Text>
|
|
{subText && (
|
|
<Text fontSize="xs" color="gray.500" mt={1}>
|
|
{subText}
|
|
</Text>
|
|
)}
|
|
{trend !== undefined && (
|
|
<HStack spacing={1} mt={1}>
|
|
<StatArrow type={trend >= 0 ? 'increase' : 'decrease'} />
|
|
<Text fontSize="xs" color={trend >= 0 ? '#FF4D4F' : '#52C41A'}>
|
|
{Math.abs(trend).toFixed(1)}%
|
|
</Text>
|
|
</HStack>
|
|
)}
|
|
{progress !== undefined && (
|
|
<Progress
|
|
value={progress}
|
|
size="xs"
|
|
colorScheme={progress >= 60 ? 'green' : progress >= 40 ? 'yellow' : 'red'}
|
|
mt={2}
|
|
borderRadius="full"
|
|
bg="rgba(255,255,255,0.1)"
|
|
/>
|
|
)}
|
|
</Box>
|
|
);
|
|
|
|
/**
|
|
* 热门事件列表项
|
|
*/
|
|
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.1)' }}
|
|
transition="all 0.2s"
|
|
>
|
|
<Badge
|
|
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
|
|
fontSize="xs"
|
|
px={2}
|
|
borderRadius="full"
|
|
>
|
|
{rank}
|
|
</Badge>
|
|
<Tooltip label={event.title} placement="top" hasArrow>
|
|
<Text
|
|
fontSize="xs"
|
|
color="gray.200"
|
|
flex="1"
|
|
noOfLines={1}
|
|
cursor="default"
|
|
>
|
|
{event.title}
|
|
</Text>
|
|
</Tooltip>
|
|
<Text
|
|
fontSize="xs"
|
|
fontWeight="bold"
|
|
color={getChgColor(event.max_chg)}
|
|
>
|
|
{formatChg(event.max_chg)}
|
|
</Text>
|
|
</HStack>
|
|
);
|
|
|
|
const EventEffectivenessStats = () => {
|
|
const [loading, setLoading] = useState(true);
|
|
const [stats, setStats] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const toast = useToast();
|
|
|
|
const fetchStats = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const apiBase = getApiBase();
|
|
const response = await fetch(`${apiBase}/api/v1/events/effectiveness-stats?days=30`);
|
|
if (!response.ok) throw new Error('获取数据失败');
|
|
const data = await response.json();
|
|
if (data.code === 200) {
|
|
setStats(data.data);
|
|
} else {
|
|
throw new Error(data.message || '数据格式错误');
|
|
}
|
|
} catch (err) {
|
|
console.error('获取事件有效性统计失败:', err);
|
|
setError(err.message);
|
|
toast({
|
|
title: '获取统计数据失败',
|
|
description: err.message,
|
|
status: 'error',
|
|
duration: 3000,
|
|
isClosable: true,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [toast]);
|
|
|
|
useEffect(() => {
|
|
fetchStats();
|
|
}, [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={4}
|
|
border="1px solid"
|
|
borderColor="rgba(255, 215, 0, 0.15)"
|
|
minH="400px"
|
|
>
|
|
<Center h="350px">
|
|
<Spinner size="lg" 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={4}
|
|
border="1px solid"
|
|
borderColor="rgba(255, 215, 0, 0.15)"
|
|
minH="400px"
|
|
>
|
|
<Center h="350px">
|
|
<VStack spacing={2}>
|
|
<Text color="gray.400">暂无数据</Text>
|
|
<Text fontSize="xs" color="gray.500">{error}</Text>
|
|
</VStack>
|
|
</Center>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const { summary, topPerformers = [] } = stats;
|
|
|
|
return (
|
|
<Box
|
|
bg="linear-gradient(180deg, rgba(25, 32, 55, 0.95) 0%, rgba(17, 24, 39, 0.98) 100%)"
|
|
borderRadius="xl"
|
|
p={4}
|
|
border="1px solid"
|
|
borderColor="rgba(255, 215, 0, 0.15)"
|
|
position="relative"
|
|
overflow="hidden"
|
|
h="100%"
|
|
>
|
|
{/* 背景装饰 */}
|
|
<Box
|
|
position="absolute"
|
|
top="-50%"
|
|
right="-30%"
|
|
w="300px"
|
|
h="300px"
|
|
bg="radial-gradient(circle, rgba(255,215,0,0.08) 0%, transparent 70%)"
|
|
pointerEvents="none"
|
|
/>
|
|
|
|
{/* 标题 */}
|
|
<HStack spacing={2} mb={4}>
|
|
<Box
|
|
w="4px"
|
|
h="20px"
|
|
bg="linear-gradient(180deg, #FFD700, #FFA500)"
|
|
borderRadius="full"
|
|
/>
|
|
<Text
|
|
fontSize="md"
|
|
fontWeight="bold"
|
|
color="white"
|
|
letterSpacing="wide"
|
|
>
|
|
事件有效性统计
|
|
</Text>
|
|
<Badge
|
|
colorScheme="yellow"
|
|
variant="subtle"
|
|
fontSize="xs"
|
|
px={2}
|
|
>
|
|
近30天
|
|
</Badge>
|
|
</HStack>
|
|
|
|
<VStack spacing={4} align="stretch">
|
|
{/* 核心指标 - 2列网格 */}
|
|
<Box
|
|
display="grid"
|
|
gridTemplateColumns="repeat(2, 1fr)"
|
|
gap={3}
|
|
>
|
|
<StatCard
|
|
label="事件总数"
|
|
value={summary?.totalEvents || 0}
|
|
icon={<FireOutlined />}
|
|
color="#FFD700"
|
|
subText="活跃事件"
|
|
/>
|
|
<StatCard
|
|
label="正向率"
|
|
value={`${(summary?.positiveRate || 0).toFixed(1)}%`}
|
|
icon={<CheckCircleOutlined />}
|
|
color={summary?.positiveRate >= 50 ? '#52C41A' : '#FF4D4F'}
|
|
progress={summary?.positiveRate || 0}
|
|
/>
|
|
<StatCard
|
|
label="平均涨幅"
|
|
value={formatChg(summary?.avgChg)}
|
|
icon={<RiseOutlined />}
|
|
color={getChgColor(summary?.avgChg)}
|
|
subText="关联股票"
|
|
/>
|
|
<StatCard
|
|
label="最大涨幅"
|
|
value={formatChg(summary?.maxChg)}
|
|
icon={<ThunderboltOutlined />}
|
|
color="#FF4D4F"
|
|
subText="单事件最佳"
|
|
/>
|
|
</Box>
|
|
|
|
{/* 评分指标 */}
|
|
<Box
|
|
display="grid"
|
|
gridTemplateColumns="repeat(2, 1fr)"
|
|
gap={3}
|
|
>
|
|
<StatCard
|
|
label="投资价值"
|
|
value={(summary?.avgInvestScore || 0).toFixed(0)}
|
|
icon={<StarOutlined />}
|
|
color="#F59E0B"
|
|
progress={summary?.avgInvestScore || 0}
|
|
subText="平均评分"
|
|
/>
|
|
<StatCard
|
|
label="超预期"
|
|
value={(summary?.avgSurpriseScore || 0).toFixed(0)}
|
|
icon={<TrophyOutlined />}
|
|
color="#8B5CF6"
|
|
progress={summary?.avgSurpriseScore || 0}
|
|
subText="惊喜程度"
|
|
/>
|
|
</Box>
|
|
|
|
{/* 分割线 */}
|
|
<Divider borderColor="rgba(255,215,0,0.1)" />
|
|
|
|
{/* TOP表现事件 */}
|
|
<Box>
|
|
<HStack spacing={2} mb={3}>
|
|
<TrophyOutlined style={{ color: '#FFD700', fontSize: '14px' }} />
|
|
<Text fontSize="sm" fontWeight="bold" color="gray.300">
|
|
TOP 表现事件
|
|
</Text>
|
|
</HStack>
|
|
<VStack spacing={2} align="stretch">
|
|
{topPerformers.slice(0, 5).map((event, idx) => (
|
|
<TopEventItem key={event.id || idx} event={event} rank={idx + 1} />
|
|
))}
|
|
{topPerformers.length === 0 && (
|
|
<Text fontSize="xs" color="gray.500" textAlign="center" py={2}>
|
|
暂无数据
|
|
</Text>
|
|
)}
|
|
</VStack>
|
|
</Box>
|
|
</VStack>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default EventEffectivenessStats;
|