Files
vf_react/src/views/Community/index.js
2025-10-17 18:59:00 +08:00

484 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Community/index.js
import React, { useState, useEffect, useCallback } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import {
Box,
Container,
Grid,
GridItem,
Card,
CardBody,
CardHeader,
Button,
Text,
Heading,
VStack,
HStack,
Badge,
Spinner,
useToast,
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,
Link,
} from '@chakra-ui/react';
import {
RepeatIcon,
TimeIcon,
InfoIcon,
SearchIcon,
CalendarIcon,
StarIcon,
ChevronRightIcon,
CloseIcon,
} from '@chakra-ui/icons';
// 导入组件
import MidjourneyHeroSection from './components/MidjourneyHeroSection';
import EventFilters from './components/EventFilters';
import EventList from './components/EventList';
import EventDetailModal from './components/EventDetailModal';
import StockDetailPanel from './components/StockDetailPanel';
import SearchBox from './components/SearchBox';
import PopularKeywords from './components/PopularKeywords';
import HotEvents from './components/HotEvents';
import ImportanceLegend from './components/ImportanceLegend';
import InvestmentCalendar from './components/InvestmentCalendar';
import { eventService } from '../../services/eventService';
// 导航栏已由 MainLayout 提供,无需在此导入
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 toast = useToast();
// 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();
// 状态管理
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 [popularKeywords, setPopularKeywords] = useState([]);
const [hotEvents, setHotEvents] = useState([]);
const [lastUpdateTime, setLastUpdateTime] = useState(new Date());
// 从URL获取筛选参数
const getFiltersFromUrl = useCallback(() => {
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)
};
}, [searchParams]);
// 更新URL参数
const updateUrlParams = useCallback((params) => {
const newParams = new URLSearchParams(searchParams);
Object.entries(params).forEach(([key, value]) => {
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
});
setSearchParams(newParams);
}, [searchParams, setSearchParams]);
// 加载事件列表
const loadEvents = useCallback(async (page = 1) => {
setLoading(true);
try {
const filters = getFiltersFromUrl();
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) {
console.error('Failed to load events:', error);
toast({
title: '加载失败',
description: '无法加载事件列表',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
}
}, [getFiltersFromUrl, pagination.pageSize, toast]);
// 加载热门关键词
const loadPopularKeywords = useCallback(async () => {
try {
const response = await eventService.getPopularKeywords(20);
if (response.success) {
setPopularKeywords(response.data);
}
} catch (error) {
console.error('Failed to load popular keywords:', error);
}
}, []);
// 加载热点事件
const loadHotEvents = useCallback(async () => {
try {
const response = await eventService.getHotEvents({ days: 5, limit: 4 });
if (response.success) {
setHotEvents(response.data);
}
} catch (error) {
console.error('Failed to load hot events:', error);
}
}, []);
// 处理筛选变化
const handleFilterChange = useCallback((filterType, value) => {
updateUrlParams({ [filterType]: value, page: 1 });
}, [updateUrlParams]);
// 处理分页变化
const handlePageChange = useCallback((page) => {
updateUrlParams({ page });
loadEvents(page);
window.scrollTo(0, 0);
}, [updateUrlParams, loadEvents]);
// 处理事件点击
const handleEventClick = useCallback((event) => {
setSelectedEventForStock(event);
onStockDrawerOpen();
}, [onStockDrawerOpen]);
// 处理查看详情
const handleViewDetail = useCallback((eventId) => {
navigate(`/event-detail/${eventId}`);
}, [navigate]);
// 处理关键词点击
const handleKeywordClick = useCallback((keyword) => {
updateUrlParams({ q: keyword, page: 1 });
}, [updateUrlParams]);
// 处理标签删除
const handleRemoveFilterTag = (key) => {
let reset = '';
if (key === 'sort') reset = 'new';
if (key === 'importance') reset = 'all';
updateUrlParams({ [key]: reset, page: 1 });
loadEvents(1);
};
// 获取筛选标签
const filters = getFiltersFromUrl();
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}` };
});
// 初始化加载
useEffect(() => {
const page = parseInt(searchParams.get('page') || '1', 10);
loadEvents(page);
loadPopularKeywords();
loadHotEvents();
}, [searchParams, loadEvents, loadPopularKeywords, loadHotEvents]);
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 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;