442 lines
16 KiB
JavaScript
442 lines
16 KiB
JavaScript
// src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js
|
||
// 动态新闻详情面板主组件(组装所有子组件)
|
||
|
||
import React, { useState, useCallback, useEffect } from 'react';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
import {
|
||
Box,
|
||
Card,
|
||
CardBody,
|
||
VStack,
|
||
Text,
|
||
Spinner,
|
||
Center,
|
||
Wrap,
|
||
WrapItem,
|
||
useColorModeValue,
|
||
useToast,
|
||
} from '@chakra-ui/react';
|
||
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 EventHeaderInfo from './EventHeaderInfo';
|
||
import CompactMetaBar from './CompactMetaBar';
|
||
import EventDescriptionSection from './EventDescriptionSection';
|
||
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';
|
||
|
||
/**
|
||
* 动态新闻详情面板主组件
|
||
* @param {Object} props
|
||
* @param {Object} props.event - 事件对象(包含详情数据)
|
||
* @param {boolean} props.showHeader - 是否显示头部信息(默认 true)
|
||
*/
|
||
const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
||
const dispatch = useDispatch();
|
||
const { user } = useAuth();
|
||
const cardBg = PROFESSIONAL_COLORS.background.card;
|
||
const borderColor = PROFESSIONAL_COLORS.border.default;
|
||
const textColor = PROFESSIONAL_COLORS.text.secondary;
|
||
const toast = useToast();
|
||
|
||
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
|
||
const userTier = user?.subscription_type || 'free';
|
||
|
||
// 从 Redux 读取关注状态
|
||
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
||
const isFollowing = event?.id ? (eventFollowStatus[event.id]?.isFollowing || false) : false;
|
||
const followerCount = event?.id ? (eventFollowStatus[event.id]?.followerCount || event.follower_count || 0) : 0;
|
||
|
||
// 🎯 浏览量机制:存储从 API 获取的完整事件详情(包含最新 view_count)
|
||
const [fullEventDetail, setFullEventDetail] = useState(null);
|
||
const [loadingDetail, setLoadingDetail] = useState(false);
|
||
|
||
// 权限判断函数
|
||
const hasAccess = useCallback((requiredTier) => {
|
||
const tierLevel = { free: 0, pro: 1, max: 2 };
|
||
const result = tierLevel[userTier] >= tierLevel[requiredTier];
|
||
return result;
|
||
}, [userTier]);
|
||
|
||
// 升级弹窗状态
|
||
const [upgradeModal, setUpgradeModal] = useState({
|
||
isOpen: false,
|
||
requiredLevel: 'pro',
|
||
featureName: ''
|
||
});
|
||
|
||
// 使用 Hook 获取实时数据(禁用自动加载,改为手动触发)
|
||
const {
|
||
stocks,
|
||
quotes,
|
||
eventDetail,
|
||
historicalEvents,
|
||
expectationScore,
|
||
loading,
|
||
loadStocksData,
|
||
loadHistoricalData,
|
||
loadChainAnalysis
|
||
} = useEventStocks(event?.id, event?.created_at, { autoLoad: false });
|
||
|
||
// 🎯 加载事件详情(增加浏览量)- 与 EventDetailModal 保持一致
|
||
const loadEventDetail = useCallback(async () => {
|
||
if (!event?.id) return;
|
||
|
||
setLoadingDetail(true);
|
||
try {
|
||
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, {
|
||
eventId: event?.id
|
||
});
|
||
} finally {
|
||
setLoadingDetail(false);
|
||
}
|
||
}, [event?.id]);
|
||
|
||
// 相关股票、相关概念、历史事件和传导链的权限
|
||
const canAccessStocks = hasAccess('pro');
|
||
const canAccessConcepts = hasAccess('pro');
|
||
const canAccessHistorical = hasAccess('pro');
|
||
const canAccessTransmission = hasAccess('max');
|
||
|
||
// 子区块折叠状态管理 + 加载追踪
|
||
// 初始值为 false,由 useEffect 根据权限动态设置
|
||
const [isStocksOpen, setIsStocksOpen] = useState(false);
|
||
const [hasLoadedStocks, setHasLoadedStocks] = 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();
|
||
}
|
||
});
|
||
|
||
// 锁定点击处理 - 弹出升级弹窗
|
||
const handleLockedClick = useCallback((featureName, requiredLevel) => {
|
||
setUpgradeModal({
|
||
isOpen: true,
|
||
requiredLevel,
|
||
featureName
|
||
});
|
||
}, []);
|
||
|
||
// 关闭升级弹窗
|
||
const handleCloseUpgradeModal = useCallback(() => {
|
||
setUpgradeModal({
|
||
isOpen: false,
|
||
requiredLevel: 'pro',
|
||
featureName: ''
|
||
});
|
||
}, []);
|
||
|
||
// 相关股票 - 展开时加载(需要 PRO 权限)
|
||
const handleStocksToggle = useCallback(() => {
|
||
const newState = !isStocksOpen;
|
||
setIsStocksOpen(newState);
|
||
|
||
if (newState && !hasLoadedStocks) {
|
||
console.log('%c📊 [相关股票] 首次展开,加载股票数据', 'color: #10B981; font-weight: bold;', { eventId: event?.id });
|
||
loadStocksData();
|
||
setHasLoadedStocks(true);
|
||
}
|
||
}, [isStocksOpen, hasLoadedStocks, loadStocksData, event?.id]);
|
||
|
||
// 相关概念 - 展开/收起(无需加载)
|
||
const handleConceptsToggle = useCallback(() => {
|
||
setIsConceptsOpen(!isConceptsOpen);
|
||
}, [isConceptsOpen]);
|
||
|
||
// 历史事件对比 - 展开时加载
|
||
const handleHistoricalToggle = useCallback(() => {
|
||
const newState = !isHistoricalOpen;
|
||
setIsHistoricalOpen(newState);
|
||
|
||
if (newState && !hasLoadedHistorical) {
|
||
console.log('%c📜 [历史事件] 首次展开,加载历史事件数据', 'color: #3B82F6; font-weight: bold;', { eventId: event?.id });
|
||
loadHistoricalData();
|
||
setHasLoadedHistorical(true);
|
||
}
|
||
}, [isHistoricalOpen, hasLoadedHistorical, loadHistoricalData, event?.id]);
|
||
|
||
// 传导链分析 - 展开时加载
|
||
const handleTransmissionToggle = useCallback(() => {
|
||
const newState = !isTransmissionOpen;
|
||
setIsTransmissionOpen(newState);
|
||
|
||
if (newState && !hasLoadedTransmission) {
|
||
console.log('%c🔗 [传导链] 首次展开,加载传导链数据', 'color: #8B5CF6; font-weight: bold;', { eventId: event?.id });
|
||
loadChainAnalysis();
|
||
setHasLoadedTransmission(true);
|
||
}
|
||
}, [isTransmissionOpen, hasLoadedTransmission, loadChainAnalysis, event?.id]);
|
||
|
||
// 事件切换时重置所有子模块状态
|
||
useEffect(() => {
|
||
console.log('%c🔄 [事件切换] 重置所有子模块状态', 'color: #F59E0B; font-weight: bold;', { eventId: event?.id });
|
||
|
||
// 🎯 加载事件详情(增加浏览量)
|
||
loadEventDetail();
|
||
|
||
// 重置所有加载状态
|
||
setHasLoadedStocks(false);
|
||
setHasLoadedHistorical(false);
|
||
setHasLoadedTransmission(false);
|
||
|
||
// 相关股票默认展开(有权限时)
|
||
if (canAccessStocks) {
|
||
setIsStocksOpen(true);
|
||
// 立即加载股票数据
|
||
console.log('%c📊 [相关股票] 事件切换,加载股票数据', 'color: #10B981; font-weight: bold;', { eventId: event?.id });
|
||
loadStocksData();
|
||
setHasLoadedStocks(true);
|
||
} else {
|
||
setIsStocksOpen(false);
|
||
}
|
||
|
||
setIsConceptsOpen(false);
|
||
setIsHistoricalOpen(false);
|
||
setIsTransmissionOpen(false);
|
||
}, [event?.id, canAccessStocks, userTier, loadStocksData, loadEventDetail]);
|
||
|
||
// 切换关注状态
|
||
const handleToggleFollow = useCallback(async () => {
|
||
if (!event?.id) return;
|
||
dispatch(toggleEventFollow(event.id));
|
||
}, [dispatch, event?.id]);
|
||
|
||
// 切换自选股
|
||
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 (
|
||
<Card bg={cardBg} borderColor={borderColor} borderWidth="1px">
|
||
<CardBody>
|
||
<Text color={textColor} textAlign="center">
|
||
请选择一个事件查看详情
|
||
</Text>
|
||
</CardBody>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
const importance = getImportanceConfig(event.importance);
|
||
|
||
return (
|
||
<Card bg={cardBg} borderColor={borderColor} borderWidth="1px">
|
||
<CardBody position="relative">
|
||
{/* 无头部模式:显示右上角精简信息栏 */}
|
||
{!showHeader && (
|
||
<CompactMetaBar
|
||
event={fullEventDetail || event}
|
||
importance={importance}
|
||
isFollowing={isFollowing}
|
||
followerCount={followerCount}
|
||
onToggleFollow={handleToggleFollow}
|
||
/>
|
||
)}
|
||
|
||
<VStack align="stretch" spacing={3}>
|
||
{/* 头部信息区 - 优先使用完整详情数据(包含最新浏览量) - 可配置显示/隐藏 */}
|
||
{showHeader && (
|
||
<EventHeaderInfo
|
||
event={fullEventDetail || event}
|
||
importance={importance}
|
||
isFollowing={isFollowing}
|
||
followerCount={followerCount}
|
||
onToggleFollow={handleToggleFollow}
|
||
/>
|
||
)}
|
||
|
||
{/* 事件描述 */}
|
||
<EventDescriptionSection description={event.description} />
|
||
|
||
{/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 - 支持精简/详细模式 */}
|
||
<CollapsibleSection
|
||
title="相关股票"
|
||
isOpen={isStocksOpen}
|
||
onToggle={handleStocksToggle}
|
||
count={stocks?.length || 0}
|
||
subscriptionBadge={(() => {
|
||
if (!canAccessStocks) {
|
||
return <SubscriptionBadge tier="pro" size="sm" />;
|
||
}
|
||
return null;
|
||
})()}
|
||
isLocked={!canAccessStocks}
|
||
onLockedClick={() => handleLockedClick('相关股票', 'pro')}
|
||
showModeToggle={canAccessStocks}
|
||
defaultMode="detailed"
|
||
simpleContent={
|
||
loading.stocks || loading.quotes ? (
|
||
<Center py={2}>
|
||
<Spinner size="sm" color="blue.500" />
|
||
<Text ml={2} color={textColor} fontSize="sm">加载股票数据中...</Text>
|
||
</Center>
|
||
) : (
|
||
<Wrap spacing={4}>
|
||
{stocks?.map((stock, index) => (
|
||
<WrapItem key={index}>
|
||
<CompactStockItem
|
||
stock={stock}
|
||
quote={quotes[stock.stock_code]}
|
||
/>
|
||
</WrapItem>
|
||
))}
|
||
</Wrap>
|
||
)
|
||
}
|
||
>
|
||
{loading.stocks || loading.quotes ? (
|
||
<Center py={4}>
|
||
<Spinner size="md" color="blue.500" />
|
||
<Text ml={2} color={textColor}>加载股票数据中...</Text>
|
||
</Center>
|
||
) : (
|
||
<RelatedStocksSection
|
||
stocks={stocks}
|
||
quotes={quotes}
|
||
eventTime={event.created_at}
|
||
watchlistSet={watchlistSet}
|
||
onWatchlistToggle={handleWatchlistToggle}
|
||
/>
|
||
)}
|
||
</CollapsibleSection>
|
||
|
||
{/* 相关概念(可折叠) - 需要 PRO 权限 */}
|
||
<RelatedConceptsSection
|
||
eventTitle={event.title}
|
||
effectiveTradingDate={event.trading_date || event.created_at}
|
||
eventTime={event.created_at}
|
||
isOpen={isConceptsOpen}
|
||
onToggle={handleConceptsToggle}
|
||
subscriptionBadge={!canAccessConcepts ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||
isLocked={!canAccessConcepts}
|
||
onLockedClick={() => handleLockedClick('相关概念', 'pro')}
|
||
/>
|
||
|
||
{/* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */}
|
||
<CollapsibleSection
|
||
title="历史事件对比"
|
||
isOpen={isHistoricalOpen}
|
||
onToggle={handleHistoricalToggle}
|
||
count={historicalEvents?.length || 0}
|
||
subscriptionBadge={!canAccessHistorical ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||
isLocked={!canAccessHistorical}
|
||
onLockedClick={() => handleLockedClick('历史事件对比', 'pro')}
|
||
>
|
||
{loading.historicalEvents ? (
|
||
<Center py={4}>
|
||
<Spinner size="sm" color="blue.500" />
|
||
<Text ml={2} color={textColor} fontSize="sm">加载历史事件...</Text>
|
||
</Center>
|
||
) : (
|
||
<HistoricalEvents
|
||
events={historicalEvents || []}
|
||
expectationScore={expectationScore}
|
||
/>
|
||
)}
|
||
</CollapsibleSection>
|
||
|
||
{/* 传导链分析(可折叠) - 懒加载 - 需要 MAX 权限 */}
|
||
<CollapsibleSection
|
||
title="传导链分析"
|
||
isOpen={isTransmissionOpen}
|
||
onToggle={handleTransmissionToggle}
|
||
subscriptionBadge={!canAccessTransmission ? <SubscriptionBadge tier="max" size="sm" /> : null}
|
||
isLocked={!canAccessTransmission}
|
||
onLockedClick={() => handleLockedClick('传导链分析', 'max')}
|
||
>
|
||
<TransmissionChainAnalysis
|
||
eventId={event.id}
|
||
eventService={eventService}
|
||
/>
|
||
</CollapsibleSection>
|
||
|
||
{/* 讨论区(评论区) - 所有登录用户可用 */}
|
||
<Box>
|
||
<EventCommentSection eventId={event.id} />
|
||
</Box>
|
||
</VStack>
|
||
</CardBody>
|
||
|
||
{/* 升级弹窗 */}
|
||
{upgradeModal.isOpen ? (
|
||
<SubscriptionUpgradeModal
|
||
isOpen={upgradeModal.isOpen}
|
||
onClose={handleCloseUpgradeModal}
|
||
requiredLevel={upgradeModal.requiredLevel}
|
||
featureName={upgradeModal.featureName}
|
||
currentLevel={userTier}
|
||
/>
|
||
): null }
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
export default DynamicNewsDetailPanel;
|