From 97c5ce0d4d7ffdce766a268b0a643bea658371fe Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Sun, 26 Oct 2025 20:31:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E9=A1=B5=E9=9D=A2=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E6=96=87=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (新增) --- .../Community/components/EventListSection.js | 65 ++++ src/views/Community/components/EventModals.js | 63 ++++ .../Community/components/EventTimelineCard.js | 80 +++++ .../components/EventTimelineHeader.js | 42 +++ .../Community/components/HotEventsSection.js | 38 +++ src/views/Community/hooks/useEventData.js | 91 +++++ src/views/Community/hooks/useEventFilters.js | 76 +++++ src/views/Community/index.js | 314 +++--------------- 8 files changed, 500 insertions(+), 269 deletions(-) create mode 100644 src/views/Community/components/EventListSection.js create mode 100644 src/views/Community/components/EventModals.js create mode 100644 src/views/Community/components/EventTimelineCard.js create mode 100644 src/views/Community/components/EventTimelineHeader.js create mode 100644 src/views/Community/components/HotEventsSection.js create mode 100644 src/views/Community/hooks/useEventData.js create mode 100644 src/views/Community/hooks/useEventFilters.js diff --git a/src/views/Community/components/EventListSection.js b/src/views/Community/components/EventListSection.js new file mode 100644 index 00000000..eb2339d3 --- /dev/null +++ b/src/views/Community/components/EventListSection.js @@ -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 ( +
+ + + 正在加载最新事件... + +
+ ); + } + + // Empty 状态 + if (!events || events.length === 0) { + return ( +
+ + 暂无事件数据 + +
+ ); + } + + // List 状态 + return ( + + ); +}; + +export default EventListSection; diff --git a/src/views/Community/components/EventModals.js b/src/views/Community/components/EventModals.js new file mode 100644 index 00000000..eea09066 --- /dev/null +++ b/src/views/Community/components/EventModals.js @@ -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 */} + + + + 事件详情 + + + + + + + + {/* 股票详情抽屉 - 使用原组件自带的 Antd Drawer */} + + + ); +}; + +export default EventModals; diff --git a/src/views/Community/components/EventTimelineCard.js b/src/views/Community/components/EventTimelineCard.js new file mode 100644 index 00000000..9e7ebd3b --- /dev/null +++ b/src/views/Community/components/EventTimelineCard.js @@ -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 ( + + {/* 标题部分 */} + + + + + {/* 主体内容 */} + + {/* 统一搜索组件(整合了话题、股票、行业、日期、排序、重要性、热门概念、筛选标签) */} + + + + + {/* 事件列表(包含Loading、Empty、List三种状态) */} + + + + ); +}); + +EventTimelineCard.displayName = 'EventTimelineCard'; + +export default EventTimelineCard; diff --git a/src/views/Community/components/EventTimelineHeader.js b/src/views/Community/components/EventTimelineHeader.js new file mode 100644 index 00000000..b85c29c7 --- /dev/null +++ b/src/views/Community/components/EventTimelineHeader.js @@ -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 ( + + + + + + 实时事件时间轴 + + + + 全网监控 + 智能捕获 + 深度分析 + + + + 最后更新: {lastUpdateTime.toLocaleTimeString()} + + + ); +}; + +export default EventTimelineHeader; diff --git a/src/views/Community/components/HotEventsSection.js b/src/views/Community/components/HotEventsSection.js new file mode 100644 index 00000000..d8eafd27 --- /dev/null +++ b/src/views/Community/components/HotEventsSection.js @@ -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 ( + + + 🔥 热点事件 + + + + + + ); +}; + +export default HotEventsSection; diff --git a/src/views/Community/hooks/useEventData.js b/src/views/Community/hooks/useEventData.js new file mode 100644 index 00000000..892dce00 --- /dev/null +++ b/src/views/Community/hooks/useEventData.js @@ -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 + }; +}; diff --git a/src/views/Community/hooks/useEventFilters.js b/src/views/Community/hooks/useEventFilters.js new file mode 100644 index 00000000..af028c09 --- /dev/null +++ b/src/views/Community/hooks/useEventFilters.js @@ -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 + }; +}; diff --git a/src/views/Community/index.js b/src/views/Community/index.js index 33a74928..206c67b2 100644 --- a/src/views/Community/index.js +++ b/src/views/Community/index.js @@ -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 = () => { {/* 主内容区域 */} - {/* 实时事件时间轴 - 统一大卡片 */} - - {/* 标题部分 */} - - - - - - - 实时事件时间轴 - - - - 全网监控 - 智能捕获 - 深度分析 - - - - 最后更新: {lastUpdateTime.toLocaleTimeString()} - - - + {/* 实时事件时间轴卡片 */} + - {/* 主体内容 */} - - {/* 统一搜索组件(整合了话题、股票、行业、日期、排序、重要性、热门概念、筛选标签) */} - - - - - {/* 事件列表 */} - {loading ? ( -
- - - 正在加载最新事件... - -
- ) : events.length > 0 ? ( - - ) : ( -
- - 暂无事件数据 - -
- )} -
-
- - {/* 热点事件 - 需要改造为Chakra UI版本 */} - {hotEvents.length > 0 && ( - - - 🔥 热点事件 - - - - - - )} + {/* 热点事件区域 */} +
- {/* 事件详情模态框 - 使用Chakra UI Modal */} - - - - 事件详情 - - - { - setSelectedEvent(null); - onEventModalClose(); - }} - /> - - - - - {/* 股票详情抽屉 - 使用原组件自带的 Antd Drawer,避免与 Chakra Drawer 重叠导致空白 */} - { - setSelectedEventForStock(null); - onStockDrawerClose(); + {/* 事件弹窗 */} + setSelectedEvent(null), + event: selectedEvent, + onEventClose: () => setSelectedEvent(null) + }} + stockDrawerState={{ + visible: !!selectedEventForStock, + event: selectedEventForStock, + onClose: () => setSelectedEventForStock(null) }} />