feat: 优化事件中心页面 重构后的文件结构

src/views/Community/
     ├── index.js (主组件,150行左右)
     ├── components/
     │   ├── EventTimelineCard.js (新增)
     │   ├── EventTimelineHeader.js (新增)
     │   ├── EventListSection.js (新增)
     │   ├── HotEventsSection.js (新增)
     │   ├── EventModals.js (新增)
     │   ├── UnifiedSearchBox.js (已有)
     │   ├── EventList.js (已有)
     │   └── ...
     └── hooks/
         ├── useEventFilters.js (新增)
         └── useEventData.js (新增)
This commit is contained in:
zdl
2025-10-26 20:31:34 +08:00
parent f1bd9680b6
commit 97c5ce0d4d
8 changed files with 500 additions and 269 deletions

View File

@@ -1,80 +1,30 @@
// src/views/Community/index.js
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { debounce } from 'lodash';
import { fetchPopularKeywords, fetchHotEvents } from '../../store/slices/communityDataSlice';
import {
Box,
Container,
Grid,
GridItem,
Card,
CardBody,
CardHeader,
Button,
Text,
Heading,
VStack,
HStack,
Badge,
Spinner,
Flex,
Tag,
TagLabel,
TagCloseButton,
IconButton,
Wrap,
WrapItem,
Stat,
StatLabel,
StatNumber,
StatHelpText,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
Drawer,
DrawerBody,
DrawerHeader,
DrawerOverlay,
DrawerContent,
DrawerCloseButton,
useDisclosure,
Center,
Image,
Divider,
useColorModeValue,
} from '@chakra-ui/react';
import {
RepeatIcon,
TimeIcon,
InfoIcon,
SearchIcon,
CalendarIcon,
StarIcon,
ChevronRightIcon,
CloseIcon,
} from '@chakra-ui/icons';
// 导入组件
import MidjourneyHeroSection from './components/MidjourneyHeroSection';
import EventList from './components/EventList';
import EventDetailModal from './components/EventDetailModal';
import StockDetailPanel from './components/StockDetailPanel';
import HotEvents from './components/HotEvents';
import UnifiedSearchBox from './components/UnifiedSearchBox'; // 已整合 SearchBox、PopularKeywords、IndustryCascader
import { eventService } from '../../services/eventService';
import EventTimelineCard from './components/EventTimelineCard';
import HotEventsSection from './components/HotEventsSection';
import EventModals from './components/EventModals';
// 导入自定义 Hooks
import { useEventData } from './hooks/useEventData';
import { useEventFilters } from './hooks/useEventFilters';
import { logger } from '../../utils/logger';
import { useNotification } from '../../contexts/NotificationContext';
// 导航栏已由 MainLayout 提供,无需在此导入
const Community = () => {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const dispatch = useDispatch();
@@ -83,12 +33,6 @@ const Community = () => {
// Chakra UI hooks
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
// Modal/Drawer控制
const { isOpen: isEventModalOpen, onOpen: onEventModalOpen, onClose: onEventModalClose } = useDisclosure();
const { isOpen: isStockDrawerOpen, onOpen: onStockDrawerOpen, onClose: onStockDrawerClose } = useDisclosure();
// Ref用于滚动到实时事件时间轴
const eventTimelineRef = useRef(null);
@@ -96,117 +40,18 @@ const Community = () => {
// ⚡ 通知权限引导
const { showCommunityGuide } = useNotification();
// 状态管理
const [events, setEvents] = useState([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0
});
const [loading, setLoading] = useState(false);
// Modal/Drawer状态
const [selectedEvent, setSelectedEvent] = useState(null);
const [selectedEventForStock, setSelectedEventForStock] = useState(null);
const [lastUpdateTime, setLastUpdateTime] = useState(new Date());
// 筛选参数状态 - 初始化时从URL读取,之后只用本地状态
const [filters, setFilters] = useState(() => {
return {
sort: searchParams.get('sort') || 'new',
importance: searchParams.get('importance') || 'all',
date_range: searchParams.get('date_range') || '',
q: searchParams.get('q') || '',
industry_classification: searchParams.get('industry_classification') || '',
industry_code: searchParams.get('industry_code') || '',
stock_code: searchParams.get('stock_code') || '',
page: parseInt(searchParams.get('page') || '1', 10)
};
// 自定义 Hooks
const { filters, updateFilters, handlePageChange, handleEventClick, handleViewDetail } = useEventFilters({
navigate,
onEventClick: (event) => setSelectedEventForStock(event),
eventTimelineRef
});
// 更新筛选参数 - 直接替换(由 UnifiedSearchBox 输出完整参数)
const updateFilters = useCallback((newFilters) => {
logger.debug('Community', 'updateFilters 接收到完整参数', newFilters);
setFilters(newFilters);
}, []);
// 加载事件列表
const loadEvents = useCallback(async (page = 1) => {
logger.debug('Community', 'loadEvents 被调用', { page });
setLoading(true);
try {
const response = await eventService.getEvents({
...filters,
page,
per_page: pagination.pageSize
});
if (response.success) {
setEvents(response.data.events);
setPagination({
current: response.data.pagination.page,
pageSize: response.data.pagination.per_page,
total: response.data.pagination.total
});
setLastUpdateTime(new Date());
}
} catch (error) {
logger.error('Community', 'loadEvents', error, {
page,
filters
});
} finally {
setLoading(false);
}
}, [filters, pagination.pageSize]);
// 处理分页变化
const handlePageChange = useCallback((page) => {
// 保持现有筛选条件,只更新页码
updateFilters({ ...filters, page });
// 滚动到实时事件时间轴(平滑滚动)
setTimeout(() => {
if (eventTimelineRef.current) {
eventTimelineRef.current.scrollIntoView({
behavior: 'smooth', // 平滑滚动
block: 'start' // 滚动到元素顶部
});
}
}, 100); // 延迟100ms确保DOM更新
}, [updateFilters, filters]);
// 处理事件点击
const handleEventClick = useCallback((event) => {
setSelectedEventForStock(event);
onStockDrawerOpen();
}, [onStockDrawerOpen]);
// 处理查看详情
const handleViewDetail = useCallback((eventId) => {
navigate(`/event-detail/${eventId}`);
}, [navigate]);
// 创建防抖的 loadEvents 函数500ms 防抖延迟)
const debouncedLoadEvents = useRef(
debounce((page) => {
logger.debug('Community', '防抖后执行 loadEvents', { page });
loadEvents(page);
}, 500)
).current;
// 加载事件列表 - 监听 filters 状态变化
// 防抖优化:用户快速切换筛选条件时,只执行最后一次请求
useEffect(() => {
logger.debug('Community', 'useEffect 触发filters 变化', { filters });
// 使用防抖加载事件
debouncedLoadEvents(filters.page);
// 组件卸载时取消防抖
return () => {
debouncedLoadEvents.cancel();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]); // 监听本地 filters 状态变化
const { events, pagination, loading, lastUpdateTime } = useEventData(filters);
// 加载热门关键词和热点事件使用Redux内部有缓存判断
useEffect(() => {
@@ -235,106 +80,37 @@ const Community = () => {
{/* 主内容区域 */}
<Container maxW="container.xl" py={8}>
{/* 实时事件时间轴 - 统一大卡片 */}
<Card ref={eventTimelineRef} bg={cardBg} borderColor={borderColor} mb={4}>
{/* 标题部分 */}
<CardHeader>
<Flex justify="space-between" align="center">
<VStack align="start" spacing={1}>
<Heading size="md">
<HStack>
<TimeIcon />
<Text>实时事件时间轴</Text>
</HStack>
</Heading>
<HStack fontSize="sm" color="gray.500">
<Badge colorScheme="green">全网监控</Badge>
<Badge colorScheme="orange">智能捕获</Badge>
<Badge colorScheme="purple">深度分析</Badge>
</HStack>
</VStack>
<Text fontSize="xs" color="gray.500">
最后更新: {lastUpdateTime.toLocaleTimeString()}
</Text>
</Flex>
</CardHeader>
{/* 实时事件时间轴卡片 */}
<EventTimelineCard
ref={eventTimelineRef}
events={events}
loading={loading}
pagination={pagination}
filters={filters}
popularKeywords={popularKeywords}
lastUpdateTime={lastUpdateTime}
onSearch={updateFilters}
onPageChange={handlePageChange}
onEventClick={handleEventClick}
onViewDetail={handleViewDetail}
/>
{/* 主体内容 */}
<CardBody>
{/* 统一搜索组件(整合了话题、股票、行业、日期、排序、重要性、热门概念、筛选标签) */}
<Box mb={4}>
<UnifiedSearchBox
onSearch={updateFilters}
popularKeywords={popularKeywords}
filters={filters}
loading={loading}
/>
</Box>
{/* 事件列表 */}
{loading ? (
<Center py={10}>
<VStack>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text color="gray.500">正在加载最新事件...</Text>
</VStack>
</Center>
) : events.length > 0 ? (
<EventList
events={events}
pagination={pagination}
onPageChange={handlePageChange}
onEventClick={handleEventClick}
onViewDetail={handleViewDetail}
/>
) : (
<Center py={10}>
<VStack>
<Text fontSize="lg" color="gray.500">暂无事件数据</Text>
</VStack>
</Center>
)}
</CardBody>
</Card>
{/* 热点事件 - 需要改造为Chakra UI版本 */}
{hotEvents.length > 0 && (
<Card mt={8} bg={cardBg}>
<CardHeader>
<Heading size="md">🔥 热点事件</Heading>
</CardHeader>
<CardBody>
<HotEvents events={hotEvents} />
</CardBody>
</Card>
)}
{/* 热点事件区域 */}
<HotEventsSection events={hotEvents} />
</Container>
{/* 事件详情模态框 - 使用Chakra UI Modal */}
<Modal isOpen={isEventModalOpen && selectedEvent} onClose={onEventModalClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>事件详情</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<EventDetailModal
event={selectedEvent}
onClose={() => {
setSelectedEvent(null);
onEventModalClose();
}}
/>
</ModalBody>
</ModalContent>
</Modal>
{/* 股票详情抽屉 - 使用原组件自带的 Antd Drawer避免与 Chakra Drawer 重叠导致空白 */}
<StockDetailPanel
visible={!!selectedEventForStock}
event={selectedEventForStock}
onClose={() => {
setSelectedEventForStock(null);
onStockDrawerClose();
{/* 事件弹窗 */}
<EventModals
eventModalState={{
isOpen: !!selectedEvent,
onClose: () => setSelectedEvent(null),
event: selectedEvent,
onEventClose: () => setSelectedEvent(null)
}}
stockDrawerState={{
visible: !!selectedEventForStock,
event: selectedEventForStock,
onClose: () => setSelectedEventForStock(null)
}}
/>
</Box>