@@ -1,27 +1,24 @@
// src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js
// 动态新闻详情面板主组件(组装所有子组件)
import React , { useState , useCallback , useEffect } from 'react' ;
import React , { useState , useCallback , useEffect , useReducer } from 'react' ;
import { useDispatch , useSelector } from 'react-redux' ;
import {
Box ,
Card ,
CardBody ,
VStack ,
HStack ,
Text ,
Spinner ,
Center ,
Wrap ,
WrapItem ,
useColorModeValue ,
useToast ,
Box ,
} from '@chakra-ui/react' ;
import { getImportanceConfig } from '../../../../ constants/importanceLevels' ;
import { eventService } from '../../../../ services/eventService' ;
import { getImportanceConfig } from '@ constants/importanceLevels' ;
import { eventService } from '@ services/eventService' ;
import { useEventStocks } from '../StockDetailPanel/hooks/useEventStocks' ;
import { toggleEventFollow , selectEventFollowStatus } from '../../../../ store/slices/communityDataSlice' ;
import { useAuth } from '../../../../ contexts/AuthContext' ;
import { toggleEventFollow , selectEventFollowStatus } from '@ store/slices/communityDataSlice' ;
import { useAuth } from '@ contexts/AuthContext' ;
import EventHeaderInfo from './EventHeaderInfo' ;
import CompactMetaBar from './CompactMetaBar' ;
import EventDescriptionSection from './EventDescriptionSection' ;
@@ -29,12 +26,56 @@ import RelatedConceptsSection from './RelatedConceptsSection';
import RelatedStocksSection from './RelatedStocksSection' ;
import CompactStockItem from './CompactStockItem' ;
import CollapsibleSection from './CollapsibleSection' ;
import HistoricalEvents from '../../.. /EventDetail/components/HistoricalEvents' ;
import TransmissionChainAnalysis from '../../.. /EventDetail/components/TransmissionChainAnalysis' ;
import SubscriptionBadge from '../../../../ components/SubscriptionBadge' ;
import SubscriptionUpgradeModal from '../../../../ components/SubscriptionUpgradeModal' ;
import { PROFESSIONAL _COLORS } from '../../../../ constants/professionalTheme' ;
import EventCommentSection from '../../../../components/EventCommentSection ';
import HistoricalEvents from '@views /EventDetail/components/HistoricalEvents' ;
import TransmissionChainAnalysis from '@views /EventDetail/components/TransmissionChainAnalysis' ;
import SubscriptionBadge from '@ components/SubscriptionBadge' ;
import SubscriptionUpgradeModal from '@ components/SubscriptionUpgradeModal' ;
import { PROFESSIONAL _COLORS } from '@ constants/professionalTheme' ;
import { useWatchlist } from '@hooks/useWatchlist ';
import EventCommentSection from '@components/EventCommentSection' ;
// 折叠区块状态管理 - 使用 useReducer 整合
const initialSectionState = {
stocks : { isOpen : true , hasLoaded : false , hasLoadedQuotes : false } ,
concepts : { isOpen : false } ,
historical : { isOpen : false , hasLoaded : false } ,
transmission : { isOpen : false , hasLoaded : false }
} ;
const sectionReducer = ( state , action ) => {
switch ( action . type ) {
case 'TOGGLE' :
return {
... state ,
[ action . section ] : {
... state [ action . section ] ,
isOpen : ! state [ action . section ] . isOpen
}
} ;
case 'SET_LOADED' :
return {
... state ,
[ action . section ] : {
... state [ action . section ] ,
hasLoaded : true
}
} ;
case 'SET_QUOTES_LOADED' :
return {
... state ,
stocks : { ... state . stocks , hasLoadedQuotes : true }
} ;
case 'RESET_ALL' :
return {
stocks : { isOpen : true , hasLoaded : false , hasLoadedQuotes : false } ,
concepts : { isOpen : false } ,
historical : { isOpen : false , hasLoaded : false } ,
transmission : { isOpen : false , hasLoaded : false }
} ;
default :
return state ;
}
} ;
/**
* 动态新闻详情面板主组件
@@ -48,7 +89,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
const cardBg = PROFESSIONAL _COLORS . background . card ;
const borderColor = PROFESSIONAL _COLORS . border . default ;
const textColor = PROFESSIONAL _COLORS . text . secondary ;
const toast = useToast ( ) ;
// 使用 useWatchlist Hook 管理自选股
const {
handleAddToWatchlist ,
handleRemoveFromWatchlist ,
isInWatchlist ,
loadWatchlistQuotes
} = useWatchlist ( ) ;
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
const userTier = user ? . subscription _type || 'free' ;
@@ -101,11 +149,6 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
const response = await eventService . getEventDetail ( event . id ) ;
if ( response . success ) {
setFullEventDetail ( response . data ) ;
console . log ( '%c📊 [浏览量] 事件详情加载成功' , 'color: #10B981; font-weight: bold;' , {
eventId : event . id ,
viewCount : response . data . view _count ,
title : response . data . title
} ) ;
}
} catch ( error ) {
console . error ( '[DynamicNewsDetailPanel] loadEventDetail 失败:' , error , {
@@ -122,30 +165,8 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
const canAccessHistorical = hasAccess ( 'pro' ) ;
const canAccessTransmission = hasAccess ( 'max' ) ;
// 子区块折叠状态管理 + 加载追踪
// 相关股票默认展开
const [ isStocksOpen , setIsStocksOpen ] = useState ( true ) ;
const [ hasLoadedStocks , setHasLoadedStocks ] = useState ( false ) ; // 股票列表是否已加载(获取数量)
const [ hasLoadedQuotes , setHasLoadedQuotes ] = useState ( false ) ; // 行情数据是否已加载
const [ isConceptsOpen , setIsConceptsOpen ] = useState ( false ) ;
// 历史事件默认折叠,但预加载数量
const [ isHistoricalOpen , setIsHistoricalOpen ] = useState ( false ) ;
const [ hasLoadedHistorical , setHasLoadedHistorical ] = useState ( false ) ;
const [ isTransmissionOpen , setIsTransmissionOpen ] = useState ( false ) ;
const [ hasLoadedTransmission , setHasLoadedTransmission ] = useState ( false ) ;
// 自选股管理(使用 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 ( ) ;
}
} ) ;
// 子区块折叠状态管理 - 使用 useReducer 整合
const [ sectionState , dispatchSection ] = useReducer ( sectionReducer , initialSectionState ) ;
// 锁定点击处理 - 弹出升级弹窗
const handleLockedClick = useCallback ( ( featureName , requiredLevel ) => {
@@ -166,87 +187,62 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
} , [ ] ) ;
// 相关股票 - 展开时加载行情(需要 PRO 权限)
// 股票列表在事件切换时预加载(显示数量),行情在展开时才加载
const handleStocksToggle = useCallback ( ( ) => {
const newState = ! isStock sOpen;
setIsStocksOpen ( newState ) ;
const willOpen = ! sectionState . stocks . i sOpen;
dispatchSection ( { type : 'TOGGLE' , section : 'stocks' } ) ;
// 展开时加载行情数据(如果还没加载过)
if ( newState && ! hasLoadedQuotes && stocks . length > 0 ) {
console . log ( '%c📈 [相关股票] 首次展开,加载行情数据' , 'color: #10B981; font-weight: bold;' , {
eventId : event ? . id ,
stockCount : stocks . length
} ) ;
if ( willOpen && ! sectionState . stocks . hasLoadedQuotes && stocks . length > 0 ) {
refreshQuotes ( ) ;
setHasLoadedQuotes ( true ) ;
dispatchSection ( { type : 'SET_QUOTES_LOADED' } ) ;
}
} , [ isStocksOpen , hasLoadedQuote s, stocks . length , refreshQuotes , event ? . id ]) ;
} , [ sectionState . stock s, stocks . length , refreshQuotes ] ) ;
// 相关概念 - 展开/收起(无需加载)
const handleConceptsToggle = useCallback ( ( ) => {
setIsConceptsOpen ( ! isC onceptsOpen ) ;
} , [ isConceptsOpen ] ) ;
dispatchSection ( { type : 'TOGGLE' , section : 'c oncepts' } ) ;
} , [ ] ) ;
// 历史事件对比 - 数据已预加载,只需切换展开状态
const handleHistoricalToggle = useCallback ( ( ) => {
const newState = ! isH istoricalOpen ;
setIsHistoricalOpen ( newState ) ;
// 数据已在事件切换时预加载,这里只需展开
if ( newState ) {
console . log ( '%c📜 [历史事件] 展开(数据已预加载)' , 'color: #3B82F6; font-weight: bold;' , {
eventId : event ? . id ,
count : historicalEvents ? . length || 0
} ) ;
}
} , [ isHistoricalOpen , event ? . id , historicalEvents ? . length ] ) ;
dispatchSection ( { type : 'TOGGLE' , section : 'h istorical' } ) ;
} , [ ] ) ;
// 传导链分析 - 展开时加载
const handleTransmissionToggle = useCallback ( ( ) => {
const newState = ! isT ransmissionOpen;
setIsTransmissionOpen ( newState ) ;
const willOpen = ! sectionState . t ransmission. is Open;
dispatchSection ( { type : 'TOGGLE' , section : 'transmission' } ) ;
if ( newState && ! hasLoadedTransmission ) {
console . log ( '%c🔗 [传导链] 首次展开,加载传导链数据' , 'color: #8B5CF6; font-weight: bold;' , { eventId : event ? . id } ) ;
if ( willOpen && ! sectionState . transmission . hasLoaded ) {
loadChainAnalysis ( ) ;
setHasLoadedT ransmission( true ) ;
dispatchSection ( { type : 'SET_LOADED' , section : 't ransmission' } ) ;
}
} , [ isTransmissionOpen , hasLoadedT ransmission, loadChainAnalysis , event ? . id ]) ;
} , [ sectionState . t ransmission, loadChainAnalysis ] ) ;
// 事件切换时重置所有子模块状态
useEffect ( ( ) => {
console . log ( '%c🔄 [事件切换] 重置所有子模块状态' , 'color: #F59E0B; font-weight: bold;' , { eventId : event ? . id } ) ;
// 🎯 加载事件详情(增加浏览量)
// 加载事件详情(增加浏览量)
loadEventDetail ( ) ;
// 重置所有加载状态
setHasLoadedStocks ( false ) ;
setHasLoadedQuotes ( false ) ; // 重置行情加载状态
setHasLoadedHistorical ( false ) ;
setHasLoadedTransmission ( false ) ;
// 加载自选股数据(用于判断股票是否已关注)
loadWatchlistQuotes ( ) ;
// 重置所有折叠区块状态
dispatchSection ( { type : 'RESET_ALL' } ) ;
// 相关股票默认展开,预加载股票列表和行情数据
setIsStocksOpen ( true ) ;
if ( canAccessStocks ) {
console . log ( '%c📊 [相关股票] 事件切换,预加载股票列表和行情数据' , 'color: #10B981; font-weight: bold;' , { eventId : event ? . id } ) ;
loadStocksData ( ) ;
setHasLoadedStocks ( true ) ;
// 由于默认展开,直接加载行情数据
setHasLoadedQuotes ( true ) ;
dispatchSection ( { type : 'SET_LOADED' , section : 'stocks' } ) ;
dispatchSection ( { type : 'SET_QUOTES_LOADED' } ) ;
}
// 历史事件默认折叠,但预加载数据(显示数量吸引点击)
setIsHistoricalOpen ( false ) ;
if ( canAccessHistorical ) {
console . log ( '%c📜 [历史事件] 事件切换,预加载历史事件(获取数量)' , 'color: #3B82F6; font-weight: bold;' , { eventId : event ? . id } ) ;
loadHistoricalData ( ) ;
setHasLoadedHistorical ( true ) ;
dispatchSection ( { type : 'SET_LOADED' , section : 'historical' } ) ;
}
setIsConceptsOpen ( false ) ;
setIsTransmissionOpen ( false ) ;
} , [ event ? . id , canAccessStocks , canAccessHistorical , userTier , loadStocksData , loadHistoricalData , loadEventDetail ] ) ;
} , [ event ? . id , canAccessStocks , canAccessHistorical , userTier , loadStocksData , loadHistoricalData , loadEventDetail , loadWatchlistQuotes ] ) ;
// 切换关注状态
const handleToggleFollow = useCallback ( async ( ) => {
@@ -254,42 +250,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
dispatch ( toggleEventFollow ( event . id ) ) ;
} , [ dispatch , event ? . id ] ) ;
// 切换自选股
const handleWatchlistToggle = useCallback ( async ( stockCode , i sInWatchlist) => {
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 ,
} ) ;
// 切换自选股(使用 useWatchlist Hook)
const handleWatchlistToggle = useCallback ( async ( stockCode , stockName , currently InWatchlist ) => {
if ( currentlyInWatchlist ) {
await handleRemoveFromWatchlist ( stockCode ) ;
} else {
await handleAddToWatchlist ( stockCode , stockName ) ;
}
} , [ w atchlistSet , toa st] ) ;
} , [ handleAddToW atchlist, handleRemoveFromWatchli st] ) ;
// 空状态
if ( ! event ) {
@@ -338,15 +306,10 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
{ /* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 - 支持精简/详细模式 */ }
< CollapsibleSection
title = "相关股票"
isOpen = { isStock sOpen}
isOpen = { sectionState . stocks . i sOpen}
onToggle = { handleStocksToggle }
count = { stocks ? . length || 0 }
subscriptionBadge = { ( ( ) => {
if ( ! canAccessStocks ) {
return < SubscriptionBadge tier = "pro" size = "sm" / > ;
}
return null ;
} ) ( ) }
subscriptionBadge = { ! canAccessStocks ? < SubscriptionBadge tier = "pro" size = "sm" / > : null }
isLocked = { ! canAccessStocks }
onLockedClick = { ( ) => handleLockedClick ( '相关股票' , 'pro' ) }
showModeToggle = { canAccessStocks }
@@ -381,7 +344,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
stocks = { stocks }
quotes = { quotes }
eventTime = { event . created _at }
w atchlistSet = { w atchlistSet }
isInW atchlist= { isInW atchlist}
onWatchlistToggle = { handleWatchlistToggle }
/ >
) }
@@ -392,7 +355,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
eventTitle = { event . title }
effectiveTradingDate = { event . trading _date || event . created _at }
eventTime = { event . created _at }
isOpen = { isConcept sOpen}
isOpen = { sectionState . concepts . i sOpen}
onToggle = { handleConceptsToggle }
subscriptionBadge = { ! canAccessConcepts ? < SubscriptionBadge tier = "pro" size = "sm" / > : null }
isLocked = { ! canAccessConcepts }
@@ -402,7 +365,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
{ /* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */ }
< CollapsibleSection
title = "历史事件对比"
isOpen = { isH istoricalOpen}
isOpen = { sectionState . h istorical. is Open}
onToggle = { handleHistoricalToggle }
count = { historicalEvents ? . length || 0 }
subscriptionBadge = { ! canAccessHistorical ? < SubscriptionBadge tier = "pro" size = "sm" / > : null }
@@ -425,7 +388,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
{ /* 传导链分析(可折叠) - 懒加载 - 需要 MAX 权限 */ }
< CollapsibleSection
title = "传导链分析"
isOpen = { isT ransmissionOpen}
isOpen = { sectionState . t ransmission. is Open}
onToggle = { handleTransmissionToggle }
subscriptionBadge = { ! canAccessTransmission ? < SubscriptionBadge tier = "max" size = "sm" / > : null }
isLocked = { ! canAccessTransmission }
@@ -453,7 +416,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
featureName = { upgradeModal . featureName }
currentLevel = { userTier }
/ >
) : null }
) : null }
< / C a r d >
) ;
} ;