From ed24a14fbfda3ed4110042717f9ef983ecd8dc5e Mon Sep 17 00:00:00 2001
From: zdl <3489966805@qq.com>
Date: Wed, 5 Nov 2025 21:31:02 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=8B=E4=BB=B6=E8=AF=A6=E6=83=85?=
=?UTF-8?q?=E6=9D=83=E9=99=90=E5=8A=A0=E4=B8=8A=E6=9D=83=E9=99=90=E6=A0=A1?=
=?UTF-8?q?=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/SubscriptionBadge.js | 97 +++++++++++++++
.../DynamicNewsDetail/CollapsibleSection.js | 28 ++++-
.../DynamicNewsDetailPanel.js | 113 ++++++++++++++++--
.../SimpleConceptCard.js | 67 ++++++++---
4 files changed, 274 insertions(+), 31 deletions(-)
create mode 100644 src/components/SubscriptionBadge.js
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)}%
+
+ )}
+
+
);
};