# StockDetailPanel 原始业务逻辑文档 > **文档版本**: 1.0 > **组件文件**: `src/views/Community/components/StockDetailPanel.js` > **原始行数**: 1067 行 > **创建日期**: 2025-10-30 > **重构前快照**: 用于记录重构前的完整业务逻辑 --- ## 📋 目录 1. [组件概述](#1-组件概述) 2. [权限控制系统](#2-权限控制系统) 3. [数据加载流程](#3-数据加载流程) 4. [K线数据缓存机制](#4-k线数据缓存机制) 5. [自选股管理](#5-自选股管理) 6. [实时监控功能](#6-实时监控功能) 7. [搜索和过滤](#7-搜索和过滤) 8. [UI 交互逻辑](#8-ui-交互逻辑) 9. [状态管理](#9-状态管理) 10. [API 端点清单](#10-api-端点清单) --- ## 1. 组件概述 ### 1.1 功能描述 StockDetailPanel 是一个 Ant Design Drawer 组件,用于展示事件相关的详细信息,包括: - **相关标的**: 事件关联的股票列表、实时行情、分时图 - **相关概念**: 事件涉及的概念板块 - **历史事件对比**: 类似历史事件的表现分析 - **传导链分析**: 事件的传导路径和影响链(Max 会员功能) ### 1.2 组件属性 ```javascript StockDetailPanel({ visible, // boolean - 是否显示 Drawer event, // Object - 事件对象 {id, title, start_time, created_at, ...} onClose // Function - 关闭回调 }) ``` ### 1.3 核心依赖 - **useSubscription**: 订阅权限管理 hook - **eventService**: 事件数据 API 服务 - **stockService**: 股票数据 API 服务 - **logger**: 日志工具 --- ## 2. 权限控制系统 ### 2.1 权限层级 系统采用三层订阅模型: | 功能 | 权限标识 | 所需版本 | 图标 | |------|---------|---------|------| | 相关标的 | `related_stocks` | Pro | 🔒 | | 相关概念 | `related_concepts` | Pro | 🔒 | | 历史事件对比 | `historical_events_full` | Pro | 🔒 | | 传导链分析 | `transmission_chain` | Max | 👑 | ### 2.2 权限检查流程 ```javascript // Hook 初始化 const { hasFeatureAccess, getRequiredLevel, getUpgradeRecommendation } = useSubscription(); // Tab 渲染时检查 hasFeatureAccess('related_stocks') ? ( // 渲染完整功能 ) : ( // 渲染锁定提示 UI renderLockedContent('related_stocks', '相关标的') ) ``` ### 2.3 权限拦截机制 **Tab 点击拦截**(已注释,未使用): ```javascript const handleTabAccess = (featureName, tabKey) => { if (!hasFeatureAccess(featureName)) { const recommendation = getUpgradeRecommendation(featureName); setUpgradeFeature(recommendation?.required || 'pro'); setUpgradeModalOpen(true); return false; // 阻止 Tab 切换 } setActiveTab(tabKey); return true; }; ``` ### 2.4 锁定 UI 渲染 ```javascript const renderLockedContent = (featureName, description) => { const recommendation = getUpgradeRecommendation(featureName); const isProRequired = recommendation?.required === 'pro'; return (
{/* 图标: Pro版显示🔒, Max版显示👑 */} or {/* 提示消息 */} {/* 升级按钮 */}
); }; ``` ### 2.5 升级模态框 ```javascript setUpgradeModalOpen(false)} requiredLevel={upgradeFeature} // 'pro' | 'max' featureName={upgradeFeature === 'pro' ? '相关分析功能' : '传导链分析'} /> ``` --- ## 3. 数据加载流程 ### 3.1 加载时机 ```javascript useEffect(() => { if (visible && event) { setActiveTab('stocks'); loadAllData(); } }, [visible, event]); ``` **触发条件**: Drawer 可见 `visible=true` 且 `event` 对象存在 ### 3.2 并发加载策略 `loadAllData()` 函数同时发起 **5 个独立 API 请求**: ```javascript const loadAllData = () => { // 1. 加载用户自选股列表 (独立调用) loadWatchlist(); // 2. 加载相关标的 → 连锁加载行情数据 eventService.getRelatedStocks(event.id) .then(res => { setRelatedStocks(res.data); // 2.1 如果有股票,立即加载行情 if (res.data.length > 0) { const codes = res.data.map(s => s.stock_code); stockService.getQuotes(codes, event.created_at) .then(quotes => setStockQuotes(quotes)); } }); // 3. 加载事件详情 eventService.getEventDetail(event.id) .then(res => setEventDetail(res.data)); // 4. 加载历史事件 eventService.getHistoricalEvents(event.id) .then(res => setHistoricalEvents(res.data)); // 5. 加载传导链分析 eventService.getTransmissionChainAnalysis(event.id) .then(res => setChainAnalysis(res.data)); // 6. 加载超预期得分 eventService.getExpectationScore(event.id) .then(res => setExpectationScore(res.data)); }; ``` ### 3.3 数据依赖关系 ```mermaid graph TD A[loadAllData] --> B[getRelatedStocks] A --> C[getEventDetail] A --> D[getHistoricalEvents] A --> E[getTransmissionChainAnalysis] A --> F[getExpectationScore] A --> G[loadWatchlist] B -->|成功且有数据| H[getQuotes] B --> I[setRelatedStocks] H --> J[setStockQuotes] C --> K[setEventDetail] D --> L[setHistoricalEvents] E --> M[setChainAnalysis] F --> N[setExpectationScore] G --> O[setWatchlistStocks] ``` ### 3.4 加载状态管理 ```javascript // 主加载状态 const [loading, setLoading] = useState(false); // 相关标的加载中 const [detailLoading, setDetailLoading] = useState(false); // 事件详情加载中 // 使用示例 setLoading(true); eventService.getRelatedStocks(event.id) .finally(() => setLoading(false)); ``` ### 3.5 错误处理 ```javascript // 使用 logger 记录错误 stockService.getQuotes(codes, event.created_at) .catch(error => logger.error('StockDetailPanel', 'getQuotes', error, { stockCodes: codes, eventTime: event.created_at })); ``` --- ## 4. K线数据缓存机制 ### 4.1 缓存架构 **三层 Map 缓存**: ```javascript // 全局缓存(组件级别,不跨实例) const klineDataCache = new Map(); // 数据缓存: key → data[] const pendingRequests = new Map(); // 请求去重: key → Promise const lastRequestTime = new Map(); // 时间戳: key → timestamp ``` ### 4.2 缓存键生成 ```javascript const getCacheKey = (stockCode, eventTime) => { const date = eventTime ? moment(eventTime).format('YYYY-MM-DD') : moment().format('YYYY-MM-DD'); return `${stockCode}|${date}`; }; // 示例: "600000.SH|2024-10-30" ``` ### 4.3 智能刷新策略 ```javascript const shouldRefreshData = (cacheKey) => { const lastTime = lastRequestTime.get(cacheKey); if (!lastTime) return true; // 无缓存,需要刷新 const now = Date.now(); const elapsed = now - lastTime; // 检测是否为当日交易时段 const today = moment().format('YYYY-MM-DD'); const isToday = cacheKey.includes(today); const currentHour = new Date().getHours(); const isTradingHours = currentHour >= 9 && currentHour < 16; if (isToday && isTradingHours) { return elapsed > 30000; // 交易时段: 30秒刷新 } return elapsed > 3600000; // 非交易时段/历史数据: 1小时刷新 }; ``` | 场景 | 刷新间隔 | 原因 | |------|---------|------| | 当日 + 交易时段 (9:00-16:00) | 30 秒 | 实时性要求高 | | 当日 + 非交易时段 | 1 小时 | 数据不会变化 | | 历史日期 | 1 小时 | 数据固定不变 | ### 4.4 请求去重机制 ```javascript const fetchKlineData = async (stockCode, eventTime) => { const cacheKey = getCacheKey(stockCode, eventTime); // 1️⃣ 检查缓存 if (klineDataCache.has(cacheKey) && !shouldRefreshData(cacheKey)) { return klineDataCache.get(cacheKey); // 直接返回缓存 } // 2️⃣ 检查是否有进行中的请求(防止重复请求) if (pendingRequests.has(cacheKey)) { return pendingRequests.get(cacheKey); // 返回同一个 Promise } // 3️⃣ 发起新请求 const requestPromise = stockService .getKlineData(stockCode, 'timeline', eventTime) .then((res) => { const data = Array.isArray(res?.data) ? res.data : []; // 更新缓存 klineDataCache.set(cacheKey, data); lastRequestTime.set(cacheKey, Date.now()); // 清除 pending 状态 pendingRequests.delete(cacheKey); return data; }) .catch((error) => { pendingRequests.delete(cacheKey); // 如果有旧缓存,返回旧数据 if (klineDataCache.has(cacheKey)) { return klineDataCache.get(cacheKey); } return []; }); // 保存到 pending pendingRequests.set(cacheKey, requestPromise); return requestPromise; }; ``` **去重效果**: - 同时有 10 个组件请求同一只股票的同一天数据 - 实际只会发出 **1 个 API 请求** - 其他 9 个请求共享同一个 Promise ### 4.5 MiniTimelineChart 使用缓存 ```javascript const MiniTimelineChart = ({ stockCode, eventTime }) => { useEffect(() => { // 检查缓存 const cacheKey = getCacheKey(stockCode, eventTime); const cachedData = klineDataCache.get(cacheKey); if (cachedData && cachedData.length > 0) { setData(cachedData); // 使用缓存 return; } // 无缓存,发起请求 fetchKlineData(stockCode, eventTime) .then(result => setData(result)); }, [stockCode, eventTime]); }; ``` --- ## 5. 自选股管理 ### 5.1 加载自选股列表 ```javascript const loadWatchlist = async () => { const apiBase = getApiBase(); // 根据环境获取 API base URL const response = await fetch(`${apiBase}/api/account/watchlist`, { credentials: 'include' // ⚠️ 关键: 发送 cookies 进行认证 }); const data = await response.json(); if (data.success && data.data) { // 转换为 Set 数据结构,便于快速查找 const watchlistSet = new Set(data.data.map(item => item.stock_code)); setWatchlistStocks(watchlistSet); } }; ``` **API 响应格式**: ```json { "success": true, "data": [ {"stock_code": "600000.SH", "stock_name": "浦发银行"}, {"stock_code": "000001.SZ", "stock_name": "平安银行"} ] } ``` ### 5.2 添加/移除自选股 ```javascript const handleWatchlistToggle = async (stockCode, isInWatchlist) => { const apiBase = getApiBase(); let response; if (isInWatchlist) { // 🗑️ 删除操作 response = await fetch(`${apiBase}/api/account/watchlist/${stockCode}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); } else { // ➕ 添加操作 const stockInfo = relatedStocks.find(s => s.stock_code === stockCode); response = await fetch(`${apiBase}/api/account/watchlist`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ stock_code: stockCode, stock_name: stockInfo?.stock_name || stockCode }) }); } const data = await response.json(); if (data.success) { message.success(isInWatchlist ? '已从自选股移除' : '已加入自选股'); // 更新本地状态(乐观更新) setWatchlistStocks(prev => { const newSet = new Set(prev); isInWatchlist ? newSet.delete(stockCode) : newSet.add(stockCode); return newSet; }); } else { message.error(data.error || '操作失败'); } }; ``` ### 5.3 UI 集成 ```javascript // 在 StockTable 的"操作"列中 { title: '操作', render: (_, record) => { const isInWatchlist = watchlistStocks.has(record.stock_code); return ( ); } } ``` --- ## 6. 实时监控功能 ### 6.1 监控机制 ```javascript const [isMonitoring, setIsMonitoring] = useState(false); const monitoringIntervalRef = useRef(null); useEffect(() => { // 清理旧定时器 if (monitoringIntervalRef.current) { clearInterval(monitoringIntervalRef.current); monitoringIntervalRef.current = null; } if (isMonitoring && relatedStocks.length > 0) { // 定义更新函数 const updateQuotes = () => { const codes = relatedStocks.map(s => s.stock_code); stockService.getQuotes(codes, event?.created_at) .then(quotes => setStockQuotes(quotes)) .catch(error => logger.error('...', error)); }; // 立即执行一次 updateQuotes(); // 设置定时器: 每 5 秒刷新 monitoringIntervalRef.current = setInterval(updateQuotes, 5000); } // 清理函数 return () => { if (monitoringIntervalRef.current) { clearInterval(monitoringIntervalRef.current); monitoringIntervalRef.current = null; } }; }, [isMonitoring, relatedStocks, event]); ``` ### 6.2 监控控制 ```javascript const handleMonitoringToggle = () => { setIsMonitoring(prev => !prev); }; ``` **UI 表现**: ```javascript
每5秒自动更新行情数据
``` ### 6.3 组件卸载清理 ```javascript useEffect(() => { return () => { // 组件卸载时清理定时器,防止内存泄漏 if (monitoringIntervalRef.current) { clearInterval(monitoringIntervalRef.current); } }; }, []); ``` --- ## 7. 搜索和过滤 ### 7.1 搜索状态 ```javascript const [searchText, setSearchText] = useState(''); const [filteredStocks, setFilteredStocks] = useState([]); ``` ### 7.2 过滤逻辑 ```javascript useEffect(() => { if (!searchText.trim()) { setFilteredStocks(relatedStocks); // 无搜索词,显示全部 } else { const filtered = relatedStocks.filter(stock => stock.stock_code.toLowerCase().includes(searchText.toLowerCase()) || stock.stock_name.toLowerCase().includes(searchText.toLowerCase()) ); setFilteredStocks(filtered); } }, [searchText, relatedStocks]); ``` **搜索特性**: - 不区分大小写 - 同时匹配股票代码和股票名称 - 实时过滤(每次输入都触发) ### 7.3 搜索 UI ```javascript setSearchText(e.target.value)} allowClear // 显示清除按钮 /> ``` --- ## 8. UI 交互逻辑 ### 8.1 Tab 切换 ```javascript const [activeTab, setActiveTab] = useState('stocks'); ``` **Tab 列表**: ```javascript const tabItems = [ { key: 'stocks', label: '相关标的', children: ... }, { key: 'concepts', label: '相关概念', children: ... }, { key: 'historical', label: '历史事件对比', children: ... }, { key: 'chain', label: '传导链分析', children: ... } ]; ``` ### 8.2 固定图表管理 **添加固定图表** (行点击): ```javascript const handleRowEvents = (record) => ({ onClick: () => { setFixedCharts((prev) => { // 防止重复添加 if (prev.find(item => item.stock.stock_code === record.stock_code)) { return prev; } return [...prev, { stock: record, chartType: 'timeline' }]; }); }, style: { cursor: 'pointer' } }); ``` **移除固定图表**: ```javascript const handleUnfixChart = (stock) => { setFixedCharts((prev) => prev.filter(item => item.stock.stock_code !== stock.stock_code) ); }; ``` **渲染固定图表**: ```javascript {fixedCharts.map(({ stock }, index) => ( handleUnfixChart(stock)} stock={stock} eventTime={formattedEventTime} fixed={true} /> ))} ``` ### 8.3 行展开/收起逻辑 ```javascript const [expandedRows, setExpandedRows] = useState(new Set()); const toggleRowExpand = (stockCode) => { setExpandedRows(prev => { const newSet = new Set(prev); newSet.has(stockCode) ? newSet.delete(stockCode) : newSet.add(stockCode); return newSet; }); }; ``` **应用场景**: 关联描述文本过长时的展开/收起 ### 8.4 讨论模态框 ```javascript const [discussionModalVisible, setDiscussionModalVisible] = useState(false); const [discussionType, setDiscussionType] = useState('事件讨论'); setDiscussionModalVisible(false)} eventId={event?.id} eventTitle={event?.title} discussionType={discussionType} /> ``` --- ## 9. 状态管理 ### 9.1 状态清单 | 状态名 | 类型 | 初始值 | 用途 | |--------|------|--------|------| | `activeTab` | string | `'stocks'` | 当前激活的 Tab | | `loading` | boolean | `false` | 相关标的加载状态 | | `detailLoading` | boolean | `false` | 事件详情加载状态 | | `relatedStocks` | Array | `[]` | 相关股票列表 | | `stockQuotes` | Object | `{}` | 股票行情字典 | | `selectedStock` | Object | `null` | 当前选中的股票(未使用) | | `chartData` | Object | `null` | 图表数据(未使用) | | `eventDetail` | Object | `null` | 事件详情 | | `historicalEvents` | Array | `[]` | 历史事件列表 | | `chainAnalysis` | Object | `null` | 传导链分析数据 | | `posts` | Array | `[]` | 讨论帖子(未使用) | | `fixedCharts` | Array | `[]` | 固定图表列表 | | `searchText` | string | `''` | 搜索文本 | | `isMonitoring` | boolean | `false` | 实时监控开关 | | `filteredStocks` | Array | `[]` | 过滤后的股票列表 | | `expectationScore` | Object | `null` | 超预期得分 | | `watchlistStocks` | Set | `new Set()` | 自选股集合 | | `discussionModalVisible` | boolean | `false` | 讨论模态框可见性 | | `discussionType` | string | `'事件讨论'` | 讨论类型 | | `upgradeModalOpen` | boolean | `false` | 升级模态框可见性 | | `upgradeFeature` | string | `''` | 需要升级的功能 | ### 9.2 Ref 引用 | Ref 名 | 用途 | |--------|------| | `monitoringIntervalRef` | 存储监控定时器 ID | | `tableRef` | Table 组件引用(未使用) | --- ## 10. API 端点清单 ### 10.1 事件相关 API | API | 方法 | 参数 | 返回数据 | 用途 | |-----|------|------|---------|------| | `eventService.getRelatedStocks(eventId)` | GET | 事件ID | `{ success, data: Stock[] }` | 获取相关股票 | | `eventService.getEventDetail(eventId)` | GET | 事件ID | `{ success, data: EventDetail }` | 获取事件详情 | | `eventService.getHistoricalEvents(eventId)` | GET | 事件ID | `{ success, data: Event[] }` | 获取历史事件 | | `eventService.getTransmissionChainAnalysis(eventId)` | GET | 事件ID | `{ success, data: ChainAnalysis }` | 获取传导链分析 | | `eventService.getExpectationScore(eventId)` | GET | 事件ID | `{ success, data: Score }` | 获取超预期得分 | ### 10.2 股票相关 API | API | 方法 | 参数 | 返回数据 | 用途 | |-----|------|------|---------|------| | `stockService.getQuotes(codes[], eventTime)` | GET | 股票代码数组, 事件时间 | `{ [code]: Quote }` | 批量获取行情 | | `stockService.getKlineData(code, type, eventTime)` | GET | 股票代码, K线类型, 事件时间 | `{ success, data: Kline[] }` | 获取K线数据 | **K线类型**: `'timeline'` (分时), `'daily'` (日K), `'weekly'` (周K), `'monthly'` (月K) ### 10.3 自选股 API | API | 方法 | 请求体 | 返回数据 | 用途 | |-----|------|--------|---------|------| | `GET /api/account/watchlist` | GET | - | `{ success, data: Watchlist[] }` | 获取自选股列表 | | `POST /api/account/watchlist` | POST | `{ stock_code, stock_name }` | `{ success }` | 添加自选股 | | `DELETE /api/account/watchlist/:code` | DELETE | - | `{ success }` | 移除自选股 | **认证方式**: 所有 API 都使用 `credentials: 'include'` 携带 cookies --- ## 📝 附录 ### A. 数据结构定义 #### Stock (股票) ```typescript interface Stock { stock_code: string; // 股票代码, 如 "600000.SH" stock_name: string; // 股票名称, 如 "浦发银行" relation_desc: string | { // 关联描述 data: Array<{ query_part?: string; sentences?: string; }> }; } ``` #### Quote (行情) ```typescript interface Quote { change: number; // 涨跌幅 (百分比) price: number; // 当前价格 volume: number; // 成交量 // ... 其他字段 } ``` #### Event (事件) ```typescript interface Event { id: string; // 事件 ID title: string; // 事件标题 start_time: string; // 事件开始时间 (ISO 8601) created_at: string; // 创建时间 // ... 其他字段 } ``` ### B. 性能优化要点 1. **请求去重**: 使用 `pendingRequests` Map 防止重复请求 2. **智能缓存**: 根据交易时段动态调整刷新策略 3. **并发加载**: 5 个 API 请求并发执行 4. **乐观更新**: 自选股操作立即更新 UI,无需等待后端响应 5. **定时器清理**: 组件卸载时清理定时器,防止内存泄漏 ### C. 安全要点 1. **认证**: 所有 API 请求携带 credentials: 'include' 2. **权限检查**: 每个 Tab 渲染前检查用户权限 3. **错误处理**: 所有 API 调用都有 catch 错误处理 4. **日志记录**: 使用 logger 记录关键操作和错误 --- **文档结束** > 该文档记录了重构前 StockDetailPanel.js 的完整业务逻辑,可作为重构验证的参考基准。