// src/views/Community/components/EventList.js import React, { useState, useEffect } from 'react'; import { Box, VStack, HStack, Text, Button, Badge, Flex, Container, useColorModeValue, Switch, FormControl, FormLabel, useToast, Center, } from '@chakra-ui/react'; import { InfoIcon } from '@chakra-ui/icons'; import { useNavigate } from 'react-router-dom'; // 导入工具函数和常量 import { logger } from '../../../utils/logger'; import { getApiBase } from '../../../utils/apiConfig'; import { useEventNotifications } from '../../../hooks/useEventNotifications'; import { browserNotificationService } from '../../../services/browserNotificationService'; import { useNotification } from '../../../contexts/NotificationContext'; import { getImportanceConfig } from '../../../constants/importanceLevels'; // 导入子组件 import EventCard from './EventCard'; // ========== 主组件 ========== 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); // 用于实时更新的本地事件列表 // 从 NotificationContext 获取推送权限相关状态和方法 const { browserPermission, requestBrowserPermission } = useNotification(); // 实时事件推送集成 const { isConnected } = useEventNotifications({ eventType: 'all', importance: 'all', enabled: true, onNewEvent: (event) => { console.log('\n[EventList DEBUG] ========== EventList 收到新事件 =========='); console.log('[EventList DEBUG] 事件数据:', event); console.log('[EventList DEBUG] 事件 ID:', event?.id); console.log('[EventList DEBUG] 事件标题:', event?.title); logger.info('EventList', '收到新事件推送', event); console.log('[EventList DEBUG] 准备显示 Toast 通知'); // 显示 Toast 通知 - 更明显的配置 const toastId = toast({ title: '🔔 新事件发布', description: event.title, status: 'success', // 改为 success,更醒目 duration: 8000, // 延长显示时间到 8 秒 isClosable: true, position: 'top', // 改为顶部居中,更显眼 variant: 'solid', // 改为 solid,背景更明显 }); console.log('[EventList DEBUG] ✓ Toast 通知已调用,ID:', toastId); // 发送浏览器原生通知 console.log('[EventList DEBUG] 准备发送浏览器原生通知'); console.log('[EventList DEBUG] 通知权限状态:', browserPermission); if (browserPermission === 'granted') { const importance = getImportanceConfig(event.importance); const notification = browserNotificationService.sendNotification({ title: `🔔 ${importance.label}级事件`, body: event.title, tag: `event_${event.id}`, data: { link: `/event-detail/${event.id}`, eventId: event.id, }, autoClose: 10000, // 10秒后自动关闭 }); if (notification) { browserNotificationService.setupClickHandler(notification, navigate); console.log('[EventList DEBUG] ✓ 浏览器原生通知已发送'); } else { console.log('[EventList DEBUG] ⚠️ 浏览器原生通知发送失败'); } } else { console.log('[EventList DEBUG] ⚠️ 浏览器通知权限未授予,跳过原生通知'); } console.log('[EventList DEBUG] 准备更新事件列表'); // 将新事件添加到列表顶部(防止重复) setLocalEvents((prevEvents) => { console.log('[EventList DEBUG] 当前事件列表数量:', prevEvents.length); const exists = prevEvents.some(e => e.id === event.id); console.log('[EventList DEBUG] 事件是否已存在:', exists); if (exists) { logger.debug('EventList', '事件已存在,跳过添加', { eventId: event.id }); console.log('[EventList DEBUG] ⚠️ 事件已存在,跳过添加'); return prevEvents; } logger.info('EventList', '新事件添加到列表顶部', { eventId: event.id }); console.log('[EventList DEBUG] ✓ 新事件添加到列表顶部'); // 添加到顶部,最多保留 100 个 const updatedEvents = [event, ...prevEvents].slice(0, 100); console.log('[EventList DEBUG] 更新后事件列表数量:', updatedEvents.length); return updatedEvents; }); console.log('[EventList DEBUG] ✓ 事件列表更新完成'); console.log('[EventList DEBUG] ========== EventList 处理完成 ==========\n'); } }); // 同步外部 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 handlePushToggle = async (e) => { const isChecked = e.target.checked; if (isChecked) { // 用户想开启推送 logger.info('EventList', '用户请求开启推送'); const permission = await requestBrowserPermission(); if (permission === 'denied') { // 权限被拒绝,显示设置指引 logger.warn('EventList', '用户拒绝了推送权限'); toast({ title: '推送权限被拒绝', description: '如需开启推送,请在浏览器设置中允许通知权限', status: 'warning', duration: 5000, isClosable: true, position: 'top', }); } else if (permission === 'granted') { logger.info('EventList', '推送权限已授予'); } } else { // 用户想关闭推送 - 提示需在浏览器设置中操作 logger.info('EventList', '用户尝试关闭推送'); toast({ title: '关闭推送通知', description: '如需关闭,请在浏览器设置中撤销通知权限', status: 'info', duration: 5000, isClosable: true, position: 'top', }); } }; // 专业的金融配色方案 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 handleTitleClick = (e, event) => { e.preventDefault(); e.stopPropagation(); onEventClick(event); }; const handleViewDetailClick = (e, eventId) => { e.stopPropagation(); navigate(`/event-detail/${eventId}`); }; // 时间轴样式配置(固定使用轻量卡片样式) const getTimelineBoxStyle = () => { return { bg: useColorModeValue('gray.50', 'gray.700'), borderColor: useColorModeValue('gray.400', 'gray.500'), borderWidth: '2px', textColor: useColorModeValue('blue.600', 'blue.400'), boxShadow: 'sm', }; }; // 分页组件 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 ( {/* 顶部控制栏:左空白 + 中间分页器 + 右侧控制(固定sticky) - 铺满全宽 */} {/* 左侧占位 */} {/* 中间:分页器 */} {pagination.total > 0 && localEvents.length > 0 ? ( 第 {pagination.current} / {Math.ceil(pagination.total / pagination.pageSize)} 页 共 {pagination.total} 条 ) : ( )} {/* 右侧:控制按钮 */} {/* WebSocket 连接状态 */} {isConnected ? '🟢 实时' : '🔴 离线'} {/* 桌面推送开关 */} 推送 {/* 视图切换控制 */} 精简 setIsCompactMode(e.target.checked)} colorScheme="blue" /> {/* 事件列表内容 */} {localEvents.length > 0 ? ( {localEvents.map((event, index) => ( ))} ) : (
暂无事件数据
)} {pagination.total > 0 && ( )}
); }; export default EventList;