// src/views/Community/components/EventList.js import React, { useState, useEffect } from 'react'; import { Box, VStack, HStack, Text, Button, Badge, Tag, TagLabel, TagLeftIcon, Flex, Avatar, Tooltip, IconButton, Divider, Container, useColorModeValue, Circle, Stat, StatNumber, StatHelpText, StatArrow, ButtonGroup, Heading, SimpleGrid, Card, CardBody, Center, Link, Spacer, Switch, FormControl, FormLabel, useToast, } from '@chakra-ui/react'; import { ViewIcon, ChatIcon, StarIcon, TimeIcon, InfoIcon, WarningIcon, WarningTwoIcon, CheckCircleIcon, TriangleUpIcon, TriangleDownIcon, ArrowForwardIcon, ExternalLinkIcon, ViewOffIcon, } from '@chakra-ui/icons'; import { useNavigate } from 'react-router-dom'; import moment from 'moment'; import { logger } from '../../../utils/logger'; import { getApiBase } from '../../../utils/apiConfig'; import { useEventNotifications } from '../../../hooks/useEventNotifications'; // ========== 工具函数定义在组件外部 ========== // 涨跌颜色配置(中国A股配色:红涨绿跌)- 分档次显示 const getPriceChangeColor = (value) => { if (value === null || value === undefined) return 'gray.500'; const absValue = Math.abs(value); if (value > 0) { // 上涨用红色,根据涨幅大小使用不同深浅 if (absValue >= 3) return 'red.600'; // 深红色:3%以上 if (absValue >= 1) return 'red.500'; // 中红色:1-3% return 'red.400'; // 浅红色:0-1% } else if (value < 0) { // 下跌用绿色,根据跌幅大小使用不同深浅 if (absValue >= 3) return 'green.600'; // 深绿色:3%以上 if (absValue >= 1) return 'green.500'; // 中绿色:1-3% return 'green.400'; // 浅绿色:0-1% } return 'gray.500'; }; const getPriceChangeBg = (value) => { if (value === null || value === undefined) return 'gray.50'; const absValue = Math.abs(value); if (value > 0) { // 上涨背景色 if (absValue >= 3) return 'red.100'; // 深色背景:3%以上 if (absValue >= 1) return 'red.50'; // 中色背景:1-3% return 'red.50'; // 浅色背景:0-1% } else if (value < 0) { // 下跌背景色 if (absValue >= 3) return 'green.100'; // 深色背景:3%以上 if (absValue >= 1) return 'green.50'; // 中色背景:1-3% return 'green.50'; // 浅色背景:0-1% } return 'gray.50'; }; const getPriceChangeBorderColor = (value) => { if (value === null || value === undefined) return 'gray.300'; const absValue = Math.abs(value); if (value > 0) { // 上涨边框色 if (absValue >= 3) return 'red.500'; // 深边框:3%以上 if (absValue >= 1) return 'red.400'; // 中边框:1-3% return 'red.300'; // 浅边框:0-1% } else if (value < 0) { // 下跌边框色 if (absValue >= 3) return 'green.500'; // 深边框:3%以上 if (absValue >= 1) return 'green.400'; // 中边框:1-3% return 'green.300'; // 浅边框:0-1% } return 'gray.300'; }; // 重要性等级配置 - 金融配色方案 const importanceLevels = { 'S': { color: 'purple.600', bgColor: 'purple.50', borderColor: 'purple.200', icon: WarningIcon, label: '极高', dotBg: 'purple.500', }, 'A': { color: 'red.600', bgColor: 'red.50', borderColor: 'red.200', icon: WarningTwoIcon, label: '高', dotBg: 'red.500', }, 'B': { color: 'orange.600', bgColor: 'orange.50', borderColor: 'orange.200', icon: InfoIcon, label: '中', dotBg: 'orange.500', }, 'C': { color: 'green.600', bgColor: 'green.50', borderColor: 'green.200', icon: CheckCircleIcon, label: '低', dotBg: 'green.500', } }; const getImportanceConfig = (importance) => { return importanceLevels[importance] || importanceLevels['C']; }; // 自定义的涨跌箭头组件(修复颜色问题) const PriceArrow = ({ value }) => { if (value === null || value === undefined) return null; const Icon = value > 0 ? TriangleUpIcon : TriangleDownIcon; const color = value > 0 ? 'red.500' : 'green.500'; return ; }; // ========== 主组件 ========== const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetail }) => { const navigate = useNavigate(); const toast = useToast(); const [isCompactMode, setIsCompactMode] = useState(false); // 新增:紧凑模式状态 const [followingMap, setFollowingMap] = useState({}); const [followCountMap, setFollowCountMap] = useState({}); const [localEvents, setLocalEvents] = useState(events); // 用于实时更新的本地事件列表 // 实时事件推送集成 const { isConnected } = useEventNotifications({ eventType: 'all', importance: 'all', enabled: true, onNewEvent: (event) => { logger.info('EventList', '收到新事件推送', event); // 显示 Toast 通知 toast({ title: '新事件发布', description: event.title, status: 'info', duration: 5000, isClosable: true, position: 'top-right', variant: 'left-accent', }); // 将新事件添加到列表顶部(防止重复) setLocalEvents((prevEvents) => { const exists = prevEvents.some(e => e.id === event.id); if (exists) { logger.debug('EventList', '事件已存在,跳过添加', { eventId: event.id }); return prevEvents; } logger.info('EventList', '新事件添加到列表顶部', { eventId: event.id }); // 添加到顶部,最多保留 100 个 return [event, ...prevEvents].slice(0, 100); }); } }); // 同步外部 events 到 localEvents useEffect(() => { setLocalEvents(events); }, [events]); // 初始化关注状态与计数 useEffect(() => { // 初始化计数映射 const initCounts = {}; localEvents.forEach(ev => { initCounts[ev.id] = ev.follower_count || 0; }); setFollowCountMap(initCounts); const loadFollowing = async () => { try { const base = getApiBase(); const res = await fetch(base + '/api/account/events/following', { credentials: 'include' }); const data = await res.json(); if (res.ok && data.success) { const map = {}; (data.data || []).forEach(ev => { map[ev.id] = true; }); setFollowingMap(map); logger.debug('EventList', '关注状态加载成功', { followingCount: Object.keys(map).length }); } } catch (e) { logger.warn('EventList', '加载关注状态失败', { error: e.message }); } }; loadFollowing(); // 仅在 localEvents 更新时重跑 }, [localEvents]); const toggleFollow = async (eventId) => { try { const base = getApiBase(); const res = await fetch(base + `/api/events/${eventId}/follow`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); const data = await res.json(); if (!res.ok || !data.success) throw new Error(data.error || '操作失败'); const isFollowing = data.data?.is_following; const count = data.data?.follower_count ?? 0; setFollowingMap(prev => ({ ...prev, [eventId]: isFollowing })); setFollowCountMap(prev => ({ ...prev, [eventId]: count })); logger.debug('EventList', '关注状态切换成功', { eventId, isFollowing, followerCount: count }); } catch (e) { logger.warn('EventList', '关注操作失败', { eventId, error: e.message }); } }; // 专业的金融配色方案 const bgColor = useColorModeValue('gray.50', 'gray.900'); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); const textColor = useColorModeValue('gray.700', 'gray.200'); const mutedColor = useColorModeValue('gray.500', 'gray.400'); const linkColor = useColorModeValue('blue.600', 'blue.400'); const hoverBg = useColorModeValue('gray.50', 'gray.700'); const renderPriceChange = (value, label) => { if (value === null || value === undefined) { return ( {label}: -- ); } const absValue = Math.abs(value); const isPositive = value > 0; // 根据涨跌幅大小选择不同的颜色深浅 let colorScheme = 'gray'; let variant = 'solid'; if (isPositive) { // 上涨用红色系 if (absValue >= 3) { colorScheme = 'red'; variant = 'solid'; // 深色 } else if (absValue >= 1) { colorScheme = 'red'; variant = 'subtle'; // 中等 } else { colorScheme = 'red'; variant = 'outline'; // 浅色 } } else { // 下跌用绿色系 if (absValue >= 3) { colorScheme = 'green'; variant = 'solid'; // 深色 } else if (absValue >= 1) { colorScheme = 'green'; variant = 'subtle'; // 中等 } else { colorScheme = 'green'; variant = 'outline'; // 浅色 } } const Icon = isPositive ? TriangleUpIcon : TriangleDownIcon; return ( {label}: {isPositive ? '+' : ''}{value.toFixed(2)}% ); }; const handleTitleClick = (e, event) => { e.preventDefault(); e.stopPropagation(); onEventClick(event); }; const handleViewDetailClick = (e, eventId) => { e.stopPropagation(); navigate(`/event-detail/${eventId}`); }; // 精简模式的事件渲染 const renderCompactEvent = (event) => { const importance = getImportanceConfig(event.importance); const isFollowing = !!followingMap[event.id]; const followerCount = followCountMap[event.id] ?? (event.follower_count || 0); return ( {/* 时间线和重要性标记 */} {event.importance || 'C'} {/* 精简事件卡片 */} onEventClick(event)} mb={3} > {/* 左侧:标题和时间 */} handleTitleClick(e, event)} cursor="pointer" noOfLines={1} > {event.title} {moment(event.created_at).format('MM-DD HH:mm')} {event.creator?.username || 'Anonymous'} {/* 右侧:涨跌幅指标 */} {event.related_avg_chg != null ? `${event.related_avg_chg > 0 ? '+' : ''}${event.related_avg_chg.toFixed(2)}%` : '--'} ); }; // 详细模式的事件渲染(原有的渲染方式,但修复了箭头颜色) const renderDetailedEvent = (event) => { const importance = getImportanceConfig(event.importance); const isFollowing = !!followingMap[event.id]; const followerCount = followCountMap[event.id] ?? (event.follower_count || 0); return ( {/* 时间线和重要性标记 */} {event.importance || 'C'} {/* 事件卡片 */} onEventClick(event)} mb={4} > {/* 标题和重要性标签 */} handleTitleClick(e, event)} cursor="pointer" > {event.title} {importance.label}优先级 {/* 元信息 */} {moment(event.created_at).format('YYYY-MM-DD HH:mm')} {event.creator?.username || 'Anonymous'} {/* 描述 */} {event.description} {/* 价格变化指标 */} 平均涨幅 {event.related_avg_chg != null ? ( {event.related_avg_chg > 0 ? '+' : ''}{event.related_avg_chg.toFixed(2)}% ) : ( -- )} 最大涨幅 {event.related_max_chg != null ? ( {event.related_max_chg > 0 ? '+' : ''}{event.related_max_chg.toFixed(2)}% ) : ( -- )} 周涨幅 {event.related_week_chg != null ? ( {event.related_week_chg > 0 ? '+' : ''}{event.related_week_chg.toFixed(2)}% ) : ( -- )} {/* 统计信息和操作按钮 */} {event.view_count || 0} {event.post_count || 0} {followerCount} ); }; // 分页组件 const Pagination = ({ current, total, pageSize, onChange }) => { const totalPages = Math.ceil(total / pageSize); // 计算要显示的页码数组(智能分页) const getPageNumbers = () => { const delta = 2; // 当前页左右各显示2个页码 const range = []; const rangeWithDots = []; // 始终显示第1页 range.push(1); // 显示当前页附近的页码 for (let i = current - delta; i <= current + delta; i++) { if (i > 1 && i < totalPages) { range.push(i); } } // 始终显示最后一页(如果总页数>1) if (totalPages > 1) { range.push(totalPages); } // 去重并排序 const uniqueRange = [...new Set(range)].sort((a, b) => a - b); // 添加省略号 let prev = 0; for (const page of uniqueRange) { if (page - prev === 2) { // 如果只差一个页码,直接显示 rangeWithDots.push(prev + 1); } else if (page - prev > 2) { // 如果差距大于2,显示省略号 rangeWithDots.push('...'); } rangeWithDots.push(page); prev = page; } return rangeWithDots; }; const pageNumbers = getPageNumbers(); return ( {pageNumbers.map((page, index) => { if (page === '...') { return ( ... ); } return ( ); })} 共 {total} 条 ); }; return ( {/* 顶部控制栏:连接状态 + 视图切换 */} {/* WebSocket 连接状态指示器 */} {isConnected ? '🟢 实时推送已开启' : '🔴 实时推送未连接'} {isConnected && ( 新事件将自动推送 )} {/* 视图切换控制 */} 精简模式 setIsCompactMode(e.target.checked)} colorScheme="blue" /> {localEvents.length > 0 ? ( {localEvents.map((event, index) => ( {isCompactMode ? renderCompactEvent(event) : renderDetailedEvent(event) } ))} ) : (
暂无事件数据
)} {pagination.total > 0 && ( )}
); }; export default EventList;