community增加事件详情

This commit is contained in:
2026-01-08 18:13:24 +08:00
parent 46b1f2452f
commit b889783f6d
2 changed files with 368 additions and 6 deletions

View File

@@ -0,0 +1,356 @@
/**
* 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;

View File

@@ -58,6 +58,7 @@ import dayjs from 'dayjs';
import KLineChartModal from '@components/StockChart/KLineChartModal';
import { FullCalendarPro } from '@components/Calendar';
import ThemeCometChart from './ThemeCometChart';
import EventDailyStats from './EventDailyStats';
const { TabPane } = Tabs;
const { Text: AntText } = Typography;
@@ -2709,10 +2710,15 @@ const HeroPanel = () => {
</HStack>
</Flex>
{/* AI舆情时空决策驾驶舱 - 左侧词频流星图,右侧日历 */}
<Flex gap={6}>
{/* 左侧:词频流星图 - 占 1/2 */}
<Box flex="1" minW="0">
{/* AI舆情时空决策驾驶舱 - 左侧统计面板,中间词频流星图,右侧日历 */}
<Flex gap={5}>
{/* 左侧:今日事件统计 */}
<Box flex="0.7" minW="0">
<EventDailyStats />
</Box>
{/* 中间:词频流星图 */}
<Box flex="1.15" minW="0">
<ThemeCometChart
onThemeSelect={(data) => {
// 当用户点击某个主题时,可以展示相关股票
@@ -2731,8 +2737,8 @@ const HeroPanel = () => {
/>
</Box>
{/* 右侧:综合日历 - 占 1/2 */}
<Box flex="1" minW="0">
{/* 右侧:综合日历 */}
<Box flex="1.15" minW="0">
<CombinedCalendar />
</Box>
</Flex>