- 更新所有 handler 函数使用 updateFilters 替代 updateUrlParams - handleFilterChange - handlePageChange(移除 loadEvents 调用,由 useEffect 自动触发) - handleKeywordClick - handleRemoveFilterTag(移除 loadEvents 调用) - 重构 useEffect:监听 filters 状态替代 searchParams - 分离 Redux 数据加载到独立的 useEffect - 保持防抖逻辑(500ms) - 简化 useEffect 注释 适配新的状态管理模式,提升性能 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
492 lines
16 KiB
JavaScript
492 lines
16 KiB
JavaScript
// src/views/Community/index.js
|
||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||
import { useSearchParams, 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 { logger } from '../../utils/logger';
|
||
import { useNotification } from '../../contexts/NotificationContext';
|
||
|
||
// 导航栏已由 MainLayout 提供,无需在此导入
|
||
|
||
// const { RangePicker } = DatePicker;
|
||
// const { Option } = AntSelect;
|
||
|
||
const filterLabelMap = {
|
||
date_range: v => v ? `日期: ${v}` : '',
|
||
sort: v => v ? `排序: ${v === 'new' ? '最新' : v === 'hot' ? '热门' : v === 'returns' ? '收益率' : v}` : '',
|
||
importance: v => v && v !== 'all' ? `重要性: ${v}` : '',
|
||
industry_classification: v => v ? `行业: ${v}` : '',
|
||
industry_code: v => v ? `行业代码: ${v}` : '',
|
||
q: v => v ? `关键词: ${v}` : '',
|
||
};
|
||
|
||
const Community = () => {
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
const navigate = useNavigate();
|
||
const dispatch = useDispatch();
|
||
|
||
// Redux状态
|
||
const { popularKeywords, hotEvents } = useSelector(state => state.communityData);
|
||
|
||
// 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);
|
||
|
||
// ⚡ 通知权限引导
|
||
const { showCommunityGuide } = useNotification();
|
||
|
||
// 状态管理
|
||
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 [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') || '',
|
||
search_type: searchParams.get('search_type') || 'topic',
|
||
industry_classification: searchParams.get('industry_classification') || '',
|
||
industry_code: searchParams.get('industry_code') || '',
|
||
page: parseInt(searchParams.get('page') || '1', 10)
|
||
};
|
||
});
|
||
|
||
// 更新筛选参数 - 不再修改URL
|
||
const updateFilters = useCallback((newFilters) => {
|
||
setFilters(prev => ({ ...prev, ...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 handleFilterChange = useCallback((filterType, value) => {
|
||
updateFilters({ [filterType]: value, page: 1 });
|
||
}, [updateFilters]);
|
||
|
||
// 处理分页变化
|
||
const handlePageChange = useCallback((page) => {
|
||
updateFilters({ page });
|
||
|
||
// 滚动到实时事件时间轴(平滑滚动)
|
||
setTimeout(() => {
|
||
if (eventTimelineRef.current) {
|
||
eventTimelineRef.current.scrollIntoView({
|
||
behavior: 'smooth', // 平滑滚动
|
||
block: 'start' // 滚动到元素顶部
|
||
});
|
||
}
|
||
}, 100); // 延迟100ms,确保DOM更新
|
||
}, [updateFilters]);
|
||
|
||
// 处理事件点击
|
||
const handleEventClick = useCallback((event) => {
|
||
setSelectedEventForStock(event);
|
||
onStockDrawerOpen();
|
||
}, [onStockDrawerOpen]);
|
||
|
||
// 处理查看详情
|
||
const handleViewDetail = useCallback((eventId) => {
|
||
navigate(`/event-detail/${eventId}`);
|
||
}, [navigate]);
|
||
|
||
// 处理关键词点击
|
||
const handleKeywordClick = useCallback((keyword) => {
|
||
updateFilters({ q: keyword, page: 1 });
|
||
}, [updateFilters]);
|
||
|
||
|
||
|
||
// 处理标签删除
|
||
const handleRemoveFilterTag = (key) => {
|
||
let reset = '';
|
||
if (key === 'sort') reset = 'new';
|
||
if (key === 'importance') reset = 'all';
|
||
updateFilters({ [key]: reset, page: 1 });
|
||
};
|
||
|
||
// 获取筛选标签
|
||
const filterTags = Object.entries(filters)
|
||
.filter(([key, value]) => {
|
||
if (key === 'industry_code') return !!value;
|
||
if (key === 'importance') return value && value !== 'all';
|
||
if (key === 'sort') return value && value !== 'new';
|
||
if (key === 'date_range') return !!value;
|
||
if (key === 'q') return !!value;
|
||
return false;
|
||
})
|
||
.map(([key, value]) => {
|
||
if (key === 'industry_code') return { key, label: `行业代码: ${value}` };
|
||
return { key, label: filterLabelMap[key] ? filterLabelMap[key](value) : `${key}: ${value}` };
|
||
});
|
||
|
||
// 创建防抖的 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,内部有缓存判断)
|
||
useEffect(() => {
|
||
dispatch(fetchPopularKeywords());
|
||
dispatch(fetchHotEvents());
|
||
}, [dispatch]);
|
||
|
||
// ⚡ 首次访问社区时,延迟显示权限引导
|
||
useEffect(() => {
|
||
if (showCommunityGuide) {
|
||
const timer = setTimeout(() => {
|
||
logger.info('Community', '显示社区权限引导');
|
||
showCommunityGuide();
|
||
}, 5000); // 延迟 5 秒,让用户先浏览页面
|
||
|
||
return () => clearTimeout(timer);
|
||
}
|
||
}, [showCommunityGuide]); // 只在组件挂载时执行一次
|
||
|
||
return (
|
||
<Box minH="100vh" bg={bgColor}>
|
||
{/* 导航栏已由 MainLayout 提供 */}
|
||
|
||
{/* Midjourney风格英雄区域 */}
|
||
<MidjourneyHeroSection />
|
||
|
||
{/* 主内容区域 */}
|
||
<Container maxW="container.xl" py={8}>
|
||
<Grid templateColumns={{ base: '1fr', lg: '2fr 1fr' }} gap={6}>
|
||
|
||
{/* 左侧主要内容 */}
|
||
<GridItem>
|
||
{/* 筛选器 - 需要改造为Chakra UI版本 */}
|
||
<Card mb={4} bg={cardBg} borderColor={borderColor}>
|
||
<CardBody>
|
||
<EventFilters
|
||
filters={filters}
|
||
onFilterChange={handleFilterChange}
|
||
loading={loading}
|
||
/>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 筛选标签 */}
|
||
{filterTags.length > 0 && (
|
||
<Wrap spacing={2} mb={4}>
|
||
{filterTags.map(tag => (
|
||
<WrapItem key={tag.key}>
|
||
<Tag size="md" variant="solid" colorScheme="blue">
|
||
<TagLabel>{tag.label}</TagLabel>
|
||
<TagCloseButton onClick={() => handleRemoveFilterTag(tag.key)} />
|
||
</Tag>
|
||
</WrapItem>
|
||
))}
|
||
</Wrap>
|
||
)}
|
||
|
||
{/* 事件列表卡片 */}
|
||
<Card ref={eventTimelineRef} bg={cardBg} borderColor={borderColor}>
|
||
<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>
|
||
{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>
|
||
</GridItem>
|
||
|
||
{/* 右侧侧边栏 */}
|
||
<GridItem>
|
||
<VStack spacing={4}>
|
||
{/* 搜索框 - 需要改造为Chakra UI版本 */}
|
||
<Card w="full" bg={cardBg}>
|
||
<CardBody>
|
||
<SearchBox
|
||
onSearch={(values) => {
|
||
updateUrlParams({ ...values, page: 1 });
|
||
}}
|
||
/>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 投资日历 - 需要改造为Chakra UI版本 */}
|
||
<Card w="full" bg={cardBg}>
|
||
<CardHeader>
|
||
<Heading size="sm">
|
||
<HStack>
|
||
<CalendarIcon />
|
||
<Text>投资日历</Text>
|
||
</HStack>
|
||
</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<InvestmentCalendar />
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 热门关键词 - 需要改造为Chakra UI版本 */}
|
||
<Card w="full" bg={cardBg}>
|
||
<CardHeader>
|
||
<Heading size="sm">
|
||
<HStack>
|
||
<StarIcon />
|
||
<Text>热门关键词</Text>
|
||
</HStack>
|
||
</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<PopularKeywords
|
||
keywords={popularKeywords}
|
||
onKeywordClick={handleKeywordClick}
|
||
/>
|
||
</CardBody>
|
||
</Card>
|
||
|
||
{/* 重要性说明 - 需要改造为Chakra UI版本 */}
|
||
<Card w="full" bg={cardBg}>
|
||
<CardHeader>
|
||
<Heading size="sm">
|
||
<HStack>
|
||
<InfoIcon />
|
||
<Text>重要性说明</Text>
|
||
</HStack>
|
||
</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<ImportanceLegend />
|
||
</CardBody>
|
||
</Card>
|
||
</VStack>
|
||
</GridItem>
|
||
</Grid>
|
||
|
||
{/* 热点事件 - 需要改造为Chakra UI版本 */}
|
||
{hotEvents.length > 0 && (
|
||
<Card mt={8} bg={cardBg}>
|
||
<CardHeader>
|
||
<Heading size="md">🔥 热点事件</Heading>
|
||
</CardHeader>
|
||
<CardBody>
|
||
<HotEvents events={hotEvents} />
|
||
</CardBody>
|
||
</Card>
|
||
)}
|
||
</Container>
|
||
|
||
{/* Footer区域 */}
|
||
<Box bg={useColorModeValue('gray.100', 'gray.800')} py={6} mt={8}>
|
||
<Container maxW="container.xl">
|
||
<VStack spacing={2}>
|
||
<Text color="gray.500" fontSize="sm">
|
||
© 2024 价值前沿. 保留所有权利.
|
||
</Text>
|
||
<HStack spacing={4} fontSize="xs" color="gray.400">
|
||
<Link
|
||
href="https://beian.mps.gov.cn/#/query/webSearch?code=11010802046286"
|
||
isExternal
|
||
_hover={{ color: 'gray.600' }}
|
||
>
|
||
京公网安备11010802046286号
|
||
</Link>
|
||
<Text>京ICP备2025107343号-1</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</Container>
|
||
</Box>
|
||
|
||
{/* 事件详情模态框 - 使用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();
|
||
}}
|
||
/>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default Community; |