- 移动42个文档文件到 docs/ 目录 - 更新 .gitignore 允许 docs/ 下的 .md 文件 - 删除根目录下的重复文档文件 📁 文档分类: - StockDetailPanel 重构文档(3个) - PostHog 集成文档(6个) - 系统架构和API文档(33个) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
826 lines
22 KiB
Markdown
826 lines
22 KiB
Markdown
# 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 (
|
||
<div>
|
||
{/* 图标: Pro版显示🔒, Max版显示👑 */}
|
||
<LockOutlined /> or <CrownOutlined />
|
||
|
||
{/* 提示消息 */}
|
||
<Alert message={`${description}功能已锁定`} />
|
||
|
||
{/* 升级按钮 */}
|
||
<Button onClick={() => setUpgradeModalOpen(true)}>
|
||
升级到 {isProRequired ? 'Pro版' : 'Max版'}
|
||
</Button>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
### 2.5 升级模态框
|
||
|
||
```javascript
|
||
<SubscriptionUpgradeModal
|
||
isOpen={upgradeModalOpen}
|
||
onClose={() => 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 (
|
||
<Button
|
||
type={isInWatchlist ? 'default' : 'primary'}
|
||
icon={isInWatchlist ? <StarFilled /> : <StarOutlined />}
|
||
onClick={(e) => {
|
||
e.stopPropagation(); // 防止触发行点击
|
||
handleWatchlistToggle(record.stock_code, isInWatchlist);
|
||
}}
|
||
>
|
||
{isInWatchlist ? '已关注' : '加自选'}
|
||
</Button>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 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
|
||
<Button
|
||
className={`monitoring-button ${isMonitoring ? 'monitoring' : ''}`}
|
||
onClick={handleMonitoringToggle}
|
||
>
|
||
{isMonitoring ? '停止监控' : '实时监控'}
|
||
</Button>
|
||
<div>每5秒自动更新行情数据</div>
|
||
```
|
||
|
||
### 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
|
||
<Input
|
||
placeholder="搜索股票代码或名称..."
|
||
value={searchText}
|
||
onChange={(e) => setSearchText(e.target.value)}
|
||
allowClear // 显示清除按钮
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
## 8. UI 交互逻辑
|
||
|
||
### 8.1 Tab 切换
|
||
|
||
```javascript
|
||
const [activeTab, setActiveTab] = useState('stocks');
|
||
|
||
<AntdTabs
|
||
activeKey={activeTab}
|
||
onChange={setActiveTab} // 直接设置,无拦截
|
||
items={tabItems}
|
||
/>
|
||
```
|
||
|
||
**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) => (
|
||
<StockChartAntdModal
|
||
key={`fixed-chart-${stock.stock_code}-${index}`}
|
||
open={true}
|
||
onCancel={() => 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('事件讨论');
|
||
|
||
<Button onClick={() => {
|
||
setDiscussionType('事件讨论');
|
||
setDiscussionModalVisible(true);
|
||
}}>
|
||
查看事件讨论
|
||
</Button>
|
||
|
||
<EventDiscussionModal
|
||
isOpen={discussionModalVisible}
|
||
onClose={() => 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 的完整业务逻辑,可作为重构验证的参考基准。
|