// src/views/Community/components/StockDetailPanel.js
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { Drawer, List, Card, Tag, Spin, Empty, Typography, Row, Col, Statistic, Tabs, Descriptions, Badge, message, Table, Modal, Button, Input, Alert } from 'antd';
import { CloseOutlined, RiseOutlined, FallOutlined, CloseCircleOutlined, PushpinOutlined, ReloadOutlined, StarOutlined, StarFilled, LockOutlined, CrownOutlined } from '@ant-design/icons';
import { eventService, stockService } from '../../../services/eventService';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts';
import './StockDetailPanel.css';
import { Tabs as AntdTabs } from 'antd';
import ReactDOM from 'react-dom';
import RelatedConcepts from '../../EventDetail/components/RelatedConcepts';
import HistoricalEvents from '../../EventDetail/components/HistoricalEvents';
import TransmissionChainAnalysis from '../../EventDetail/components/TransmissionChainAnalysis';
import EventDiscussionModal from './EventDiscussionModal';
import { useSubscription } from '../../../hooks/useSubscription';
import SubscriptionUpgradeModal from '../../../components/SubscriptionUpgradeModal';
import moment from 'moment';
import { logger } from '../../../utils/logger';
import { getApiBase } from '../../../utils/apiConfig';
import RiskDisclaimer from '../../../components/RiskDisclaimer';
const { Title, Text } = Typography;
const { TabPane } = Tabs;
// ================= 全局缓存和请求管理 =================
const klineDataCache = new Map(); // 缓存K线数据: key = `${code}|${date}` -> data
const pendingRequests = new Map(); // 正在进行的请求: key = `${code}|${date}` -> Promise
const lastRequestTime = new Map(); // 最后请求时间: key = `${code}|${date}` -> timestamp
// 请求间隔限制(毫秒)
const REQUEST_INTERVAL = 30000; // 30秒内不重复请求同一只股票的数据
// 获取缓存key
const getCacheKey = (stockCode, eventTime) => {
const date = eventTime ? moment(eventTime).format('YYYY-MM-DD') : moment().format('YYYY-MM-DD');
return `${stockCode}|${date}`;
};
// 检查是否需要刷新数据
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 > REQUEST_INTERVAL;
}
// 历史数据不需要频繁更新
return elapsed > 3600000; // 1小时
};
// 获取K线数据(带缓存和防重复请求)
const fetchKlineData = async (stockCode, eventTime) => {
const cacheKey = getCacheKey(stockCode, eventTime);
// 1. 检查缓存
if (klineDataCache.has(cacheKey)) {
// 检查是否需要刷新
if (!shouldRefreshData(cacheKey)) {
logger.debug('StockDetailPanel', '使用缓存数据', { cacheKey });
return klineDataCache.get(cacheKey);
}
}
// 2. 检查是否有正在进行的请求
if (pendingRequests.has(cacheKey)) {
logger.debug('StockDetailPanel', '等待进行中的请求', { cacheKey });
return pendingRequests.get(cacheKey);
}
// 3. 发起新请求
logger.debug('StockDetailPanel', '发起新K线数据请求', { cacheKey });
const normalizedEventTime = eventTime ? moment(eventTime).format('YYYY-MM-DD HH:mm') : undefined;
const requestPromise = stockService
.getKlineData(stockCode, 'minute', normalizedEventTime)
.then((res) => {
const data = Array.isArray(res?.data) ? res.data : [];
// 更新缓存
klineDataCache.set(cacheKey, data);
lastRequestTime.set(cacheKey, Date.now());
// 清除pending状态
pendingRequests.delete(cacheKey);
logger.debug('StockDetailPanel', 'K线数据请求完成并缓存', {
cacheKey,
dataPoints: data.length
});
return data;
})
.catch((error) => {
logger.error('StockDetailPanel', 'fetchKlineData', error, { stockCode, cacheKey });
// 清除pending状态
pendingRequests.delete(cacheKey);
// 如果有旧缓存,返回旧数据
if (klineDataCache.has(cacheKey)) {
return klineDataCache.get(cacheKey);
}
return [];
});
// 保存pending请求
pendingRequests.set(cacheKey, requestPromise);
return requestPromise;
};
// ================= 优化后的迷你分时图组件 =================
const MiniTimelineChart = React.memo(function MiniTimelineChart({ stockCode, eventTime }) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const mountedRef = useRef(true);
const loadedRef = useRef(false); // 标记是否已加载过数据
const dataFetchedRef = useRef(false); // 防止重复请求的标记
// 稳定的事件时间,避免因为格式化导致的重复请求
const stableEventTime = useMemo(() => {
return eventTime ? moment(eventTime).format('YYYY-MM-DD HH:mm') : '';
}, [eventTime]);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
useEffect(() => {
if (!stockCode) {
setData([]);
loadedRef.current = false;
dataFetchedRef.current = false;
return;
}
// 如果已经请求过数据,不再重复请求
if (dataFetchedRef.current) {
return;
}
// 检查缓存
const cacheKey = getCacheKey(stockCode, stableEventTime);
const cachedData = klineDataCache.get(cacheKey);
// 如果有缓存数据,直接使用
if (cachedData && cachedData.length > 0) {
setData(cachedData);
loadedRef.current = true;
dataFetchedRef.current = true;
return;
}
// 标记正在请求
dataFetchedRef.current = true;
setLoading(true);
// 使用全局的fetchKlineData函数
fetchKlineData(stockCode, stableEventTime)
.then((result) => {
if (mountedRef.current) {
setData(result);
setLoading(false);
loadedRef.current = true;
}
})
.catch(() => {
if (mountedRef.current) {
setData([]);
setLoading(false);
loadedRef.current = true;
}
});
}, [stockCode, stableEventTime]); // 注意这里使用 stableEventTime
const chartOption = useMemo(() => {
const prices = data.map(item => item.close ?? item.price).filter(v => typeof v === 'number');
const times = data.map(item => item.time);
const hasData = prices.length > 0;
if (!hasData) {
return {
title: {
text: loading ? '加载中...' : '无数据',
left: 'center',
top: 'middle',
textStyle: { color: '#999', fontSize: 10 }
}
};
}
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
const isUp = prices[prices.length - 1] >= prices[0];
const lineColor = isUp ? '#ef5350' : '#26a69a';
// 计算事件时间对应的分时索引
let eventMarkLineData = [];
if (stableEventTime && Array.isArray(times) && times.length > 0) {
try {
const eventMinute = moment(stableEventTime, 'YYYY-MM-DD HH:mm').format('HH:mm');
const parseMinuteTime = (timeStr) => {
const [h, m] = String(timeStr).split(':').map(Number);
return h * 60 + m;
};
const eventMin = parseMinuteTime(eventMinute);
let nearestIdx = 0;
for (let i = 1; i < times.length; i++) {
if (Math.abs(parseMinuteTime(times[i]) - eventMin) < Math.abs(parseMinuteTime(times[nearestIdx]) - eventMin)) {
nearestIdx = i;
}
}
eventMarkLineData.push({
xAxis: nearestIdx,
lineStyle: { color: '#FFD700', type: 'solid', width: 1.5 },
label: { show: false }
});
} catch (e) {
// 忽略事件时间解析异常
}
}
return {
grid: { left: 2, right: 2, top: 2, bottom: 2, containLabel: false },
xAxis: { type: 'category', data: times, show: false, boundaryGap: false },
yAxis: { type: 'value', show: false, min: minPrice * 0.995, max: maxPrice * 1.005, scale: true },
series: [{
data: prices,
type: 'line',
smooth: true,
symbol: 'none',
lineStyle: { color: lineColor, width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: isUp ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)' },
{ offset: 1, color: isUp ? 'rgba(239, 83, 80, 0.05)' : 'rgba(38, 166, 154, 0.05)' }
])
},
markLine: {
silent: true,
symbol: 'none',
label: { show: false },
data: [
...(prices.length ? [{ yAxis: prices[0], lineStyle: { color: '#aaa', type: 'dashed', width: 1 } }] : []),
...eventMarkLineData
]
}
}],
tooltip: { show: false },
animation: false
};
}, [data, loading, stableEventTime]);
return (
);
}, (prevProps, nextProps) => {
// 自定义比较函数,只有当stockCode或eventTime变化时才重新渲染
return prevProps.stockCode === nextProps.stockCode &&
prevProps.eventTime === nextProps.eventTime;
});
import StockChartAntdModal from '../../../components/StockChart/StockChartAntdModal';
// 使用统一的股票详情组件
const StockDetailModal = ({ stock, onClose, fixed, eventTime }) => {
return (
);
};
function StockDetailPanel({ visible, event, onClose }) {
logger.debug('StockDetailPanel', '组件加载', {
visible,
eventId: event?.id,
eventTitle: event?.title
});
// 权限控制
const { hasFeatureAccess, getRequiredLevel, getUpgradeRecommendation } = useSubscription();
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
const [upgradeFeature, setUpgradeFeature] = useState('');
// 1. hooks
const [activeTab, setActiveTab] = useState('stocks');
const [loading, setLoading] = useState(false);
const [detailLoading, setDetailLoading] = useState(false);
const [relatedStocks, setRelatedStocks] = useState([]);
const [stockQuotes, setStockQuotes] = useState({});
const [selectedStock, setSelectedStock] = useState(null);
const [chartData, setChartData] = useState(null);
const [eventDetail, setEventDetail] = useState(null);
const [historicalEvents, setHistoricalEvents] = useState([]);
const [chainAnalysis, setChainAnalysis] = useState(null);
const [posts, setPosts] = useState([]);
// 移除悬浮相关的state
// const [hoveredStock, setHoveredStock] = useState(null);
const [fixedCharts, setFixedCharts] = useState([]); // [{stock, chartType}]
// const [hoveredRowIndex, setHoveredRowIndex] = useState(null);
// const [tableRect, setTableRect] = useState(null);
const tableRef = React.useRef();
// 讨论模态框相关状态
const [discussionModalVisible, setDiscussionModalVisible] = useState(false);
const [discussionType, setDiscussionType] = useState('事件讨论');
// 移除滚动相关的ref
// const isScrollingRef = React.useRef(false);
// const scrollStopTimerRef = React.useRef(null);
// const hoverTimerRef = React.useRef(null);
// const [hoverTab, setHoverTab] = useState('stock');
const [searchText, setSearchText] = useState(''); // 搜索文本
const [isMonitoring, setIsMonitoring] = useState(false); // 实时监控状态
const [filteredStocks, setFilteredStocks] = useState([]); // 过滤后的股票列表
const [expectationScore, setExpectationScore] = useState(null); // 超预期得分
const monitoringIntervalRef = useRef(null); // 监控定时器引用
const [watchlistStocks, setWatchlistStocks] = useState(new Set()); // 自选股列表
// 清理函数
useEffect(() => {
return () => {
// 组件卸载时清理定时器
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current);
}
};
}, []);
// 过滤股票列表
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]);
// 实时监控定时器 - 优化版本
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('StockDetailPanel', 'updateQuotes', error, {
stockCodes: codes,
eventTime: event?.created_at
}));
};
updateQuotes();
// 设置定时器
monitoringIntervalRef.current = setInterval(updateQuotes, 5000);
}
return () => {
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current);
monitoringIntervalRef.current = null;
}
};
}, [isMonitoring, relatedStocks, event]);
// 加载用户自选股列表
const loadWatchlist = useCallback(async () => {
try {
const isProduction = process.env.NODE_ENV === 'production';
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/api/account/watchlist`, {
credentials: 'include' // 确保发送cookies
});
const data = await response.json();
if (data.success && data.data) {
const watchlistSet = new Set(data.data.map(item => item.stock_code));
setWatchlistStocks(watchlistSet);
logger.debug('StockDetailPanel', '自选股列表加载成功', {
count: watchlistSet.size
});
}
} catch (error) {
logger.error('StockDetailPanel', 'loadWatchlist', error);
}
}, []);
// 加入/移除自选股
const handleWatchlistToggle = async (stockCode, isInWatchlist) => {
try {
const isProduction = process.env.NODE_ENV === 'production';
const apiBase = getApiBase();
let response;
if (isInWatchlist) {
// 移除自选股
response = await fetch(`${apiBase}/api/account/watchlist/${stockCode}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include' // 确保发送cookies
});
} 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', // 确保发送cookies
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);
if (isInWatchlist) {
newSet.delete(stockCode);
} else {
newSet.add(stockCode);
}
return newSet;
});
} else {
message.error(data.error || '操作失败');
}
} catch (error) {
message.error('操作失败,请稍后重试');
}
};
// 初始化数据加载
useEffect(() => {
logger.debug('StockDetailPanel', 'useEffect 触发', {
visible,
eventId: event?.id
});
if (visible && event) {
setActiveTab('stocks');
loadAllData();
}
}, [visible, event]);
// 加载所有数据的函数
const loadAllData = useCallback(() => {
logger.debug('StockDetailPanel', 'loadAllData 被调用', {
eventId: event?.id
});
if (!event) return;
// 加载自选股列表
loadWatchlist();
// 加载相关标的
setLoading(true);
eventService.getRelatedStocks(event.id)
.then(res => {
logger.debug('StockDetailPanel', '接收到事件相关股票数据', {
eventId: event.id,
success: res.success,
stockCount: res.data?.length || 0
});
if (res.success) {
if (res.data && res.data[0]) {
logger.debug('StockDetailPanel', '第一只股票数据', {
stockCode: res.data[0].stock_code,
stockName: res.data[0].stock_name,
hasRelationDesc: !!res.data[0].relation_desc
});
}
setRelatedStocks(res.data);
if (res.data.length > 0) {
const codes = res.data.map(s => s.stock_code);
stockService.getQuotes(codes, event.created_at)
.then(quotes => setStockQuotes(quotes))
.catch(error => logger.error('StockDetailPanel', 'getQuotes', error, {
stockCodes: codes,
eventTime: event.created_at
}));
}
}
})
.finally(() => setLoading(false));
// 加载详细信息
setDetailLoading(true);
eventService.getEventDetail(event.id)
.then(res => {
if (res.success) setEventDetail(res.data);
})
.finally(() => setDetailLoading(false));
// 加载历史事件
eventService.getHistoricalEvents(event.id)
.then(res => {
if (res.success) setHistoricalEvents(res.data);
});
// 加载传导链分析
eventService.getTransmissionChainAnalysis(event.id)
.then(res => {
if (res.success) setChainAnalysis(res.data);
});
// 加载社区讨论
if (eventService.getPosts) {
eventService.getPosts(event.id)
.then(res => {
if (res.success) setPosts(res.data);
});
}
// 加载超预期得分
if (eventService.getExpectationScore) {
eventService.getExpectationScore(event.id)
.then(res => {
if (res.success) setExpectationScore(res.data.score);
})
.catch(() => setExpectationScore(null));
}
}, [event, loadWatchlist]);
// 2. renderCharts函数
const renderCharts = useCallback((stock, chartType, onClose, fixed) => {
// 保证事件时间格式为 'YYYY-MM-DD HH:mm'
const formattedEventTime = event?.start_time ? moment(event.start_time).format('YYYY-MM-DD HH:mm') : undefined;
return ;
}, [event]);
// 3. 简化handleRowEvents函数 - 只处理点击事件
const handleRowEvents = useCallback((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' } // 添加手型光标提示可点击
}), []);
// 展开/收缩的行
const [expandedRows, setExpandedRows] = useState(new Set());
// 稳定的事件时间,避免重复渲染
const stableEventTime = useMemo(() => {
return event?.start_time ? moment(event.start_time).format('YYYY-MM-DD HH:mm') : '';
}, [event?.start_time]);
// 切换行展开状态
const toggleRowExpand = useCallback((stockCode) => {
setExpandedRows(prev => {
const newSet = new Set(prev);
if (newSet.has(stockCode)) {
newSet.delete(stockCode);
} else {
newSet.add(stockCode);
}
return newSet;
});
}, []);
// 4. stockColumns数组 - 使用优化后的 MiniTimelineChart
const stockColumns = useMemo(() => [
{
title: '股票代码',
dataIndex: 'stock_code',
key: 'stock_code',
width: 100,
render: (code, record) => (
),
},
{
title: '股票名称',
dataIndex: 'stock_name',
key: 'stock_name',
width: 120,
},
{
title: '关联描述',
dataIndex: 'relation_desc',
key: 'relation_desc',
width: 300,
render: (relationDesc, record) => {
logger.debug('StockDetailPanel', '表格渲染 - 股票关联描述', {
stockCode: record.stock_code,
hasRelationDesc: !!relationDesc
});
// 处理 relation_desc 的两种格式
let text = '';
if (!relationDesc) {
return '--';
} else if (typeof relationDesc === 'string') {
// 旧格式:直接是字符串
text = relationDesc;
} else if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) {
// 新格式:{data: [{query_part: "...", sentences: "..."}]}
// 提取所有 query_part,用逗号连接
text = relationDesc.data
.map(item => item.query_part || item.sentences || '')
.filter(s => s)
.join(';') || '--';
} else {
logger.warn('StockDetailPanel', '未知的 relation_desc 格式', {
stockCode: record.stock_code,
relationDescType: typeof relationDesc
});
return '--';
}
if (!text || text === '--') return '--';
const isExpanded = expandedRows.has(record.stock_code);
const maxLength = 30; // 收缩时显示的最大字符数
const needTruncate = text.length > maxLength;
return (
{isExpanded ? text : (needTruncate ? text.substring(0, maxLength) + '...' : text)}
{needTruncate && (
)}
);
},
},
{
title: '分时图',
key: 'timeline',
width: 150,
render: (_, record) => (
),
},
{
title: '涨跌幅',
key: 'change',
width: 100,
render: (_, record) => {
const quote = stockQuotes[record.stock_code];
if (!quote) return '--';
const color = quote.change > 0 ? 'red' : quote.change < 0 ? 'green' : 'inherit';
return {quote.change > 0 ? '+' : ''}{quote.change?.toFixed(2)}%;
},
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
render: (_, record) => {
const isInWatchlist = watchlistStocks.has(record.stock_code);
return (
: }
onClick={(e) => {
e.stopPropagation();
handleWatchlistToggle(record.stock_code, isInWatchlist);
}}
style={{ minWidth: '70px' }}
>
{isInWatchlist ? '已关注' : '加自选'}
);
},
},
], [stockQuotes, stableEventTime, expandedRows, toggleRowExpand, watchlistStocks, handleWatchlistToggle, relatedStocks]); // 注意这里依赖改为 stableEventTime
// 处理搜索
const handleSearch = (value) => {
setSearchText(value);
};
// 处理实时监控切换
const handleMonitoringToggle = () => {
setIsMonitoring(!isMonitoring);
if (!isMonitoring) {
message.info('已开启实时监控,每5秒自动更新');
} else {
message.info('已停止实时监控');
}
};
// 处理刷新 - 只清理当天数据的缓存
const handleRefresh = useCallback(() => {
// 手动刷新分时图缓存
const today = moment().format('YYYY-MM-DD');
relatedStocks.forEach(stock => {
const cacheKey = getCacheKey(stock.stock_code, stableEventTime);
// 如果是今天的数据,强制刷新
if (cacheKey.includes(today)) {
lastRequestTime.delete(cacheKey);
klineDataCache.delete(cacheKey); // 清除缓存数据
}
});
// 重新加载数据
loadAllData();
}, [relatedStocks, stableEventTime, loadAllData]);
// 固定图表关闭
const handleUnfixChart = useCallback((stock) => {
setFixedCharts((prev) => prev.filter(item => item.stock.stock_code !== stock.stock_code));
}, []);
// 权限检查函数
const handleTabAccess = (featureName, tabKey) => {
if (!hasFeatureAccess(featureName)) {
const recommendation = getUpgradeRecommendation(featureName);
setUpgradeFeature(recommendation?.required || 'pro');
setUpgradeModalOpen(true);
return false;
}
setActiveTab(tabKey);
return true;
};
// 渲染锁定内容
const renderLockedContent = (featureName, description) => {
const recommendation = getUpgradeRecommendation(featureName);
const isProRequired = recommendation?.required === 'pro';
return (
{isProRequired ? : }
);
};
// 5. tabItems数组
const tabItems = [
{
key: 'stocks',
label: (
相关标的
{!hasFeatureAccess('related_stocks') && (
)}
),
children: hasFeatureAccess('related_stocks') ? (
{/* 头部信息 */}
📊
相关标的
共 {filteredStocks.length} 只股票
每5秒自动更新行情数据
{/* 搜索和操作栏 */}
{/* 股票列表 */}
{/* 固定图表 */}
{fixedCharts.map(({ stock }, index) =>
{renderCharts(stock, 'timeline', () => handleUnfixChart(stock), true)}
)}
{/* 讨论按钮 */}
}
onClick={() => {
setDiscussionType('事件讨论');
setDiscussionModalVisible(true);
}}
>
查看事件讨论
) : renderLockedContent('related_stocks', '相关标的')
},
{
key: 'concepts',
label: (
相关概念
{!hasFeatureAccess('related_concepts') && (
)}
),
children: hasFeatureAccess('related_concepts') ? (
) : renderLockedContent('related_concepts', '相关概念')
},
{
key: 'history',
label: (
历史事件对比
{!hasFeatureAccess('historical_events_full') && (
)}
),
children: (
)
},
{
key: 'chain',
label: (
传导链分析
{!hasFeatureAccess('transmission_chain') && (
)}
),
children: hasFeatureAccess('transmission_chain') ? (
) : renderLockedContent('transmission_chain', '传导链分析')
}
];
return (
<>
{event?.title}
}
placement="right"
width={900}
open={visible}
onClose={onClose}
closable={false}
className="stock-detail-panel"
>
{/* 风险提示 */}
{/* 事件讨论模态框 */}
setDiscussionModalVisible(false)}
eventId={event?.id}
eventTitle={event?.title}
discussionType={discussionType}
/>
{/* 订阅升级模态框 */}
setUpgradeModalOpen(false)}
requiredLevel={upgradeFeature}
featureName={upgradeFeature === 'pro' ? '相关分析功能' : '传导链分析'}
/>
>
);
}
export default StockDetailPanel;