From 4ac9b30bfb853cd36183eb4811335cbd7230287d Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Thu, 18 Dec 2025 20:06:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0Company=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84UI=E4=B8=BAFUI=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 7 ++ .../DynamicNewsDetailPanel.js | 1 + .../DetailedConceptCard.js | 60 +++++++-- .../SimpleConceptCard.js | 61 ++++++---- .../RelatedConceptsSection/index.js | 114 +++++++++++------- 5 files changed, 165 insertions(+), 78 deletions(-) diff --git a/app.py b/app.py index 313dfafb..5426c09b 100755 --- a/app.py +++ b/app.py @@ -18875,5 +18875,12 @@ if __name__ == '__main__': # 初始化事件轮询机制(WebSocket 推送) initialize_event_polling() + # 启动时预热股票缓存(股票名称 + 前收盘价) + print("[启动] 正在预热股票缓存...") + try: + preload_stock_cache() + except Exception as e: + print(f"[启动] 预热缓存失败(不影响服务启动): {e}") + # 使用 socketio.run 替代 app.run 以支持 WebSocket socketio.run(app, host='0.0.0.0', port=5001, debug=False, allow_unsafe_werkzeug=True) \ No newline at end of file diff --git a/src/components/EventDetailPanel/DynamicNewsDetailPanel.js b/src/components/EventDetailPanel/DynamicNewsDetailPanel.js index d6ceb26a..508ce68e 100644 --- a/src/components/EventDetailPanel/DynamicNewsDetailPanel.js +++ b/src/components/EventDetailPanel/DynamicNewsDetailPanel.js @@ -352,6 +352,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { {/* 相关概念(可折叠) - 需要 PRO 权限 */} { const borderColor = useColorModeValue('gray.200', 'gray.600'); const headingColor = useColorModeValue('gray.700', 'gray.200'); const stockCountColor = useColorModeValue('gray.500', 'gray.400'); + const reasonBg = useColorModeValue('blue.50', 'blue.900'); + const reasonColor = useColorModeValue('gray.700', 'gray.200'); // 计算相关度百分比 const relevanceScore = Math.round((concept.score || 0) * 100); @@ -43,6 +46,9 @@ const DetailedConceptCard = ({ concept, onClick }) => { const changeColor = changePct > 0 ? 'red' : changePct < 0 ? 'green' : 'gray'; const changeSymbol = changePct > 0 ? '+' : ''; + // 判断是否来自数据库(有 reason 字段) + const isFromDatabase = !!concept.reason; + return ( { {concept.concept} - - 相关度: {relevanceScore}% - - - {concept.stock_count} 只股票 - + {/* 数据库数据显示"AI分析"标签,搜索数据显示相关度 */} + {isFromDatabase ? ( + + AI 分析 + + ) : ( + + 相关度: {relevanceScore}% + + )} + {/* 只有搜索数据才显示股票数量 */} + {!isFromDatabase && concept.stock_count > 0 && ( + + {concept.stock_count} 只股票 + + )} - {/* 右侧:涨跌幅 */} - {concept.price_info?.avg_change_pct && ( + {/* 右侧:涨跌幅(仅搜索数据有) */} + {!isFromDatabase && concept.price_info?.avg_change_pct && ( 平均涨跌幅 @@ -97,8 +113,30 @@ const DetailedConceptCard = ({ concept, onClick }) => { - {/* 概念描述 */} - {concept.description && ( + {/* 关联原因(来自数据库,突出显示) */} + {concept.reason && ( + + + 关联原因 + + + {concept.reason} + + + )} + + {/* 概念描述(仅搜索数据有,且没有 reason 时显示) */} + {!concept.reason && concept.description && ( { const changeColor = changePct !== null ? (changePct > 0 ? 'red' : changePct < 0 ? 'green' : 'gray') : null; const changeSymbol = changePct !== null && changePct > 0 ? '+' : ''; + // 判断是否来自数据库(有 reason 字段) + const isFromDatabase = !!concept.reason; + return ( { wordBreak="break-word" lineHeight="1.4" > - {concept.concept}{' '} - - ({concept.stock_count}) - + {concept.concept} + {/* 只有搜索数据才显示股票数量 */} + {!isFromDatabase && concept.stock_count > 0 && ( + + {' '}({concept.stock_count}) + + )} - {/* 第二行:相关度 + 涨跌幅 */} + {/* 第二行:标签 */} - {/* 相关度标签 */} - - - 相关度: {relevanceScore}% - - + {/* 数据库数据显示"AI分析",搜索数据显示相关度 */} + {isFromDatabase ? ( + + AI 分析 + + ) : ( + + + 相关度: {relevanceScore}% + + + )} - {/* 涨跌幅数据 */} - {changePct !== null && ( + {/* 涨跌幅数据(仅搜索数据有) */} + {!isFromDatabase && changePct !== null && ( { - const searchConcepts = async () => { + const fetchConcepts = async () => { console.log('[RelatedConceptsSection] useEffect 触发', { + eventId, eventTitle, effectiveTradingDate }); + // 优先使用 eventId 获取数据库中的相关概念 + if (eventId) { + try { + setLoading(true); + setError(null); + + const apiUrl = `${getApiBase()}/api/events/${eventId}/concepts`; + console.log('[RelatedConceptsSection] 从数据库获取相关概念', { url: apiUrl }); + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + + if (!response.ok) { + // 如果是 403,说明需要订阅,不是错误 + if (response.status === 403) { + console.log('[RelatedConceptsSection] 需要订阅才能查看'); + setConcepts([]); + setLoading(false); + return; + } + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('[RelatedConceptsSection] 数据库响应', data); + + if (data.success && Array.isArray(data.data)) { + // 转换数据格式,使其与原有展示逻辑兼容 + const formattedConcepts = data.data.map(item => ({ + concept: item.concept, + reason: item.reason, + concept_code: item.concept_code, + // 保留原有字段以兼容 DetailedConceptCard + score: 1, // 数据库中的都是高相关度 + description: item.reason, // reason 作为描述 + stocks: [], // 暂无股票数据 + stock_count: 0 + })); + console.log('[RelatedConceptsSection] 设置概念数据', formattedConcepts); + setConcepts(formattedConcepts); + } else { + setConcepts([]); + } + } catch (err) { + console.error('[RelatedConceptsSection] 获取概念失败', err); + logger.error('RelatedConceptsSection', 'fetchConcepts', err); + setError('加载概念数据失败'); + setConcepts([]); + } finally { + setLoading(false); + } + return; + } + + // 降级方案:使用 eventTitle 搜索概念(兼容旧逻辑) if (!eventTitle || !effectiveTradingDate) { console.log('[RelatedConceptsSection] 缺少必要参数,跳过搜索', { + hasEventId: !!eventId, hasEventTitle: !!eventTitle, hasEffectiveTradingDate: !!effectiveTradingDate }); @@ -86,19 +149,14 @@ const RelatedConceptsSection = ({ setLoading(true); setError(null); - // 格式化交易日期 - 统一使用 moment 处理 + // 格式化交易日期 let formattedTradeDate; try { - // 不管传入的是什么格式,都用 moment 解析并格式化为 YYYY-MM-DD formattedTradeDate = dayjs(effectiveTradingDate).format('YYYY-MM-DD'); - - // 验证日期是否有效 if (!dayjs(formattedTradeDate, 'YYYY-MM-DD', true).isValid()) { - console.warn('[RelatedConceptsSection] 无效日期,使用当前日期'); formattedTradeDate = dayjs().format('YYYY-MM-DD'); } } catch (error) { - console.warn('[RelatedConceptsSection] 日期格式化失败,使用当前日期', error); formattedTradeDate = dayjs().format('YYYY-MM-DD'); } @@ -111,67 +169,37 @@ const RelatedConceptsSection = ({ }; const apiUrl = `${getApiBase()}/concept-api/search`; - console.log('[RelatedConceptsSection] 发送请求', { - url: apiUrl, - requestBody - }); - logger.debug('RelatedConceptsSection', '搜索概念', requestBody); + console.log('[RelatedConceptsSection] 降级:使用搜索接口', { url: apiUrl, requestBody }); const response = await fetch(apiUrl, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); - console.log('[RelatedConceptsSection] 响应状态', { - ok: response.ok, - status: response.status, - statusText: response.statusText - }); - if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); - console.log('[RelatedConceptsSection] 响应数据', { - hasResults: !!data.results, - resultsCount: data.results?.length || 0, - hasDataConcepts: !!(data.data && data.data.concepts), - data: data - }); - logger.debug('RelatedConceptsSection', '概念搜索响应', { - hasResults: !!data.results, - resultsCount: data.results?.length || 0 - }); - - // 设置概念数据 if (data.results && Array.isArray(data.results)) { - console.log('[RelatedConceptsSection] 设置概念数据 (results)', data.results); setConcepts(data.results); } else if (data.data && data.data.concepts) { - // 向后兼容 - console.log('[RelatedConceptsSection] 设置概念数据 (data.concepts)', data.data.concepts); setConcepts(data.data.concepts); } else { - console.log('[RelatedConceptsSection] 没有找到概念数据,设置为空数组'); setConcepts([]); } } catch (err) { console.error('[RelatedConceptsSection] 搜索概念失败', err); - logger.error('RelatedConceptsSection', 'searchConcepts', err); setError('加载概念数据失败'); setConcepts([]); } finally { - console.log('[RelatedConceptsSection] 加载完成'); setLoading(false); } }; - searchConcepts(); - }, [eventTitle, effectiveTradingDate]); + fetchConcepts(); + }, [eventId, eventTitle, effectiveTradingDate]); // 加载中状态 if (loading) {