- 迁移 klineDataCache.js 到 src/utils/stock/(被 StockChart 使用) - 迁移 InvestmentCalendar 到 src/components/InvestmentCalendar/(被 Navbar、Dashboard 使用) - 迁移 DynamicNewsDetail 到 src/components/EventDetailPanel/(被 EventDetail 使用) - 更新所有相关导入路径,使用路径别名 - 保持 Community 目录其余结构不变 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
425 lines
15 KiB
JavaScript
425 lines
15 KiB
JavaScript
// src/components/EventDetailPanel/DynamicNewsDetailPanel.js
|
||
// 动态新闻详情面板主组件(组装所有子组件)
|
||
|
||
import React, { useState, useCallback, useEffect, useReducer } from 'react';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
import {
|
||
Card,
|
||
CardBody,
|
||
VStack,
|
||
Text,
|
||
Spinner,
|
||
Center,
|
||
Wrap,
|
||
WrapItem,
|
||
Box,
|
||
} from '@chakra-ui/react';
|
||
import { getImportanceConfig } from '@constants/importanceLevels';
|
||
import { eventService } from '@services/eventService';
|
||
import { useEventStocks } from '@views/Community/components/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 '@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;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 动态新闻详情面板主组件
|
||
* @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;
|
||
|
||
// 使用 useWatchlist Hook 管理自选股
|
||
const {
|
||
handleAddToWatchlist,
|
||
handleRemoveFromWatchlist,
|
||
isInWatchlist,
|
||
loadWatchlistQuotes
|
||
} = useWatchlist();
|
||
|
||
// 获取用户会员等级(修复:字段名从 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 获取实时数据
|
||
// - autoLoad: false - 禁用自动加载所有数据,改为手动触发
|
||
// - autoLoadQuotes: true - 股票数据加载后自动加载行情(相关股票默认展开)
|
||
const {
|
||
stocks,
|
||
quotes,
|
||
eventDetail,
|
||
historicalEvents,
|
||
expectationScore,
|
||
loading,
|
||
loadStocksData,
|
||
loadHistoricalData,
|
||
loadChainAnalysis,
|
||
refreshQuotes
|
||
} = useEventStocks(event?.id, event?.created_at, { autoLoad: false, autoLoadQuotes: true });
|
||
|
||
// 🎯 加载事件详情(增加浏览量)- 与 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);
|
||
}
|
||
} 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');
|
||
|
||
// 子区块折叠状态管理 - 使用 useReducer 整合
|
||
const [sectionState, dispatchSection] = useReducer(sectionReducer, initialSectionState);
|
||
|
||
// 锁定点击处理 - 弹出升级弹窗
|
||
const handleLockedClick = useCallback((featureName, requiredLevel) => {
|
||
setUpgradeModal({
|
||
isOpen: true,
|
||
requiredLevel,
|
||
featureName
|
||
});
|
||
}, []);
|
||
|
||
// 关闭升级弹窗
|
||
const handleCloseUpgradeModal = useCallback(() => {
|
||
setUpgradeModal({
|
||
isOpen: false,
|
||
requiredLevel: 'pro',
|
||
featureName: ''
|
||
});
|
||
}, []);
|
||
|
||
// 相关股票 - 展开时加载行情(需要 PRO 权限)
|
||
const handleStocksToggle = useCallback(() => {
|
||
const willOpen = !sectionState.stocks.isOpen;
|
||
dispatchSection({ type: 'TOGGLE', section: 'stocks' });
|
||
|
||
// 展开时加载行情数据(如果还没加载过)
|
||
if (willOpen && !sectionState.stocks.hasLoadedQuotes && stocks.length > 0) {
|
||
refreshQuotes();
|
||
dispatchSection({ type: 'SET_QUOTES_LOADED' });
|
||
}
|
||
}, [sectionState.stocks, stocks.length, refreshQuotes]);
|
||
|
||
// 相关概念 - 展开/收起(无需加载)
|
||
const handleConceptsToggle = useCallback(() => {
|
||
dispatchSection({ type: 'TOGGLE', section: 'concepts' });
|
||
}, []);
|
||
|
||
// 历史事件对比 - 数据已预加载,只需切换展开状态
|
||
const handleHistoricalToggle = useCallback(() => {
|
||
dispatchSection({ type: 'TOGGLE', section: 'historical' });
|
||
}, []);
|
||
|
||
// 传导链分析 - 展开时加载
|
||
const handleTransmissionToggle = useCallback(() => {
|
||
const willOpen = !sectionState.transmission.isOpen;
|
||
dispatchSection({ type: 'TOGGLE', section: 'transmission' });
|
||
|
||
if (willOpen && !sectionState.transmission.hasLoaded) {
|
||
loadChainAnalysis();
|
||
dispatchSection({ type: 'SET_LOADED', section: 'transmission' });
|
||
}
|
||
}, [sectionState.transmission, loadChainAnalysis]);
|
||
|
||
// 事件切换时重置所有子模块状态
|
||
useEffect(() => {
|
||
// 加载事件详情(增加浏览量)
|
||
loadEventDetail();
|
||
|
||
// 加载自选股数据(用于判断股票是否已关注)
|
||
loadWatchlistQuotes();
|
||
|
||
// 重置所有折叠区块状态
|
||
dispatchSection({ type: 'RESET_ALL' });
|
||
|
||
// 相关股票默认展开,预加载股票列表和行情数据
|
||
if (canAccessStocks) {
|
||
loadStocksData();
|
||
dispatchSection({ type: 'SET_LOADED', section: 'stocks' });
|
||
dispatchSection({ type: 'SET_QUOTES_LOADED' });
|
||
}
|
||
|
||
// 历史事件默认折叠,但预加载数据(显示数量吸引点击)
|
||
if (canAccessHistorical) {
|
||
loadHistoricalData();
|
||
dispatchSection({ type: 'SET_LOADED', section: 'historical' });
|
||
}
|
||
}, [event?.id, canAccessStocks, canAccessHistorical, userTier, loadStocksData, loadHistoricalData, loadEventDetail, loadWatchlistQuotes]);
|
||
|
||
// 切换关注状态
|
||
const handleToggleFollow = useCallback(async () => {
|
||
if (!event?.id) return;
|
||
dispatch(toggleEventFollow(event.id));
|
||
}, [dispatch, event?.id]);
|
||
|
||
// 切换自选股(使用 useWatchlist Hook)
|
||
const handleWatchlistToggle = useCallback(async (stockCode, stockName, currentlyInWatchlist) => {
|
||
if (currentlyInWatchlist) {
|
||
await handleRemoveFromWatchlist(stockCode);
|
||
} else {
|
||
await handleAddToWatchlist(stockCode, stockName);
|
||
}
|
||
}, [handleAddToWatchlist, handleRemoveFromWatchlist]);
|
||
|
||
// 空状态
|
||
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={sectionState.stocks.isOpen}
|
||
onToggle={handleStocksToggle}
|
||
count={stocks?.length || 0}
|
||
subscriptionBadge={!canAccessStocks ? <SubscriptionBadge tier="pro" size="sm" /> : 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}
|
||
isInWatchlist={isInWatchlist}
|
||
onWatchlistToggle={handleWatchlistToggle}
|
||
/>
|
||
)}
|
||
</CollapsibleSection>
|
||
|
||
{/* 相关概念(可折叠) - 需要 PRO 权限 */}
|
||
<RelatedConceptsSection
|
||
eventTitle={event.title}
|
||
effectiveTradingDate={event.trading_date || event.created_at}
|
||
eventTime={event.created_at}
|
||
isOpen={sectionState.concepts.isOpen}
|
||
onToggle={handleConceptsToggle}
|
||
subscriptionBadge={!canAccessConcepts ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||
isLocked={!canAccessConcepts}
|
||
onLockedClick={() => handleLockedClick('相关概念', 'pro')}
|
||
/>
|
||
|
||
{/* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */}
|
||
<CollapsibleSection
|
||
title="历史事件对比"
|
||
isOpen={sectionState.historical.isOpen}
|
||
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={sectionState.transmission.isOpen}
|
||
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;
|