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:
65
src/views/Community/components/EventListSection.js
Normal file
65
src/views/Community/components/EventListSection.js
Normal 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;
|
||||||
63
src/views/Community/components/EventModals.js
Normal file
63
src/views/Community/components/EventModals.js
Normal 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;
|
||||||
80
src/views/Community/components/EventTimelineCard.js
Normal file
80
src/views/Community/components/EventTimelineCard.js
Normal 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;
|
||||||
42
src/views/Community/components/EventTimelineHeader.js
Normal file
42
src/views/Community/components/EventTimelineHeader.js
Normal 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;
|
||||||
38
src/views/Community/components/HotEventsSection.js
Normal file
38
src/views/Community/components/HotEventsSection.js
Normal 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;
|
||||||
91
src/views/Community/hooks/useEventData.js
Normal file
91
src/views/Community/hooks/useEventData.js
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
76
src/views/Community/hooks/useEventFilters.js
Normal file
76
src/views/Community/hooks/useEventFilters.js
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,80 +1,30 @@
|
|||||||
// src/views/Community/index.js
|
// src/views/Community/index.js
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { fetchPopularKeywords, fetchHotEvents } from '../../store/slices/communityDataSlice';
|
import { fetchPopularKeywords, fetchHotEvents } from '../../store/slices/communityDataSlice';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
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,
|
useColorModeValue,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
|
||||||
RepeatIcon,
|
|
||||||
TimeIcon,
|
|
||||||
InfoIcon,
|
|
||||||
SearchIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
StarIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
CloseIcon,
|
|
||||||
} from '@chakra-ui/icons';
|
|
||||||
|
|
||||||
|
|
||||||
// 导入组件
|
// 导入组件
|
||||||
import MidjourneyHeroSection from './components/MidjourneyHeroSection';
|
import MidjourneyHeroSection from './components/MidjourneyHeroSection';
|
||||||
import EventList from './components/EventList';
|
import EventTimelineCard from './components/EventTimelineCard';
|
||||||
import EventDetailModal from './components/EventDetailModal';
|
import HotEventsSection from './components/HotEventsSection';
|
||||||
import StockDetailPanel from './components/StockDetailPanel';
|
import EventModals from './components/EventModals';
|
||||||
import HotEvents from './components/HotEvents';
|
|
||||||
import UnifiedSearchBox from './components/UnifiedSearchBox'; // 已整合 SearchBox、PopularKeywords、IndustryCascader
|
// 导入自定义 Hooks
|
||||||
import { eventService } from '../../services/eventService';
|
import { useEventData } from './hooks/useEventData';
|
||||||
|
import { useEventFilters } from './hooks/useEventFilters';
|
||||||
|
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
|
|
||||||
// 导航栏已由 MainLayout 提供,无需在此导入
|
// 导航栏已由 MainLayout 提供,无需在此导入
|
||||||
|
|
||||||
const Community = () => {
|
const Community = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -83,12 +33,6 @@ const Community = () => {
|
|||||||
|
|
||||||
// Chakra UI hooks
|
// Chakra UI hooks
|
||||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
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:用于滚动到实时事件时间轴
|
// Ref:用于滚动到实时事件时间轴
|
||||||
const eventTimelineRef = useRef(null);
|
const eventTimelineRef = useRef(null);
|
||||||
@@ -96,117 +40,18 @@ const Community = () => {
|
|||||||
// ⚡ 通知权限引导
|
// ⚡ 通知权限引导
|
||||||
const { showCommunityGuide } = useNotification();
|
const { showCommunityGuide } = useNotification();
|
||||||
|
|
||||||
// 状态管理
|
// Modal/Drawer状态
|
||||||
const [events, setEvents] = useState([]);
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||||
const [selectedEventForStock, setSelectedEventForStock] = useState(null);
|
const [selectedEventForStock, setSelectedEventForStock] = useState(null);
|
||||||
const [lastUpdateTime, setLastUpdateTime] = useState(new Date());
|
|
||||||
|
|
||||||
// 筛选参数状态 - 初始化时从URL读取,之后只用本地状态
|
// 自定义 Hooks
|
||||||
const [filters, setFilters] = useState(() => {
|
const { filters, updateFilters, handlePageChange, handleEventClick, handleViewDetail } = useEventFilters({
|
||||||
return {
|
navigate,
|
||||||
sort: searchParams.get('sort') || 'new',
|
onEventClick: (event) => setSelectedEventForStock(event),
|
||||||
importance: searchParams.get('importance') || 'all',
|
eventTimelineRef
|
||||||
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 { events, pagination, loading, lastUpdateTime } = useEventData(filters);
|
||||||
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 状态变化
|
|
||||||
|
|
||||||
// 加载热门关键词和热点事件(使用Redux,内部有缓存判断)
|
// 加载热门关键词和热点事件(使用Redux,内部有缓存判断)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -235,106 +80,37 @@ const Community = () => {
|
|||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<Container maxW="container.xl" py={8}>
|
<Container maxW="container.xl" py={8}>
|
||||||
{/* 实时事件时间轴 - 统一大卡片 */}
|
{/* 实时事件时间轴卡片 */}
|
||||||
<Card ref={eventTimelineRef} bg={cardBg} borderColor={borderColor} mb={4}>
|
<EventTimelineCard
|
||||||
{/* 标题部分 */}
|
ref={eventTimelineRef}
|
||||||
<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
|
|
||||||
events={events}
|
events={events}
|
||||||
|
loading={loading}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
|
filters={filters}
|
||||||
|
popularKeywords={popularKeywords}
|
||||||
|
lastUpdateTime={lastUpdateTime}
|
||||||
|
onSearch={updateFilters}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onEventClick={handleEventClick}
|
onEventClick={handleEventClick}
|
||||||
onViewDetail={handleViewDetail}
|
onViewDetail={handleViewDetail}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Center py={10}>
|
|
||||||
<VStack>
|
|
||||||
<Text fontSize="lg" color="gray.500">暂无事件数据</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 热点事件 - 需要改造为Chakra UI版本 */}
|
{/* 热点事件区域 */}
|
||||||
{hotEvents.length > 0 && (
|
<HotEventsSection events={hotEvents} />
|
||||||
<Card mt={8} bg={cardBg}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading size="md">🔥 热点事件</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<HotEvents events={hotEvents} />
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{/* 事件详情模态框 - 使用Chakra UI Modal */}
|
{/* 事件弹窗 */}
|
||||||
<Modal isOpen={isEventModalOpen && selectedEvent} onClose={onEventModalClose} size="xl">
|
<EventModals
|
||||||
<ModalOverlay />
|
eventModalState={{
|
||||||
<ModalContent>
|
isOpen: !!selectedEvent,
|
||||||
<ModalHeader>事件详情</ModalHeader>
|
onClose: () => setSelectedEvent(null),
|
||||||
<ModalCloseButton />
|
event: selectedEvent,
|
||||||
<ModalBody pb={6}>
|
onEventClose: () => setSelectedEvent(null)
|
||||||
<EventDetailModal
|
|
||||||
event={selectedEvent}
|
|
||||||
onClose={() => {
|
|
||||||
setSelectedEvent(null);
|
|
||||||
onEventModalClose();
|
|
||||||
}}
|
}}
|
||||||
/>
|
stockDrawerState={{
|
||||||
</ModalBody>
|
visible: !!selectedEventForStock,
|
||||||
</ModalContent>
|
event: selectedEventForStock,
|
||||||
</Modal>
|
onClose: () => setSelectedEventForStock(null)
|
||||||
|
|
||||||
{/* 股票详情抽屉 - 使用原组件自带的 Antd Drawer,避免与 Chakra Drawer 重叠导致空白 */}
|
|
||||||
<StockDetailPanel
|
|
||||||
visible={!!selectedEventForStock}
|
|
||||||
event={selectedEventForStock}
|
|
||||||
onClose={() => {
|
|
||||||
setSelectedEventForStock(null);
|
|
||||||
onStockDrawerClose();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user