diff --git a/src/mocks/data/events.js b/src/mocks/data/events.js index 652de263..cc6d2d0d 100644 --- a/src/mocks/data/events.js +++ b/src/mocks/data/events.js @@ -845,6 +845,8 @@ export function generateMockEvents(params = {}) { is_ai_generated: i % 4 === 0, // 25% 的事件是AI生成 industry: industry, related_stocks: relatedStocks, // 添加相关股票列表 + historical_events: generateHistoricalEvents(industry, i), // 添加历史事件 + transmission_chain: generateTransmissionChain(industry, i), // 添加传导链数据 }); } diff --git a/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js b/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js new file mode 100644 index 00000000..c4a7274d --- /dev/null +++ b/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js @@ -0,0 +1,41 @@ +// src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js +// 通用可折叠区块组件 + +import React from 'react'; +import { + Box, + Collapse, + useColorModeValue, +} from '@chakra-ui/react'; +import CollapsibleHeader from './CollapsibleHeader'; + +/** + * 通用可折叠区块组件 + * @param {Object} props + * @param {string} props.title - 标题文本 + * @param {boolean} props.isOpen - 是否展开 + * @param {Function} props.onToggle - 切换展开/收起的回调 + * @param {number} props.count - 可选的数量徽章 + * @param {React.ReactNode} props.children - 子内容 + */ +const CollapsibleSection = ({ title, isOpen, onToggle, count = null, children }) => { + const sectionBg = useColorModeValue('gray.50', 'gray.750'); + + return ( + + + + + {children} + + + + ); +}; + +export default CollapsibleSection; diff --git a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js new file mode 100644 index 00000000..9e712c2d --- /dev/null +++ b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js @@ -0,0 +1,207 @@ +// src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js +// 动态新闻详情面板主组件(组装所有子组件) + +import React, { useState, useMemo, useCallback } from 'react'; +import { + Card, + CardBody, + VStack, + Text, + useColorModeValue, + useToast, +} from '@chakra-ui/react'; +import { getImportanceConfig } from '../../../../constants/importanceLevels'; +import { eventService } from '../../../../services/eventService'; +import EventHeaderInfo from './EventHeaderInfo'; +import EventDescriptionSection from './EventDescriptionSection'; +import RelatedConceptsSection from './RelatedConceptsSection'; +import RelatedStocksSection from './RelatedStocksSection'; +import CollapsibleSection from './CollapsibleSection'; +import HistoricalEvents from '../../../EventDetail/components/HistoricalEvents'; +import TransmissionChainAnalysis from '../../../EventDetail/components/TransmissionChainAnalysis'; + +/** + * 动态新闻详情面板主组件 + * @param {Object} props + * @param {Object} props.event - 事件对象(包含详情数据) + */ +const DynamicNewsDetailPanel = ({ event }) => { + const cardBg = useColorModeValue('white', 'gray.800'); + const borderColor = useColorModeValue('gray.200', 'gray.700'); + const textColor = useColorModeValue('gray.600', 'gray.400'); + const toast = useToast(); + + // 折叠状态管理 + const [isStocksOpen, setIsStocksOpen] = useState(true); + const [isHistoricalOpen, setIsHistoricalOpen] = useState(false); + const [isTransmissionOpen, setIsTransmissionOpen] = useState(false); + + // 关注状态管理 + const [isFollowing, setIsFollowing] = useState(false); + const [followerCount, setFollowerCount] = useState(0); + + // 自选股管理(使用 localStorage) + const [watchlistSet, setWatchlistSet] = useState(() => { + try { + const saved = localStorage.getItem('stock_watchlist'); + return saved ? new Set(JSON.parse(saved)) : new Set(); + } catch { + return new Set(); + } + }); + + // 生成模拟行情数据 + const quotes = useMemo(() => { + if (!event?.related_stocks) return {}; + + const quotesData = {}; + event.related_stocks.forEach(stock => { + // 优先使用 stock.daily_change,否则生成随机涨跌幅 + const change = stock.daily_change + ? parseFloat(stock.daily_change) + : (Math.random() * 10 - 3); // -3% ~ +7% + + quotesData[stock.stock_code] = { + change: change, + price: 10 + Math.random() * 90 // 模拟价格 10-100 + }; + }); + + return quotesData; + }, [event?.related_stocks]); + + // 切换关注状态 + const handleToggleFollow = async () => { + try { + if (isFollowing) { + // 取消关注 + await eventService.unfollowEvent(event.id); + setIsFollowing(false); + setFollowerCount(prev => Math.max(0, prev - 1)); + } else { + // 添加关注 + await eventService.followEvent(event.id); + setIsFollowing(true); + setFollowerCount(prev => prev + 1); + } + } catch (error) { + console.error('切换关注状态失败:', error); + } + }; + + // 切换自选股 + const handleWatchlistToggle = useCallback(async (stockCode, isInWatchlist) => { + try { + const newWatchlist = new Set(watchlistSet); + + if (isInWatchlist) { + newWatchlist.delete(stockCode); + toast({ + title: '已移除自选股', + status: 'info', + duration: 2000, + isClosable: true, + }); + } else { + newWatchlist.add(stockCode); + toast({ + title: '已添加至自选股', + status: 'success', + duration: 2000, + isClosable: true, + }); + } + + setWatchlistSet(newWatchlist); + localStorage.setItem('stock_watchlist', JSON.stringify(Array.from(newWatchlist))); + } catch (error) { + console.error('切换自选股失败:', error); + toast({ + title: '操作失败', + description: error.message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + }, [watchlistSet, toast]); + + // 空状态 + if (!event) { + return ( + + + + 请选择一个事件查看详情 + + + + ); + } + + const importance = getImportanceConfig(event.importance); + + return ( + + + + {/* 头部信息区 */} + + + {/* 事件描述 */} + + + {/* 相关概念 */} + + + {/* 相关股票(可折叠) */} + setIsStocksOpen(!isStocksOpen)} + onWatchlistToggle={handleWatchlistToggle} + /> + + {/* 历史事件对比(可折叠) */} + setIsHistoricalOpen(!isHistoricalOpen)} + count={event.historical_events?.length || 0} + > + + + + {/* 传导链分析(可折叠) */} + setIsTransmissionOpen(!isTransmissionOpen)} + > + + + + + + ); +}; + +export default DynamicNewsDetailPanel; diff --git a/src/views/Community/index.js b/src/views/Community/index.js index 41c6cd82..e88cb2b1 100644 --- a/src/views/Community/index.js +++ b/src/views/Community/index.js @@ -110,9 +110,10 @@ const Community = () => { const { generateMockEvents } = await import('../../mocks/data/events'); const mockData = generateMockEvents({ page: 1, per_page: 30 }); - // 调试:检查第一个事件的 related_stocks 数据 - if (mockData.events[0]?.related_stocks) { + // 调试:检查第一个事件的 related_stocks 和 historical_events 数据 + if (mockData.events[0]) { console.log('Mock 数据第一个事件的股票:', mockData.events[0].related_stocks); + console.log('Mock 数据第一个事件的历史事件:', mockData.events[0].historical_events); } setDynamicNewsEvents(mockData.events);