diff --git a/docs/Community.md b/docs/Community.md
index 641c4800..05f8a39a 100644
--- a/docs/Community.md
+++ b/docs/Community.md
@@ -1101,6 +1101,70 @@ feat: 删除不需要的组件
---
+### 2025-01-XX: CollapsibleSection 支持精简/详细模式切换
+
+**变更类型**: 功能增强
+
+**优化内容**: 为事件详情面板的 CollapsibleSection 组件添加精简/详细双模式切换功能,提升用户浏览体验。
+
+**新增功能**:
+
+1. **CollapsibleHeader.js** - 支持模式切换按钮
+ - 新增 `showModeToggle` prop:是否显示模式切换按钮
+ - 新增 `currentMode` prop:当前模式('detailed' | 'simple')
+ - 新增 `onModeToggle` prop:模式切换回调
+ - 按钮样式:
+ - 详细模式展开:`详细模式 ▲`
+ - 详细模式收起:`详细模式 ▼`
+ - 精简模式:`精简模式`
+ - 向后兼容:不提供 `showModeToggle` 时,保持原有的 IconButton 样式
+
+2. **CollapsibleSection.js** - 支持精简/详细模式
+ - 新增 `simpleContent` prop:精简模式的内容
+ - 新增 `showModeToggle` prop:是否显示模式切换按钮
+ - 新增 `defaultMode` prop:默认模式(默认 'detailed')
+ - 模式切换逻辑:
+ - 详细模式 → 精简模式:显示 simpleContent
+ - 精简模式 → 详细模式:自动展开 children
+ - 权限控制:锁定状态下点击模式切换,触发付费弹窗
+
+3. **SimpleStocksList.js** - 新增精简股票列表组件
+ - 横向展示股票名称和涨跌幅
+ - 自动根据涨跌幅显示颜色(红涨绿跌)
+ - 支持加载中状态
+ - 响应式设计,自动折行
+
+4. **DynamicNewsDetailPanel.js** - 相关股票模块启用双模式
+ - 添加 `showModeToggle={true}`
+ - 添加 `simpleContent`:使用 SimpleStocksList 显示精简股票列表
+ - `children`:保持原有的 RelatedStocksSection(完整信息)
+ - 默认详细模式展开(PRO 会员)
+
+**用户体验提升**:
+- ✅ 支持精简/详细两种浏览模式,满足不同场景需求
+- ✅ 精简模式:快速浏览股票名称和涨跌幅
+- ✅ 详细模式:查看完整股票信息(价格、按钮等)
+- ✅ 一键切换,无需重新加载数据
+- ✅ PRO 会员默认展开详细模式,优化会员体验
+
+**技术细节**:
+- 使用 React useState 管理模式状态
+- 模式切换不触发数据重新加载
+- 向后兼容:不使用 `showModeToggle` 的 CollapsibleSection 保持原有行为
+- 权限控制:锁定状态下切换模式触发付费引导
+
+**影响范围**:
+- `src/views/Community/components/DynamicNewsDetail/CollapsibleHeader.js`
+- `src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js`
+- `src/views/Community/components/DynamicNewsDetail/SimpleStocksList.js`(新增)
+- `src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js`
+
+**扩展性**:
+- 其他模块(历史事件对比、传导链分析)可以复用此功能
+- 只需提供对应的 `simpleContent` 即可启用双模式
+
+---
+
## 🔗 相关文档
- [项目总览 - CLAUDE.md](../CLAUDE.md)
diff --git a/src/views/Community/components/DynamicNewsCard/EventDetailScrollPanel.js b/src/views/Community/components/DynamicNewsCard/EventDetailScrollPanel.js
index 7a12194f..c531220f 100644
--- a/src/views/Community/components/DynamicNewsCard/EventDetailScrollPanel.js
+++ b/src/views/Community/components/DynamicNewsCard/EventDetailScrollPanel.js
@@ -13,13 +13,21 @@ import DynamicNewsDetailPanel from '../DynamicNewsDetail';
* @param {string} scrollbarTrackBg - 滚动条轨道背景色
* @param {string} scrollbarThumbBg - 滚动条滑块背景色
* @param {string} scrollbarThumbHoverBg - 滚动条滑块悬浮背景色
+ * @param {string} detailMode - 详情模式:'full' | 'no-header'(默认 'full')
+ * @param {boolean} showHeader - 是否显示头部(可选,优先级高于 detailMode)
*/
const EventDetailScrollPanel = ({
selectedEvent,
scrollbarTrackBg,
scrollbarThumbBg,
scrollbarThumbHoverBg,
+ detailMode = 'full',
+ showHeader,
}) => {
+ // 计算是否显示头部:showHeader 显式指定时优先,否则根据 detailMode 判断
+ const shouldShowHeader = showHeader !== undefined
+ ? showHeader
+ : detailMode === 'full';
return (
{selectedEvent ? (
-
+
) : (
diff --git a/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js b/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js
index 05eca5c7..c2437d02 100644
--- a/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js
+++ b/src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js
@@ -112,6 +112,7 @@ const VerticalModeLayout = ({
timelineStyle={getTimelineBoxStyle()}
borderColor={borderColor}
indicatorSize={layoutMode === 'detail' ? 'default' : 'comfortable'}
+ layout="vertical"
/>
))}
@@ -161,6 +162,7 @@ const VerticalModeLayout = ({
{/* 详情面板 */}
diff --git a/src/views/Community/components/DynamicNewsDetail/CollapsibleHeader.js b/src/views/Community/components/DynamicNewsDetail/CollapsibleHeader.js
index 95d32fd4..5d9175a2 100644
--- a/src/views/Community/components/DynamicNewsDetail/CollapsibleHeader.js
+++ b/src/views/Community/components/DynamicNewsDetail/CollapsibleHeader.js
@@ -9,6 +9,7 @@ import {
Heading,
Badge,
IconButton,
+ Button,
useColorModeValue,
} from '@chakra-ui/react';
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
@@ -21,22 +22,53 @@ import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
* @param {Function} props.onToggle - 切换展开/收起的回调
* @param {number} props.count - 可选的数量徽章
* @param {React.ReactNode} props.subscriptionBadge - 可选的会员标签组件
+ * @param {boolean} props.showModeToggle - 是否显示模式切换按钮(默认 false)
+ * @param {string} props.currentMode - 当前模式:'detailed' | 'simple'
+ * @param {Function} props.onModeToggle - 模式切换回调
+ * @param {boolean} props.isLocked - 是否锁定(不可展开)
*/
-const CollapsibleHeader = ({ title, isOpen, onToggle, count = null, subscriptionBadge = null }) => {
+const CollapsibleHeader = ({
+ title,
+ isOpen,
+ onToggle,
+ count = null,
+ subscriptionBadge = null,
+ showModeToggle = false,
+ currentMode = 'detailed',
+ onModeToggle = null,
+ isLocked = false
+}) => {
const sectionBg = useColorModeValue('gray.50', 'gray.750');
const hoverBg = useColorModeValue('gray.100', 'gray.700');
const headingColor = useColorModeValue('gray.700', 'gray.200');
+ // 获取按钮文案
+ const getButtonText = () => {
+ if (currentMode === 'simple') {
+ return '查看详情'; // 简单模式时,按钮显示"查看详情"
+ }
+ return '精简模式'; // 详细模式时,按钮显示"精简模式"
+ };
+
+ // 获取按钮图标
+ const getButtonIcon = () => {
+ if (currentMode === 'simple') {
+ return null; // 简单模式不显示图标
+ }
+ // 详细模式:展开显示向上箭头,收起显示向下箭头
+ return isOpen ? : ;
+ };
+
return (
@@ -54,12 +86,32 @@ const CollapsibleHeader = ({ title, isOpen, onToggle, count = null, subscription
)}
- : }
- size="sm"
- variant="ghost"
- aria-label={isOpen ? '收起' : '展开'}
- />
+
+ {/* 只有 showModeToggle=true 时才显示模式切换按钮 */}
+ {showModeToggle && onModeToggle && (
+
+ )}
+
+ {/* showModeToggle=false 时显示原有的 IconButton */}
+ {!showModeToggle && (
+ : }
+ size="sm"
+ variant="ghost"
+ aria-label={isOpen ? '收起' : '展开'}
+ />
+ )}
);
};
diff --git a/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js b/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js
index ce01f031..3b34dfdc 100644
--- a/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js
+++ b/src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js
@@ -1,7 +1,7 @@
// src/views/Community/components/DynamicNewsDetail/CollapsibleSection.js
// 通用可折叠区块组件
-import React from 'react';
+import React, { useState } from 'react';
import {
Box,
Collapse,
@@ -19,7 +19,10 @@ import CollapsibleHeader from './CollapsibleHeader';
* @param {React.ReactNode} props.subscriptionBadge - 可选的会员标签组件
* @param {boolean} props.isLocked - 是否锁定(不可展开)
* @param {Function} props.onLockedClick - 锁定时点击的回调
- * @param {React.ReactNode} props.children - 子内容
+ * @param {React.ReactNode} props.children - 详细内容
+ * @param {React.ReactNode} props.simpleContent - 精简模式的内容(可选)
+ * @param {boolean} props.showModeToggle - 是否显示模式切换按钮(默认 false)
+ * @param {string} props.defaultMode - 默认模式:'detailed' | 'simple'(默认 'detailed')
*/
const CollapsibleSection = ({
title,
@@ -29,10 +32,16 @@ const CollapsibleSection = ({
subscriptionBadge = null,
isLocked = false,
onLockedClick = null,
- children
+ children,
+ simpleContent = null,
+ showModeToggle = false,
+ defaultMode = 'detailed'
}) => {
const sectionBg = useColorModeValue('gray.50', 'gray.750');
+ // 模式状态:'detailed' | 'simple'
+ const [displayMode, setDisplayMode] = useState(defaultMode);
+
// 处理点击:如果锁定则触发锁定回调,否则触发正常切换
const handleToggle = () => {
if (isLocked && onLockedClick) {
@@ -42,15 +51,43 @@ const CollapsibleSection = ({
}
};
- return (
-
-
+ // 处理模式切换
+ const handleModeToggle = (e) => {
+ e.stopPropagation(); // 阻止冒泡到标题栏的 onToggle
+
+ if (isLocked && onLockedClick) {
+ // 如果被锁定,触发付费弹窗
+ onLockedClick();
+ return;
+ }
+
+ if (displayMode === 'detailed') {
+ // 从详细模式切换到精简模式
+ setDisplayMode('simple');
+ } else {
+ // 从精简模式切换回详细模式
+ setDisplayMode('detailed');
+ // 切换回详细模式时,如果未展开则自动展开
+ if (!isOpen && onToggle) {
+ onToggle();
+ }
+ }
+ };
+
+ // 渲染精简模式
+ const renderSimpleMode = () => {
+ if (!simpleContent) return null;
+
+ return (
+
+ {simpleContent}
+
+ );
+ };
+
+ // 渲染详细模式
+ const renderDetailedMode = () => {
+ return (
+ );
+ };
+
+ return (
+
+
+
+ {/* 根据当前模式渲染对应内容 */}
+ {displayMode === 'simple' ? renderSimpleMode() : renderDetailedMode()}
);
};
diff --git a/src/views/Community/components/DynamicNewsDetail/CompactMetaBar.js b/src/views/Community/components/DynamicNewsDetail/CompactMetaBar.js
new file mode 100644
index 00000000..7abe78ad
--- /dev/null
+++ b/src/views/Community/components/DynamicNewsDetail/CompactMetaBar.js
@@ -0,0 +1,100 @@
+// src/views/Community/components/DynamicNewsDetail/CompactMetaBar.js
+// 精简信息栏组件(无头部模式下右上角显示)
+
+import React from 'react';
+import {
+ HStack,
+ Badge,
+ Text,
+ Icon,
+ useColorModeValue,
+} from '@chakra-ui/react';
+import { ViewIcon } from '@chakra-ui/icons';
+import EventFollowButton from '../EventCard/EventFollowButton';
+
+/**
+ * 精简信息栏组件
+ * 在无头部模式下,显示在 CardBody 右上角
+ * 包含:重要性徽章、浏览次数、关注按钮
+ *
+ * @param {Object} props
+ * @param {Object} props.event - 事件对象
+ * @param {Object} props.importance - 重要性配置对象(包含 level, icon 等)
+ * @param {boolean} props.isFollowing - 是否已关注
+ * @param {number} props.followerCount - 关注数
+ * @param {Function} props.onToggleFollow - 切换关注回调
+ */
+const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggleFollow }) => {
+ const viewCountBg = useColorModeValue('white', 'gray.700');
+ const viewCountTextColor = useColorModeValue('gray.600', 'gray.300');
+
+ // 获取重要性文本
+ const getImportanceText = () => {
+ const levelMap = {
+ 'S': '极高',
+ 'A': '高',
+ 'B': '中',
+ 'C': '低'
+ };
+ return levelMap[importance.level] || '中';
+ };
+
+ return (
+
+ {/* 重要性徽章 - 与 EventHeaderInfo 样式一致,尺寸略小 */}
+
+
+ 重要性:{getImportanceText()}
+
+
+ {/* 浏览次数 - 添加容器背景以提高可读性 */}
+
+
+
+ {(event.view_count || 0).toLocaleString()}
+
+
+
+ {/* 关注按钮 */}
+
+
+ );
+};
+
+export default CompactMetaBar;
diff --git a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js
index 668d9ab9..e76054df 100644
--- a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js
+++ b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js
@@ -10,6 +10,8 @@ import {
Text,
Spinner,
Center,
+ Wrap,
+ WrapItem,
useColorModeValue,
useToast,
} from '@chakra-ui/react';
@@ -19,9 +21,11 @@ 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';
@@ -32,8 +36,9 @@ import SubscriptionUpgradeModal from '../../../../components/SubscriptionUpgrade
* 动态新闻详情面板主组件
* @param {Object} props
* @param {Object} props.event - 事件对象(包含详情数据)
+ * @param {boolean} props.showHeader - 是否显示头部信息(默认 true)
*/
-const DynamicNewsDetailPanel = ({ event }) => {
+const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
const dispatch = useDispatch();
const { user } = useAuth();
const cardBg = useColorModeValue('white', 'gray.800');
@@ -111,8 +116,8 @@ const DynamicNewsDetailPanel = ({ event }) => {
const canAccessTransmission = hasAccess('max');
// 子区块折叠状态管理 + 加载追踪
- // PRO 会员的相关股票默认展开
- const [isStocksOpen, setIsStocksOpen] = useState(canAccessStocks && userTier === 'pro');
+ // 初始值为 false,由 useEffect 根据权限动态设置
+ const [isStocksOpen, setIsStocksOpen] = useState(false);
const [hasLoadedStocks, setHasLoadedStocks] = useState(false);
const [isConceptsOpen, setIsConceptsOpen] = useState(false);
@@ -199,14 +204,18 @@ const DynamicNewsDetailPanel = ({ event }) => {
// 🎯 加载事件详情(增加浏览量)
loadEventDetail();
- // PRO 会员的相关股票默认展开,其他情况收起
- const shouldOpenStocks = canAccessStocks && userTier === 'pro';
+ // PRO 和 MAX 会员的相关股票默认展开,其他情况收起
+ const shouldOpenStocks = canAccessStocks;
setIsStocksOpen(shouldOpenStocks);
setHasLoadedStocks(false);
- // PRO 会员默认展开时,自动加载股票数据
- if (shouldOpenStocks) {
- console.log('%c📊 [PRO会员] 自动加载相关股票数据', 'color: #10B981; font-weight: bold;', { eventId: event?.id });
+ // PRO 和 MAX 会员自动加载股票数据(无论是否展开)
+ const shouldLoadStocks = canAccessStocks; // PRO 或 MAX 都有权限
+ if (shouldLoadStocks) {
+ console.log('%c📊 [PRO/MAX会员] 自动加载相关股票数据', 'color: #10B981; font-weight: bold;', {
+ eventId: event?.id,
+ userTier
+ });
loadStocksData();
setHasLoadedStocks(true);
}
@@ -278,21 +287,34 @@ const DynamicNewsDetailPanel = ({ event }) => {
return (
-
-
- {/* 头部信息区 - 优先使用完整详情数据(包含最新浏览量) */}
-
+ {/* 无头部模式:显示右上角精简信息栏 */}
+ {!showHeader && (
+
+ )}
+
+
+ {/* 头部信息区 - 优先使用完整详情数据(包含最新浏览量) - 可配置显示/隐藏 */}
+ {showHeader && (
+
+ )}
{/* 事件描述 */}
- {/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 */}
+ {/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 - 支持精简/详细模式 */}
{
})()}
isLocked={!canAccessStocks}
onLockedClick={() => handleLockedClick('相关股票', 'pro')}
+ showModeToggle={canAccessStocks}
+ defaultMode="detailed"
+ simpleContent={
+ loading.stocks || loading.quotes ? (
+
+
+ 加载股票数据中...
+
+ ) : (
+
+ {stocks?.map((stock, index) => (
+
+
+
+ ))}
+
+ )
+ }
>
{loading.stocks || loading.quotes ? (
diff --git a/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/index.js b/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/index.js
index 009e8573..5df76c95 100644
--- a/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/index.js
+++ b/src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/index.js
@@ -38,9 +38,13 @@ const RelatedConceptsSection = ({
eventTime,
subscriptionBadge = null,
isLocked = false,
- onLockedClick = null
+ onLockedClick = null,
+ isOpen = undefined, // 新增:受控模式(外部控制展开状态)
+ onToggle = undefined // 新增:受控模式(外部控制展开回调)
}) => {
- const [isExpanded, setIsExpanded] = useState(false);
+ // 使用外部 isOpen,如果没有则使用内部 useState
+ const [internalExpanded, setInternalExpanded] = useState(false);
+ const isExpanded = onToggle !== undefined ? isOpen : internalExpanded;
const [concepts, setConcepts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -179,10 +183,8 @@ const RelatedConceptsSection = ({
);
}
- // 如果没有概念,不渲染
- if (!concepts || concepts.length === 0) {
- return null;
- }
+ // 判断是否有数据
+ const hasNoConcepts = !concepts || concepts.length === 0;
/**
* 根据相关度获取颜色(浅色背景 + 深色文字)
@@ -232,9 +234,12 @@ const RelatedConceptsSection = ({
// 如果被锁定且有回调函数,触发付费弹窗
if (isLocked && onLockedClick) {
onLockedClick();
+ } else if (onToggle !== undefined) {
+ // 受控模式:调用外部回调
+ onToggle();
} else {
- // 否则正常展开/收起
- setIsExpanded(!isExpanded);
+ // 非受控模式:使用内部状态
+ setInternalExpanded(!internalExpanded);
}
}}
>
@@ -249,30 +254,49 @@ const RelatedConceptsSection = ({
{/* 简单模式:横向卡片列表(总是显示) */}
-
- {concepts.map((concept, index) => (
-
- ))}
-
-
-
- {/* 详细模式:卡片网格(可折叠) */}
-
- {/* 详细概念卡片网格 */}
-
+ {hasNoConcepts ? (
+
+ {error ? (
+ {error}
+ ) : (
+ 暂无相关概念数据
+ )}
+
+ ) : (
+
{concepts.map((concept, index) => (
-
))}
-
+
+ )}
+
+ {/* 详细模式:卡片网格(可折叠) */}
+
+ {hasNoConcepts ? (
+
+ {error ? (
+ {error}
+ ) : (
+ 暂无详细数据
+ )}
+
+ ) : (
+ /* 详细概念卡片网格 */
+
+ {concepts.map((concept, index) => (
+
+ ))}
+
+ )}
);
diff --git a/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js b/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
index 8802db74..2be81942 100644
--- a/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
+++ b/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
@@ -1,21 +1,13 @@
// src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
// 相关股票列表区组件(纯内容,不含标题)
-import React, { useState } from 'react';
-import {
- VStack,
- Flex,
- Button,
- ButtonGroup,
- Wrap,
- WrapItem,
-} from '@chakra-ui/react';
-import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
+import React from 'react';
+import { VStack } from '@chakra-ui/react';
import StockListItem from './StockListItem';
-import CompactStockItem from './CompactStockItem';
/**
* 相关股票列表区组件(纯内容部分)
+ * 只负责渲染详细的股票列表,精简模式由外层 CollapsibleSection 的 simpleContent 提供
* @param {Object} props
* @param {Array