// src/views/EventDetail/components/RelatedConcepts.js - 支持概念API调用 import React, { useState, useEffect } from 'react'; import { Icon, // 明确导入 Icon 组件 Box, VStack, HStack, Text, Badge, SimpleGrid, Image, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useDisclosure, Skeleton, Alert, AlertIcon, Card, CardBody, useColorModeValue, IconButton, Tooltip, Button, Center, Divider } from '@chakra-ui/react'; import { FaEye, FaExternalLinkAlt, FaChartLine, FaCalendarAlt } from 'react-icons/fa'; import moment from 'moment'; import tradingDayUtils from '../../../utils/tradingDayUtils'; // 引入交易日工具 // API配置 const API_BASE_URL = process.env.NODE_ENV === 'production' ? '/concept-api' : 'https://valuefrontier.cn/concept-api'; // 增强版 ConceptCard 组件 - 展示更多数据细节 const ConceptCard = ({ concept, tradingDate, onViewDetails }) => { const [isExpanded, setIsExpanded] = useState(false); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.600'); const textColor = useColorModeValue('gray.600', 'gray.400'); const highlightBg = useColorModeValue('yellow.50', 'yellow.900'); // 计算涨跌幅颜色和符号 const changeColor = concept.price_info?.avg_change_pct > 0 ? 'red' : 'green'; const changeSymbol = concept.price_info?.avg_change_pct > 0 ? '+' : ''; const hasValidPriceInfo = concept.price_info && concept.price_info.avg_change_pct !== null; // 获取匹配类型的中文名称 const getMatchTypeName = (type) => { const typeMap = { 'hybrid_knn': '混合匹配', 'keyword': '关键词匹配', 'semantic': '语义匹配' }; return typeMap[type] || type; }; // 处理概念点击 const handleConceptClick = () => { window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(concept.concept)}.html`, '_blank'); }; return ( {/* 头部信息 */} {concept.concept} 相关度: {concept.score.toFixed(2)} {getMatchTypeName(concept.match_type)} {concept.stock_count} 只股票 {hasValidPriceInfo && ( {tradingDate || concept.price_info.trade_date} {changeSymbol}{concept.price_info.avg_change_pct?.toFixed(2)}% )} {/* 概念描述 */} {concept.description} {concept.description && concept.description.length > 200 && ( )} {/* 历史发生时间 */} {concept.happened_times && concept.happened_times.length > 0 && ( 历史触发时间: {concept.happened_times.map((time, idx) => ( {time} ))} )} {/* 相关股票展示 - 增强版 */} {concept.stocks && concept.stocks.length > 0 && ( 核心相关股票 共 {concept.stock_count} 只 {concept.stocks.slice(0, isExpanded ? 8 : 4).map((stock, idx) => ( {stock.stock_name} {stock.stock_code} {stock.reason && ( {stock.reason} )} ))} {concept.stocks.length > 4 && !isExpanded && ( )} )} {/* 操作按钮 */} ); }; // 主组件 - 修改为接收事件信息并调用API const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoading, error: externalError }) => { // 调试:检查 Icon 组件是否可用 if (typeof Icon === 'undefined') { console.error('Icon component is not defined! Make sure @chakra-ui/react is properly imported.'); return
组件加载错误:Icon 组件未定义
; } const { isOpen, onOpen, onClose } = useDisclosure(); const [selectedConcept, setSelectedConcept] = useState(null); const [concepts, setConcepts] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [effectiveTradingDate, setEffectiveTradingDate] = useState(null); const bgColor = useColorModeValue('blue.50', 'blue.900'); const textColor = useColorModeValue('gray.600', 'gray.400'); // 数据验证函数 const validateConceptData = (data) => { if (!data || typeof data !== 'object') { throw new Error('Invalid response data format'); } // 验证新的API格式 if (data.results && Array.isArray(data.results)) { return data.results.every(item => item && typeof item === 'object' && (item.concept || item.concept_id) && typeof item.score === 'number' ); } // 验证旧的API格式 if (data.data && data.data.concepts && Array.isArray(data.data.concepts)) { return data.data.concepts.every(item => item && typeof item === 'object' && (item.concept || item.concept_id) && typeof item.score === 'number' ); } return false; }; // 搜索相关概念 const searchConcepts = async (title, tradeDate) => { try { setLoading(true); setError(null); // 确保tradeDate是字符串格式 let formattedTradeDate; if (typeof tradeDate === 'string') { formattedTradeDate = tradeDate; } else if (tradeDate instanceof Date) { formattedTradeDate = moment(tradeDate).format('YYYY-MM-DD'); } else if (moment.isMoment(tradeDate)) { formattedTradeDate = tradeDate.format('YYYY-MM-DD'); } else { console.warn('Invalid tradeDate format:', tradeDate, typeof tradeDate); formattedTradeDate = moment().format('YYYY-MM-DD'); } const requestBody = { query: title, size: 4, page: 1, sort_by: "_score", trade_date: formattedTradeDate }; console.log('Searching concepts with:', requestBody); const response = await fetch(`${API_BASE_URL}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log('Concept search response:', data); // 数据验证 if (!validateConceptData(data)) { console.warn('Invalid concept data format:', data); setConcepts([]); setError('返回的数据格式无效'); return; } // 修复:适配实际的API响应格式 if (data.results && Array.isArray(data.results)) { setConcepts(data.results); // 使用传入的交易日期作为生效日期 setEffectiveTradingDate(formattedTradeDate); } else if (data.data && data.data.concepts) { // 保持向后兼容 setConcepts(data.data.concepts); setEffectiveTradingDate(data.data.trade_date || formattedTradeDate); } else { setConcepts([]); console.warn('No concepts found in response'); } } catch (err) { console.error('Failed to search concepts:', err); setError(err.message); setConcepts([]); } finally { setLoading(false); } }; // 当事件信息变化时,调用API搜索概念 useEffect(() => { if (eventTitle && eventTime) { // 格式化日期为 YYYY-MM-DD let formattedDate; try { // eventTime 可能是Date对象或字符串,使用 moment 处理 let eventMoment; // 检查是否是Date对象 if (eventTime instanceof Date) { eventMoment = moment(eventTime); } else if (typeof eventTime === 'string') { eventMoment = moment(eventTime); } else if (typeof eventTime === 'number') { eventMoment = moment(eventTime); } else { console.warn('Unknown eventTime format:', eventTime, typeof eventTime); eventMoment = moment(); } // 确保moment对象有效 if (!eventMoment.isValid()) { console.warn('Invalid eventTime:', eventTime); eventMoment = moment(); } formattedDate = eventMoment.format('YYYY-MM-DD'); // 如果时间是15:00之后,获取下一个交易日 if (eventMoment.hour() >= 15) { // 使用 tradingDayUtils 获取下一个交易日 if (tradingDayUtils && tradingDayUtils.getNextTradingDay) { const nextTradingDay = tradingDayUtils.getNextTradingDay(formattedDate); // 确保返回的是字符串格式 if (typeof nextTradingDay === 'string') { formattedDate = nextTradingDay; } else if (nextTradingDay instanceof Date) { formattedDate = moment(nextTradingDay).format('YYYY-MM-DD'); } else { console.warn('tradingDayUtils.getNextTradingDay returned invalid format:', nextTradingDay); formattedDate = eventMoment.add(1, 'day').format('YYYY-MM-DD'); } } else { // 降级处理:简单地加一天(不考虑周末和节假日) console.warn('tradingDayUtils.getNextTradingDay not available, using simple date addition'); formattedDate = eventMoment.add(1, 'day').format('YYYY-MM-DD'); } } } catch (e) { console.error('Failed to format event time:', e); // 使用当前交易日作为fallback if (tradingDayUtils && tradingDayUtils.getCurrentTradingDay) { const currentTradingDay = tradingDayUtils.getCurrentTradingDay(); // 确保返回的是字符串格式 if (typeof currentTradingDay === 'string') { formattedDate = currentTradingDay; } else if (currentTradingDay instanceof Date) { formattedDate = moment(currentTradingDay).format('YYYY-MM-DD'); } else { console.warn('tradingDayUtils.getCurrentTradingDay returned invalid format:', currentTradingDay); formattedDate = moment().format('YYYY-MM-DD'); } } else { formattedDate = moment().format('YYYY-MM-DD'); } } searchConcepts(eventTitle, formattedDate); } else if (!eventTitle) { console.warn('No event title provided for concept search'); setConcepts([]); } }, [eventTitle, eventTime]); const handleViewDetails = (concept) => { setSelectedConcept(concept); onOpen(); }; // 合并加载状态 const isLoading = externalLoading || loading; const displayError = externalError || error; // 加载状态 if (isLoading) { return ( {[1, 2, 3, 4].map((i) => ( ))} ); } // 错误状态 if (displayError) { return ( 加载相关概念失败: {displayError} ); } // 无数据状态 if (!concepts || concepts.length === 0) { return ( {eventTitle ? '未找到相关概念' : '暂无相关概念数据'} ); } return ( <> {/* 如果有交易日期,显示日期信息 */} {effectiveTradingDate && ( 涨跌幅数据日期:{effectiveTradingDate} {eventTime && effectiveTradingDate !== moment(eventTime).format('YYYY-MM-DD') && ( (事件发生于 {typeof eventTime === 'object' ? moment(eventTime).format('YYYY-MM-DD HH:mm') : eventTime},显示下一交易日数据) )} )} {/* 概念卡片网格 */} {concepts.map((concept, index) => ( ))} {/* 进入概念中心按钮 */}
探索更多概念板块,发现投资机会
{/* 增强版概念详情模态框 */} {selectedConcept?.concept} 相关度: {selectedConcept?.score?.toFixed(2)} {selectedConcept?.stock_count} 只股票 {selectedConcept?.price_info && ( {selectedConcept.price_info.trade_date || '暂无数据'} 0 ? 'red' : 'green'} fontSize="lg" px={4} py={2} > {selectedConcept.price_info.avg_change_pct > 0 ? '+' : ''} {selectedConcept.price_info.avg_change_pct?.toFixed(2) || '0.00'}% )} {/* 概念描述 - 完整版 */} {selectedConcept?.description && ( 概念解析 {selectedConcept.description} )} {/* 历史触发时间线 */} {selectedConcept?.happened_times && selectedConcept.happened_times.length > 0 && ( 历史触发时间 {selectedConcept.happened_times.map((time, idx) => ( {time} ))} )} {/* 相关股票详细列表 */} {selectedConcept?.stocks && selectedConcept.stocks.length > 0 && ( 核心相关股票 ({selectedConcept.stock_count}只) {selectedConcept.stocks.map((stock, idx) => ( {stock.stock_name} {stock.stock_code} {stock.reason && ( {stock.reason} )} {(stock.行业 || stock.项目) && ( {stock.行业 && ( {stock.行业} )} {stock.项目 && ( {stock.项目} )} )} ))} )} {/* 操作按钮 */} ); }; export default RelatedConcepts;