// 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 && (
)}
)}
{/* 操作按钮 */}
}
flex={1}
onClick={(e) => {
e.stopPropagation();
handleConceptClick();
}}
>
查看概念详情
}
flex={1}
onClick={(e) => {
e.stopPropagation();
onViewDetails(concept);
}}
>
快速预览
);
};
// 主组件 - 修改为接收事件信息并调用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 ? '未找到相关概念' : '暂无相关概念数据'}
}
onClick={() => window.open('https://valuefrontier.cn/concepts', '_blank')}
>
进入概念中心
);
}
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) => (
))}
{/* 进入概念中心按钮 */}
}
onClick={() => window.open('https://valuefrontier.cn/concepts', '_blank')}
px={8}
py={6}
fontSize="md"
fontWeight="bold"
bgGradient="linear(to-r, blue.400, cyan.400)"
_hover={{
bgGradient: "linear(to-r, blue.500, cyan.500)",
transform: "translateY(-2px)",
shadow: "lg"
}}
transition="all 0.2s"
>
进入概念中心
探索更多概念板块,发现投资机会
{/* 增强版概念详情模态框 */}
{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;