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

@@ -0,0 +1,65 @@
// src/views/Community/components/EventListSection.js
// 事件列表区域组件包含Loading、Empty、List三种状态
import React from 'react';
import {
Center,
VStack,
Spinner,
Text
} from '@chakra-ui/react';
import EventList from './EventList';
/**
* 事件列表区域组件
* @param {boolean} loading - 加载状态
* @param {Array} events - 事件列表
* @param {Object} pagination - 分页信息
* @param {Function} onPageChange - 分页变化回调
* @param {Function} onEventClick - 事件点击回调
* @param {Function} onViewDetail - 查看详情回调
*/
const EventListSection = ({
loading,
events,
pagination,
onPageChange,
onEventClick,
onViewDetail
}) => {
// Loading 状态
if (loading) {
return (
<Center py={10}>
<VStack>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text color="gray.500">正在加载最新事件...</Text>
</VStack>
</Center>
);
}
// Empty 状态
if (!events || events.length === 0) {
return (
<Center py={10}>
<VStack>
<Text fontSize="lg" color="gray.500">暂无事件数据</Text>
</VStack>
</Center>
);
}
// List 状态
return (
<EventList
events={events}
pagination={pagination}
onPageChange={onPageChange}
onEventClick={onEventClick}
onViewDetail={onViewDetail}
/>
);
};
export default EventListSection;

View File

@@ -0,0 +1,63 @@
// src/views/Community/components/EventModals.js
// 事件弹窗组合组件包含详情Modal和股票Drawer
import React from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton
} from '@chakra-ui/react';
import EventDetailModal from './EventDetailModal';
import StockDetailPanel from './StockDetailPanel';
/**
* 事件弹窗组合组件
* @param {Object} eventModalState - 事件详情Modal状态
* @param {boolean} eventModalState.isOpen - 是否打开
* @param {Function} eventModalState.onClose - 关闭回调
* @param {Object} eventModalState.event - 事件对象
* @param {Function} eventModalState.onEventClose - 事件关闭回调(清除状态)
* @param {Object} stockDrawerState - 股票详情Drawer状态
* @param {boolean} stockDrawerState.visible - 是否显示
* @param {Object} stockDrawerState.event - 事件对象
* @param {Function} stockDrawerState.onClose - 关闭回调
*/
const EventModals = ({
eventModalState,
stockDrawerState
}) => {
return (
<>
{/* 事件详情模态框 - 使用Chakra UI Modal */}
<Modal
isOpen={eventModalState.isOpen}
onClose={eventModalState.onClose}
size="xl"
>
<ModalOverlay />
<ModalContent>
<ModalHeader>事件详情</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<EventDetailModal
event={eventModalState.event}
onClose={eventModalState.onEventClose}
/>
</ModalBody>
</ModalContent>
</Modal>
{/* 股票详情抽屉 - 使用原组件自带的 Antd Drawer */}
<StockDetailPanel
visible={stockDrawerState.visible}
event={stockDrawerState.event}
onClose={stockDrawerState.onClose}
/>
</>
);
};
export default EventModals;

View File

@@ -0,0 +1,80 @@
// src/views/Community/components/EventTimelineCard.js
// 事件时间轴卡片组件整合Header + Search + List
import React, { forwardRef } from 'react';
import {
Card,
CardHeader,
CardBody,
Box,
useColorModeValue
} from '@chakra-ui/react';
import EventTimelineHeader from './EventTimelineHeader';
import UnifiedSearchBox from './UnifiedSearchBox';
import EventListSection from './EventListSection';
/**
* 事件时间轴卡片组件
* @param {Array} events - 事件列表
* @param {boolean} loading - 加载状态
* @param {Object} pagination - 分页信息
* @param {Object} filters - 筛选条件
* @param {Array} popularKeywords - 热门关键词
* @param {Date} lastUpdateTime - 最后更新时间
* @param {Function} onSearch - 搜索回调
* @param {Function} onPageChange - 分页变化回调
* @param {Function} onEventClick - 事件点击回调
* @param {Function} onViewDetail - 查看详情回调
* @param {Object} ref - 用于滚动的ref
*/
const EventTimelineCard = forwardRef(({
events,
loading,
pagination,
filters,
popularKeywords,
lastUpdateTime,
onSearch,
onPageChange,
onEventClick,
onViewDetail
}, ref) => {
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
return (
<Card ref={ref} bg={cardBg} borderColor={borderColor} mb={4}>
{/* 标题部分 */}
<CardHeader>
<EventTimelineHeader lastUpdateTime={lastUpdateTime} />
</CardHeader>
{/* 主体内容 */}
<CardBody>
{/* 统一搜索组件(整合了话题、股票、行业、日期、排序、重要性、热门概念、筛选标签) */}
<Box mb={4}>
<UnifiedSearchBox
onSearch={onSearch}
popularKeywords={popularKeywords}
filters={filters}
loading={loading}
/>
</Box>
{/* 事件列表包含Loading、Empty、List三种状态 */}
<EventListSection
loading={loading}
events={events}
pagination={pagination}
onPageChange={onPageChange}
onEventClick={onEventClick}
onViewDetail={onViewDetail}
/>
</CardBody>
</Card>
);
});
EventTimelineCard.displayName = 'EventTimelineCard';
export default EventTimelineCard;

View File

@@ -0,0 +1,42 @@
// src/views/Community/components/EventTimelineHeader.js
// 事件时间轴标题组件
import React from 'react';
import {
Flex,
VStack,
HStack,
Heading,
Text,
Badge
} from '@chakra-ui/react';
import { TimeIcon } from '@chakra-ui/icons';
/**
* 事件时间轴标题组件
* @param {Date} lastUpdateTime - 最后更新时间
*/
const EventTimelineHeader = ({ lastUpdateTime }) => {
return (
<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>
);
};
export default EventTimelineHeader;

View File

@@ -0,0 +1,38 @@
// src/views/Community/components/HotEventsSection.js
// 热点事件区域组件
import React from 'react';
import {
Card,
CardHeader,
CardBody,
Heading,
useColorModeValue
} from '@chakra-ui/react';
import HotEvents from './HotEvents';
/**
* 热点事件区域组件
* @param {Array} events - 热点事件列表
*/
const HotEventsSection = ({ events }) => {
const cardBg = useColorModeValue('white', 'gray.800');
// 如果没有热点事件,不渲染组件
if (!events || events.length === 0) {
return null;
}
return (
<Card mt={8} bg={cardBg}>
<CardHeader>
<Heading size="md">🔥 热点事件</Heading>
</CardHeader>
<CardBody>
<HotEvents events={events} />
</CardBody>
</Card>
);
};
export default HotEventsSection;

View File

@@ -0,0 +1,91 @@
// src/views/Community/hooks/useEventData.js
// 事件数据加载逻辑 Hook
import { useState, useEffect, useRef, useCallback } from 'react';
import { debounce } from 'lodash';
import { eventService } from '../../../services/eventService';
import { logger } from '../../../utils/logger';
/**
* 事件数据加载 Hook
* @param {Object} filters - 筛选条件
* @param {number} pageSize - 每页数量
* @returns {Object} 事件数据和加载状态
*/
export const useEventData = (filters, pageSize = 10) => {
const [events, setEvents] = useState([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: pageSize,
total: 0
});
const [loading, setLoading] = useState(false);
const [lastUpdateTime, setLastUpdateTime] = useState(new Date());
// 加载事件列表
const loadEvents = useCallback(async (page = 1) => {
logger.debug('useEventData', '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());
logger.debug('useEventData', 'loadEvents 成功', {
count: response.data.events.length,
total: response.data.pagination.total
});
}
} catch (error) {
logger.error('useEventData', 'loadEvents', error, {
page,
filters
});
} finally {
setLoading(false);
}
}, [filters, pagination.pageSize]);
// 创建防抖的 loadEvents 函数500ms 防抖延迟)
const debouncedLoadEvents = useRef(
debounce((page) => {
logger.debug('useEventData', '防抖后执行 loadEvents', { page });
loadEvents(page);
}, 500)
).current;
// 监听 filters 变化,自动加载数据
// 防抖优化:用户快速切换筛选条件时,只执行最后一次请求
useEffect(() => {
logger.debug('useEventData', 'useEffect 触发filters 变化', { filters });
// 使用防抖加载事件
debouncedLoadEvents(filters.page || 1);
// 组件卸载时取消防抖
return () => {
debouncedLoadEvents.cancel();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]); // 监听 filters 状态变化
return {
events,
pagination,
loading,
lastUpdateTime,
loadEvents
};
};

View File

@@ -0,0 +1,76 @@
// src/views/Community/hooks/useEventFilters.js
// 事件筛选逻辑 Hook
import { useState, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { logger } from '../../../utils/logger';
/**
* 事件筛选逻辑 Hook
* @param {Object} options - 配置选项
* @param {Function} options.navigate - 路由导航函数
* @param {Function} options.onEventClick - 事件点击回调
* @param {Object} options.eventTimelineRef - 时间轴ref用于滚动
* @returns {Object} 筛选状态和处理函数
*/
export const useEventFilters = ({ navigate, onEventClick, eventTimelineRef } = {}) => {
const [searchParams] = useSearchParams();
// 筛选参数状态 - 初始化时从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)
};
});
// 更新筛选参数 - 直接替换(由 UnifiedSearchBox 输出完整参数)
const updateFilters = useCallback((newFilters) => {
logger.debug('useEventFilters', 'updateFilters 接收到完整参数', newFilters);
setFilters(newFilters);
}, []);
// 处理分页变化
const handlePageChange = useCallback((page) => {
// 保持现有筛选条件,只更新页码
updateFilters({ ...filters, page });
// 滚动到实时事件时间轴(平滑滚动)
if (eventTimelineRef && eventTimelineRef.current) {
setTimeout(() => {
eventTimelineRef.current.scrollIntoView({
behavior: 'smooth', // 平滑滚动
block: 'start' // 滚动到元素顶部
});
}, 100); // 延迟100ms确保DOM更新
}
}, [filters, updateFilters, eventTimelineRef]);
// 处理事件点击
const handleEventClick = useCallback((event) => {
if (onEventClick) {
onEventClick(event);
}
}, [onEventClick]);
// 处理查看详情
const handleViewDetail = useCallback((eventId) => {
if (navigate) {
navigate(`/event-detail/${eventId}`);
}
}, [navigate]);
return {
filters,
updateFilters,
handlePageChange,
handleEventClick,
handleViewDetail
};
};

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>
{/* 主体内容 */}
<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
{/* 实时事件时间轴卡片 */}
<EventTimelineCard
ref={eventTimelineRef}
events={events}
loading={loading}
pagination={pagination}
filters={filters}
popularKeywords={popularKeywords}
lastUpdateTime={lastUpdateTime}
onSearch={updateFilters}
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();
{/* 事件弹窗 */}
<EventModals
eventModalState={{
isOpen: !!selectedEvent,
onClose: () => setSelectedEvent(null),
event: selectedEvent,
onEventClose: () => setSelectedEvent(null)
}}
/>
</ModalBody>
</ModalContent>
</Modal>
{/* 股票详情抽屉 - 使用原组件自带的 Antd Drawer避免与 Chakra Drawer 重叠导致空白 */}
<StockDetailPanel
visible={!!selectedEventForStock}
event={selectedEventForStock}
onClose={() => {
setSelectedEventForStock(null);
onStockDrawerClose();
stockDrawerState={{
visible: !!selectedEventForStock,
event: selectedEventForStock,
onClose: () => setSelectedEventForStock(null)
}}
/>
</Box>