# 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, 'minute', 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 (
: }
onClick={(e) => {
e.stopPropagation(); // 防止触发行点击
handleWatchlistToggle(record.stock_code, isInWatchlist);
}}
>
{isInWatchlist ? '已关注' : '加自选'}
);
}
}
```
---
## 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线类型**: `'minute'` (分时), `'day'` (日K), `'week'` (周K), `'month'` (月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 的完整业务逻辑,可作为重构验证的参考基准。