// src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/index.js // 相关概念区组件(主组件) import React, { useState, useEffect } from 'react'; import { Box, SimpleGrid, Flex, Button, Collapse, Heading, Center, Spinner, Text, useColorModeValue, } from '@chakra-ui/react'; import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'; import { useNavigate } from 'react-router-dom'; import moment from 'moment'; import SimpleConceptCard from './SimpleConceptCard'; import DetailedConceptCard from './DetailedConceptCard'; import TradingDateInfo from './TradingDateInfo'; import { logger } from '../../../../../utils/logger'; /** * 相关概念区组件 * @param {Object} props * @param {string} props.eventTitle - 事件标题(用于搜索概念) * @param {string} props.effectiveTradingDate - 有效交易日期(涨跌幅数据日期) * @param {string|Object} props.eventTime - 事件发生时间 * @param {React.ReactNode} props.subscriptionBadge - 订阅徽章组件(可选) * @param {boolean} props.isLocked - 是否锁定详细模式(需要付费) * @param {Function} props.onLockedClick - 锁定时的点击回调(触发付费弹窗) */ const RelatedConceptsSection = ({ eventTitle, effectiveTradingDate, eventTime, subscriptionBadge = null, isLocked = false, onLockedClick = null, isOpen = undefined, // 新增:受控模式(外部控制展开状态) onToggle = undefined // 新增:受控模式(外部控制展开回调) }) => { // 使用外部 isOpen,如果没有则使用内部 useState const [internalExpanded, setInternalExpanded] = useState(false); const isExpanded = onToggle !== undefined ? isOpen : internalExpanded; const [concepts, setConcepts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const navigate = useNavigate(); // 颜色配置 const sectionBg = useColorModeValue('gray.50', 'gray.750'); const headingColor = useColorModeValue('gray.700', 'gray.200'); const textColor = useColorModeValue('gray.600', 'gray.400'); console.log('[RelatedConceptsSection] 组件渲染', { eventTitle, effectiveTradingDate, eventTime, loading, conceptsCount: concepts?.length || 0, error }); // 搜索相关概念 useEffect(() => { const searchConcepts = async () => { console.log('[RelatedConceptsSection] useEffect 触发', { eventTitle, effectiveTradingDate }); if (!eventTitle || !effectiveTradingDate) { console.log('[RelatedConceptsSection] 缺少必要参数,跳过搜索', { hasEventTitle: !!eventTitle, hasEffectiveTradingDate: !!effectiveTradingDate }); setLoading(false); return; } try { setLoading(true); setError(null); // 格式化交易日期 - 统一使用 moment 处理 let formattedTradeDate; try { // 不管传入的是什么格式,都用 moment 解析并格式化为 YYYY-MM-DD formattedTradeDate = moment(effectiveTradingDate).format('YYYY-MM-DD'); // 验证日期是否有效 if (!moment(formattedTradeDate, 'YYYY-MM-DD', true).isValid()) { console.warn('[RelatedConceptsSection] 无效日期,使用当前日期'); formattedTradeDate = moment().format('YYYY-MM-DD'); } } catch (error) { console.warn('[RelatedConceptsSection] 日期格式化失败,使用当前日期', error); formattedTradeDate = moment().format('YYYY-MM-DD'); } const requestBody = { query: eventTitle, size: 5, page: 1, sort_by: "_score", trade_date: formattedTradeDate }; console.log('[RelatedConceptsSection] 发送请求', { url: '/concept-api/search', requestBody }); logger.debug('RelatedConceptsSection', '搜索概念', requestBody); const response = await fetch('/concept-api/search', { method: 'POST', 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]); // 加载中状态 if (loading) { return (
加载相关概念中...
); } // 判断是否有数据 const hasNoConcepts = !concepts || concepts.length === 0; /** * 根据相关度获取颜色(浅色背景 + 深色文字) * @param {number} relevance - 相关度(0-100) * @returns {Object} 包含背景色和文字色 */ const getRelevanceColor = (relevance) => { if (relevance >= 90) { return { bg: 'purple.50', color: 'purple.800' }; // 极高相关 } else if (relevance >= 80) { return { bg: 'pink.50', color: 'pink.800' }; // 高相关 } else if (relevance >= 70) { return { bg: 'orange.50', color: 'orange.800' }; // 中等相关 } else { return { bg: 'gray.100', color: 'gray.700' }; // 低相关 } }; /** * 处理概念点击 * @param {Object} concept - 概念对象 */ const handleConceptClick = (concept) => { // 跳转到概念中心,并搜索该概念 navigate(`/concepts?q=${encodeURIComponent(concept.concept)}`); }; return ( {/* 标题栏 - 两行布局 */} {/* 第一行:标题 + Badge + 按钮 */} 相关概念 {/* 订阅徽章 */} {subscriptionBadge} {/* 第二行:交易日期信息 */} {/* 简单模式:横向卡片列表(总是显示) */} {hasNoConcepts ? ( {error ? ( {error} ) : ( 暂无相关概念数据 )} ) : ( {concepts.map((concept, index) => ( ))} )} {/* 详细模式:卡片网格(可折叠) */} {hasNoConcepts ? ( {error ? ( {error} ) : ( 暂无详细数据 )} ) : ( /* 详细概念卡片网格 */ {concepts.map((concept, index) => ( ))} )} ); }; export default RelatedConceptsSection;