diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index 63753f4f..9e084b36 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -51,7 +51,6 @@ import { eventService } from '@services/eventService'; import { getApiBase } from '@utils/apiConfig'; import ReactMarkdown from 'react-markdown'; import dayjs from 'dayjs'; -import RelatedStocksSection from '@components/EventDetailPanel/RelatedStocksSection'; const { TabPane } = Tabs; const { Text: AntText } = Typography; @@ -415,6 +414,9 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading const [selectedEventStocks, setSelectedEventStocks] = useState([]); const [selectedEventTime, setSelectedEventTime] = useState(null); const [selectedEventTitle, setSelectedEventTitle] = useState(''); + const [stockQuotes, setStockQuotes] = useState({}); + const [stockQuotesLoading, setStockQuotesLoading] = useState(false); + const [expandedReasons, setExpandedReasons] = useState({}); // 板块数据处理 - 必须在条件返回之前调用所有hooks const sectorList = useMemo(() => { @@ -473,6 +475,235 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading setDetailDrawerVisible(true); }; + // 获取六位股票代码(去掉后缀) + const getSixDigitCode = (code) => { + if (!code) return code; + return code.split('.')[0]; + }; + + // 加载股票行情 + const loadStockQuotes = async (stocks) => { + if (!stocks || stocks.length === 0) return; + setStockQuotesLoading(true); + const quotes = {}; + + for (const stock of stocks) { + const code = getSixDigitCode(stock.code); + try { + const response = await fetch(`${getApiBase()}/api/market/trade/${code}?days=1`); + if (response.ok) { + const data = await response.json(); + if (data.success && data.data && data.data.length > 0) { + const latest = data.data[data.data.length - 1]; + quotes[stock.code] = { + price: latest.close, + change: latest.change_amount, + changePercent: latest.change_percent + }; + } + } + } catch (err) { + console.error('加载股票行情失败:', code, err); + } + } + + setStockQuotes(quotes); + setStockQuotesLoading(false); + }; + + // 显示相关股票 + const showRelatedStocks = (stocks, eventTime, eventTitle) => { + if (!stocks || stocks.length === 0) return; + + // 归一化股票数据格式 + const normalizedStocks = stocks.map(stock => { + if (typeof stock === 'object' && !Array.isArray(stock)) { + return { + code: stock.code || stock.stock_code || '', + name: stock.name || stock.stock_name || '', + description: stock.description || stock.relation_desc || '', + score: stock.score || 0, + report: stock.report || null, + }; + } + if (Array.isArray(stock)) { + return { + code: stock[0] || '', + name: stock[1] || '', + description: stock[2] || '', + score: stock[3] || 0, + report: null, + }; + } + return null; + }).filter(Boolean); + + // 按相关度排序 + const sortedStocks = normalizedStocks.sort((a, b) => (b.score || 0) - (a.score || 0)); + + setSelectedEventStocks(sortedStocks); + setSelectedEventTime(eventTime); + setSelectedEventTitle(eventTitle); + setStocksDrawerVisible(true); + setExpandedReasons({}); + loadStockQuotes(sortedStocks); + }; + + // 相关股票表格列定义(和投资日历保持一致) + const stockColumns = [ + { + title: '代码', + dataIndex: 'code', + key: 'code', + width: 90, + render: (code) => { + const sixDigitCode = getSixDigitCode(code); + return ( + + {sixDigitCode} + + ); + } + }, + { + title: '名称', + dataIndex: 'name', + key: 'name', + width: 100, + render: (name, record) => { + const sixDigitCode = getSixDigitCode(record.code); + return ( + + {name} + + ); + } + }, + { + title: '现价', + key: 'price', + width: 80, + render: (_, record) => { + const quote = stockQuotes[record.code]; + if (quote && quote.price !== undefined) { + return ( + 0 ? 'danger' : 'success'}> + {quote.price?.toFixed(2)} + + ); + } + return -; + } + }, + { + title: '涨跌幅', + key: 'change', + width: 100, + render: (_, record) => { + const quote = stockQuotes[record.code]; + if (quote && quote.changePercent !== undefined) { + const changePercent = quote.changePercent || 0; + return ( + 0 ? 'red' : changePercent < 0 ? 'green' : 'default'}> + {changePercent > 0 ? '+' : ''}{changePercent.toFixed(2)}% + + ); + } + return -; + } + }, + { + title: '关联理由', + dataIndex: 'description', + key: 'reason', + render: (description, record) => { + const stockCode = record.code; + const isExpanded = expandedReasons[stockCode] || false; + const reason = typeof description === 'string' ? description : ''; + const shouldTruncate = reason && reason.length > 80; + + const toggleExpanded = () => { + setExpandedReasons(prev => ({ + ...prev, + [stockCode]: !prev[stockCode] + })); + }; + + return ( +
+ + {isExpanded || !shouldTruncate + ? reason || '-' + : `${reason?.slice(0, 80)}...` + } + + {shouldTruncate && ( + + )} + {reason && ( +
+ (AI合成) +
+ )} +
+ ); + } + }, + { + title: '研报引用', + dataIndex: 'report', + key: 'report', + width: 180, + render: (report) => { + if (!report || !report.title) { + return -; + } + return ( +
+ +
+ + {report.title.length > 18 ? `${report.title.slice(0, 18)}...` : report.title} + + {report.author && ( + + {report.author} + + )} + {report.declare_date && ( + + {dayjs(report.declare_date).format('YYYY-MM-DD')} + + )} + {report.match_score && ( + + 匹配度: {report.match_score} + + )} +
+
+
+ ); + } + }, + ]; + // 涨停板块表格列 const sectorColumns = [ { @@ -713,12 +944,7 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading type="link" size="small" icon={} - onClick={() => { - setSelectedEventStocks(stocks); - setSelectedEventTime(record.calendar_time); - setSelectedEventTitle(record.title); - setStocksDrawerVisible(true); - }} + onClick={() => showRelatedStocks(stocks, record.calendar_time, record.title)} > {stocks.length}只 @@ -974,22 +1200,25 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading isOpen={stocksDrawerVisible} placement="right" size="xl" - onClose={() => setStocksDrawerVisible(false)} + onClose={() => { + setStocksDrawerVisible(false); + setExpandedReasons({}); + }} > - + 相关股票 {selectedEventTitle && ( - + {selectedEventTitle} )} @@ -997,13 +1226,18 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading {selectedEventStocks?.length || 0}只 + {stockQuotesLoading && } - + {selectedEventStocks && selectedEventStocks.length > 0 ? ( - record.code} + size="middle" + pagination={false} + scroll={{ y: 500 }} /> ) : (