diff --git a/src/components/SubscriptionBadge.js b/src/components/SubscriptionBadge.js new file mode 100644 index 00000000..243a81e9 --- /dev/null +++ b/src/components/SubscriptionBadge.js @@ -0,0 +1,97 @@ +// src/components/SubscriptionBadge.js +// 会员专享标签组件 + +import React from 'react'; +import { + Badge, + HStack, + Icon, + Text, + useColorModeValue, +} from '@chakra-ui/react'; +import { FaStar, FaCrown } from 'react-icons/fa'; +import { logger } from '../utils/logger'; + +/** + * 会员专享标签组件 + * @param {Object} props + * @param {'pro' | 'max'} props.tier - 会员等级:pro 或 max + * @param {'sm' | 'md'} props.size - 标签尺寸 + */ +const SubscriptionBadge = ({ tier = 'pro', size = 'sm' }) => { + // 🔍 调试:SubscriptionBadge 被渲染(强制输出) + console.error('🔴 [DEBUG] SubscriptionBadge 组件被渲染 - 这不应该出现(max 会员)', { + tier, + size, + 调用位置: new Error().stack + }); + + // 🔍 调试:SubscriptionBadge 被渲染(logger) + logger.debug('SubscriptionBadge', '组件被渲染', { + tier, + size, + 调用栈: new Error().stack + }); + + // PRO 和 MAX 配置 + const config = { + pro: { + label: 'PRO专享', + icon: FaStar, + bgGradient: 'linear(to-r, blue.400, purple.500)', + color: 'white', + }, + max: { + label: 'MAX专享', + icon: FaCrown, + bgGradient: 'linear(to-r, pink.400, red.500)', + color: 'white', + }, + }; + + const tierConfig = config[tier] || config.pro; + + // 尺寸配置 + const sizeConfig = { + sm: { + fontSize: 'xs', + iconSize: 2.5, + px: 2, + py: 0.5, + }, + md: { + fontSize: 'sm', + iconSize: 3, + px: 3, + py: 1, + }, + }; + + const currentSize = sizeConfig[size] || sizeConfig.sm; + + return ( + + + {tierConfig.label} + + ); +}; + +export default SubscriptionBadge; diff --git a/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js b/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js index 2008fd81..ce01f031 100644 --- a/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js +++ b/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js @@ -16,21 +16,43 @@ import CollapsibleHeader from './CollapsibleHeader'; * @param {boolean} props.isOpen - 是否展开 * @param {Function} props.onToggle - 切换展开/收起的回调 * @param {number} props.count - 可选的数量徽章 + * @param {React.ReactNode} props.subscriptionBadge - 可选的会员标签组件 + * @param {boolean} props.isLocked - 是否锁定(不可展开) + * @param {Function} props.onLockedClick - 锁定时点击的回调 * @param {React.ReactNode} props.children - 子内容 */ -const CollapsibleSection = ({ title, isOpen, onToggle, count = null, children }) => { +const CollapsibleSection = ({ + title, + isOpen, + onToggle, + count = null, + subscriptionBadge = null, + isLocked = false, + onLockedClick = null, + children +}) => { const sectionBg = useColorModeValue('gray.50', 'gray.750'); + // 处理点击:如果锁定则触发锁定回调,否则触发正常切换 + const handleToggle = () => { + if (isLocked && onLockedClick) { + onLockedClick(); + } else if (!isLocked) { + onToggle(); + } + }; + return ( { const dispatch = useDispatch(); + const { user } = useAuth(); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); const textColor = useColorModeValue('gray.600', 'gray.400'); 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; + // 权限判断函数 + 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, @@ -55,10 +76,19 @@ const DynamicNewsDetailPanel = ({ event }) => { loadChainAnalysis } = useEventStocks(event?.id, event?.created_at, { autoLoad: false }); - // 子区块折叠状态管理(默认折叠)+ 加载追踪 - const [isStocksOpen, setIsStocksOpen] = useState(false); + // 相关股票、相关概念、历史事件和传导链的权限 + const canAccessStocks = hasAccess('pro'); + const canAccessConcepts = hasAccess('pro'); + const canAccessHistorical = hasAccess('pro'); + const canAccessTransmission = hasAccess('max'); + + // 子区块折叠状态管理 + 加载追踪 + // PRO 会员的相关股票默认展开 + const [isStocksOpen, setIsStocksOpen] = useState(canAccessStocks && userTier === 'pro'); const [hasLoadedStocks, setHasLoadedStocks] = useState(false); + const [isConceptsOpen, setIsConceptsOpen] = useState(false); + const [isHistoricalOpen, setIsHistoricalOpen] = useState(false); const [hasLoadedHistorical, setHasLoadedHistorical] = useState(false); @@ -75,7 +105,25 @@ const DynamicNewsDetailPanel = ({ event }) => { } }); - // 相关股票 - 展开时加载 + // 锁定点击处理 - 弹出升级弹窗 + 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); @@ -87,6 +135,11 @@ const DynamicNewsDetailPanel = ({ event }) => { } }, [isStocksOpen, hasLoadedStocks, loadStocksData, event?.id]); + // 相关概念 - 展开/收起(无需加载) + const handleConceptsToggle = useCallback(() => { + setIsConceptsOpen(!isConceptsOpen); + }, [isConceptsOpen]); + // 历史事件对比 - 展开时加载 const handleHistoricalToggle = useCallback(() => { const newState = !isHistoricalOpen; @@ -114,13 +167,25 @@ const DynamicNewsDetailPanel = ({ event }) => { // 事件切换时重置所有子模块状态 useEffect(() => { console.log('%c🔄 [事件切换] 重置所有子模块状态', 'color: #F59E0B; font-weight: bold;', { eventId: event?.id }); - setIsStocksOpen(false); + + // PRO 会员的相关股票默认展开,其他情况收起 + const shouldOpenStocks = canAccessStocks && userTier === 'pro'; + setIsStocksOpen(shouldOpenStocks); setHasLoadedStocks(false); + + // PRO 会员默认展开时,自动加载股票数据 + if (shouldOpenStocks) { + console.log('%c📊 [PRO会员] 自动加载相关股票数据', 'color: #10B981; font-weight: bold;', { eventId: event?.id }); + loadStocksData(); + setHasLoadedStocks(true); + } + + setIsConceptsOpen(false); setIsHistoricalOpen(false); setHasLoadedHistorical(false); setIsTransmissionOpen(false); setHasLoadedTransmission(false); - }, [event?.id]); + }, [event?.id, canAccessStocks, userTier, loadStocksData]); // 切换关注状态 const handleToggleFollow = useCallback(async () => { @@ -196,12 +261,20 @@ const DynamicNewsDetailPanel = ({ event }) => { {/* 事件描述 */} - {/* 相关股票(可折叠) - 懒加载 */} + {/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 */} { + if (!canAccessStocks) { + return ; + } + return null; + })()} + isLocked={!canAccessStocks} + onLockedClick={() => handleLockedClick('相关股票', 'pro')} > {loading.stocks || loading.quotes ? (
@@ -219,19 +292,27 @@ const DynamicNewsDetailPanel = ({ event }) => { )} - {/* 相关概念 */} + {/* 相关概念(可折叠) - 需要 PRO 权限 */} : null} + isLocked={!canAccessConcepts} + onLockedClick={() => handleLockedClick('相关概念', 'pro')} /> - {/* 历史事件对比(可折叠) - 懒加载 */} + {/* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */} : null} + isLocked={!canAccessHistorical} + onLockedClick={() => handleLockedClick('历史事件对比', 'pro')} > {loading.historicalEvents ? (
@@ -246,11 +327,14 @@ const DynamicNewsDetailPanel = ({ event }) => { )} - {/* 传导链分析(可折叠) - 懒加载 */} + {/* 传导链分析(可折叠) - 懒加载 - 需要 MAX 权限 */} : null} + isLocked={!canAccessTransmission} + onLockedClick={() => handleLockedClick('传导链分析', 'max')} > { + + {/* 升级弹窗 */} + {upgradeModal.isOpen ? ( + + ): null } ); }; diff --git a/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/SimpleConceptCard.js b/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/SimpleConceptCard.js index d9086717..212269d0 100644 --- a/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/SimpleConceptCard.js +++ b/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/SimpleConceptCard.js @@ -4,8 +4,10 @@ import React from 'react'; import { Flex, + VStack, Box, Text, + Badge, useColorModeValue, } from '@chakra-ui/react'; @@ -27,10 +29,15 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => { const relevanceScore = Math.round((concept.score || 0) * 100); const relevanceColors = getRelevanceColor(relevanceScore); + // 涨跌幅数据 + const changePct = concept.price_info?.avg_change_pct ? parseFloat(concept.price_info.avg_change_pct) : null; + const changeColor = changePct !== null ? (changePct > 0 ? 'red' : changePct < 0 ? 'green' : 'gray') : null; + const changeSymbol = changePct !== null && changePct > 0 ? '+' : ''; + return ( - { }} onClick={() => onClick(concept)} > - {/* 左侧:概念名 + 数量 */} - + {/* 第一行:概念名 + 数量(允许折行) */} + {concept.concept}{' '} ({concept.stock_count}) - {/* 右侧:相关度标签 */} - - - 相关度: {relevanceScore}% - - - + {/* 第二行:相关度 + 涨跌幅 */} + + {/* 相关度标签 */} + + + 相关度: {relevanceScore}% + + + + {/* 涨跌幅数据 */} + {changePct !== null && ( + + {changeSymbol}{changePct.toFixed(2)}% + + )} + + ); };