updated
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
// src/views/EventDetail/components/RelatedConcepts.js - 支持概念API调用
|
||||
// src/views/EventDetail/components/RelatedConcepts.js - 支持交易日计算
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Icon, // 明确导入 Icon 组件
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
@@ -26,40 +25,37 @@ import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Button,
|
||||
Center,
|
||||
Divider
|
||||
Center
|
||||
} 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';
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'production' ? '/concept-api' : 'http://111.198.58.126:16801';
|
||||
|
||||
// 增强版 ConceptCard 组件 - 展示更多数据细节
|
||||
// ConceptCard 组件 - 修改为使用新的数据结构
|
||||
const ConceptCard = ({ concept, tradingDate, onViewDetails }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
const [imageError, setImageError] = 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 changeColor = concept.price_info?.avg_change_pct > 0 ? 'red.500' : 'green.500';
|
||||
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 handleImageLoad = () => {
|
||||
setImageLoading(false);
|
||||
};
|
||||
|
||||
// 处理概念点击
|
||||
const handleImageError = () => {
|
||||
setImageLoading(false);
|
||||
setImageError(true);
|
||||
};
|
||||
|
||||
// 处理概念点击 - 跳转到概念详情页
|
||||
const handleConceptClick = () => {
|
||||
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(concept.concept)}.html`, '_blank');
|
||||
};
|
||||
@@ -68,196 +64,100 @@ const ConceptCard = ({ concept, tradingDate, onViewDetails }) => {
|
||||
<Card
|
||||
bg={cardBg}
|
||||
borderColor={borderColor}
|
||||
borderWidth={2}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
shadow: 'xl',
|
||||
borderColor: 'blue.400'
|
||||
shadow: 'lg',
|
||||
borderColor: 'blue.300'
|
||||
}}
|
||||
transition="all 0.3s"
|
||||
transition="all 0.2s"
|
||||
onClick={handleConceptClick}
|
||||
>
|
||||
<CardBody p={5}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* 头部信息 */}
|
||||
<Box>
|
||||
<HStack justify="space-between" align="flex-start" mb={2}>
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<Text fontSize="lg" fontWeight="bold" color="blue.600">
|
||||
{concept.concept}
|
||||
</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
<Badge colorScheme="purple" fontSize="xs">
|
||||
相关度: {concept.score.toFixed(2)}
|
||||
</Badge>
|
||||
<Badge colorScheme="teal" fontSize="xs">
|
||||
{getMatchTypeName(concept.match_type)}
|
||||
</Badge>
|
||||
<Badge colorScheme="orange" fontSize="xs">
|
||||
{concept.stock_count} 只股票
|
||||
</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
{hasValidPriceInfo && (
|
||||
<Box textAlign="right">
|
||||
<Text fontSize="xs" color={textColor} mb={1}>
|
||||
{tradingDate || concept.price_info.trade_date}
|
||||
</Text>
|
||||
<Badge
|
||||
size="lg"
|
||||
colorScheme={changeColor}
|
||||
fontSize="md"
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
<CardBody p={4}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{/* 概念信息 */}
|
||||
<VStack spacing={2} align="stretch">
|
||||
<HStack justify="space-between" align="flex-start">
|
||||
<Text fontSize="md" fontWeight="bold" noOfLines={2}>
|
||||
{concept.concept}
|
||||
</Text>
|
||||
{hasValidPriceInfo ? (
|
||||
<Tooltip label={`${tradingDate} 平均涨跌幅`}>
|
||||
<Badge size="sm" colorScheme={concept.price_info.avg_change_pct > 0 ? 'red' : 'green'}>
|
||||
{changeSymbol}{concept.price_info.avg_change_pct?.toFixed(2)}%
|
||||
</Badge>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label={tradingDate ? `${tradingDate} 暂无数据` : '暂无涨跌数据'}>
|
||||
<Badge size="sm" variant="outline" colorScheme="gray">
|
||||
--%
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
{/* 股票数量和相关度 */}
|
||||
<HStack spacing={2}>
|
||||
<Badge variant="subtle" colorScheme="blue">
|
||||
{concept.stock_count} 只股票
|
||||
</Badge>
|
||||
<Badge variant="subtle" colorScheme="purple">
|
||||
相关度: {(concept.score * 10).toFixed(1)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
{/* 概念描述 */}
|
||||
<Box>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={textColor}
|
||||
noOfLines={isExpanded ? undefined : 4}
|
||||
lineHeight="1.6"
|
||||
>
|
||||
{concept.description}
|
||||
</Text>
|
||||
{concept.description && concept.description.length > 200 && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="link"
|
||||
colorScheme="blue"
|
||||
mt={1}
|
||||
{/* 概念描述 */}
|
||||
{concept.description && (
|
||||
<Text fontSize="sm" color={textColor} noOfLines={3}>
|
||||
{concept.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 部分股票展示 */}
|
||||
{concept.stocks && concept.stocks.length > 0 && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color={textColor} mb={1}>
|
||||
相关股票:
|
||||
</Text>
|
||||
<HStack spacing={1} flexWrap="wrap">
|
||||
{concept.stocks.slice(0, 3).map((stock, idx) => (
|
||||
<Badge key={idx} size="sm" variant="outline">
|
||||
{stock.stock_name}
|
||||
</Badge>
|
||||
))}
|
||||
{concept.stocks.length > 3 && (
|
||||
<Text fontSize="xs" color={textColor}>
|
||||
等{concept.stock_count}只
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<HStack justify="space-between" align="center" pt={2}>
|
||||
<Text fontSize="xs" color={textColor}>
|
||||
点击查看概念详情
|
||||
</Text>
|
||||
<IconButton
|
||||
icon={<FaExternalLinkAlt />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="查看详情"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
handleConceptClick();
|
||||
}}
|
||||
>
|
||||
{isExpanded ? '收起' : '展开更多'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 历史发生时间 */}
|
||||
{concept.happened_times && concept.happened_times.length > 0 && (
|
||||
<Box>
|
||||
<Text fontSize="xs" fontWeight="semibold" mb={2} color={textColor}>
|
||||
历史触发时间:
|
||||
</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{concept.happened_times.map((time, idx) => (
|
||||
<Badge key={idx} variant="subtle" colorScheme="gray" fontSize="xs">
|
||||
{time}
|
||||
</Badge>
|
||||
))}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 相关股票展示 - 增强版 */}
|
||||
{concept.stocks && concept.stocks.length > 0 && (
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={2}>
|
||||
<Text fontSize="sm" fontWeight="semibold" color={textColor}>
|
||||
核心相关股票
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
共 {concept.stock_count} 只
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={2}>
|
||||
{concept.stocks.slice(0, isExpanded ? 8 : 4).map((stock, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
bg={useColorModeValue('gray.50', 'gray.700')}
|
||||
fontSize="xs"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="semibold">
|
||||
{stock.stock_name}
|
||||
</Text>
|
||||
<Badge size="sm" variant="outline">
|
||||
{stock.stock_code}
|
||||
</Badge>
|
||||
</HStack>
|
||||
{stock.reason && (
|
||||
<Text fontSize="xs" color={textColor} mt={1} noOfLines={2}>
|
||||
{stock.reason}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{concept.stocks.length > 4 && !isExpanded && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
mt={2}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(true);
|
||||
}}
|
||||
>
|
||||
查看更多股票
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<HStack spacing={2} pt={2}>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
leftIcon={<FaChartLine />}
|
||||
flex={1}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleConceptClick();
|
||||
}}
|
||||
>
|
||||
查看概念详情
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
leftIcon={<FaEye />}
|
||||
flex={1}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewDetails(concept);
|
||||
}}
|
||||
>
|
||||
快速预览
|
||||
</Button>
|
||||
</HStack>
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 主组件 - 修改为接收事件信息并调用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 <div>组件加载错误:Icon 组件未定义</div>;
|
||||
}
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [selectedConcept, setSelectedConcept] = useState(null);
|
||||
const [concepts, setConcepts] = useState([]);
|
||||
@@ -267,63 +167,37 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
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');
|
||||
// 计算有效交易日
|
||||
useEffect(() => {
|
||||
if (eventTime) {
|
||||
const tradingDate = tradingDayUtils.getEffectiveTradingDay(eventTime);
|
||||
setEffectiveTradingDate(tradingDate);
|
||||
console.log('事件时间:', eventTime, '-> 有效交易日:', tradingDate);
|
||||
}
|
||||
|
||||
// 验证新的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;
|
||||
};
|
||||
}, [eventTime]);
|
||||
|
||||
// 搜索概念函数
|
||||
const searchConcepts = async (title, tradingDate) => {
|
||||
if (!title) {
|
||||
setConcepts([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 搜索相关概念
|
||||
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,
|
||||
size: 4, // 返回前4个最相关的概念
|
||||
page: 1,
|
||||
sort_by: "_score",
|
||||
trade_date: formattedTradeDate
|
||||
sort_by: "_score" // 按相关度排序
|
||||
};
|
||||
|
||||
console.log('Searching concepts with:', requestBody);
|
||||
// 如果有交易日期,添加到请求中
|
||||
if (tradingDate) {
|
||||
requestBody.trade_date = tradingDate;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/search`, {
|
||||
method: 'POST',
|
||||
@@ -334,35 +208,18 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
throw new Error(`搜索失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Concept search response:', data);
|
||||
setConcepts(data.results || []);
|
||||
|
||||
// 数据验证
|
||||
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');
|
||||
// 如果返回了价格日期,更新显示
|
||||
if (data.price_date && data.price_date !== tradingDate) {
|
||||
console.log('API返回的实际价格日期:', data.price_date);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to search concepts:', err);
|
||||
console.error('概念搜索错误:', err);
|
||||
setError(err.message);
|
||||
setConcepts([]);
|
||||
} finally {
|
||||
@@ -370,92 +227,23 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
}
|
||||
};
|
||||
|
||||
// 当事件信息变化时,调用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([]);
|
||||
if (eventTitle && effectiveTradingDate) {
|
||||
searchConcepts(eventTitle, effectiveTradingDate);
|
||||
} else if (eventTitle) {
|
||||
// 如果没有交易日期,仍然搜索但不带日期参数
|
||||
searchConcepts(eventTitle, null);
|
||||
}
|
||||
}, [eventTitle, eventTime]);
|
||||
}, [eventTitle, effectiveTradingDate]);
|
||||
|
||||
const handleViewDetails = (concept) => {
|
||||
setSelectedConcept(concept);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
// 合并加载状态
|
||||
const isLoading = externalLoading || loading;
|
||||
const displayError = externalError || error;
|
||||
|
||||
// 加载状态
|
||||
if (isLoading) {
|
||||
if (loading || externalLoading) {
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
@@ -468,11 +256,11 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (displayError) {
|
||||
if (error || externalError) {
|
||||
return (
|
||||
<Alert status="error" borderRadius="lg">
|
||||
<AlertIcon />
|
||||
加载相关概念失败: {displayError}
|
||||
加载相关概念失败: {error || externalError}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -481,9 +269,7 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
if (!concepts || concepts.length === 0) {
|
||||
return (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="gray.500" mb={4}>
|
||||
{eventTitle ? '未找到相关概念' : '暂无相关概念数据'}
|
||||
</Text>
|
||||
<Text color="gray.500" mb={4}>暂无相关概念数据</Text>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
@@ -505,9 +291,9 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
<FaCalendarAlt color={textColor} />
|
||||
<Text fontSize="sm" color={textColor}>
|
||||
涨跌幅数据日期:{effectiveTradingDate}
|
||||
{eventTime && effectiveTradingDate !== moment(eventTime).format('YYYY-MM-DD') && (
|
||||
{eventTime && effectiveTradingDate !== eventTime.split(' ')[0] && (
|
||||
<Text as="span" ml={2} fontSize="xs">
|
||||
(事件发生于 {typeof eventTime === 'object' ? moment(eventTime).format('YYYY-MM-DD HH:mm') : eventTime},显示下一交易日数据)
|
||||
(事件发生于 {eventTime},显示下一交易日数据)
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
@@ -517,9 +303,9 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
|
||||
{/* 概念卡片网格 */}
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{concepts.map((concept, index) => (
|
||||
{concepts.map((concept) => (
|
||||
<ConceptCard
|
||||
key={concept.concept_id || index}
|
||||
key={concept.concept_id}
|
||||
concept={concept}
|
||||
tradingDate={effectiveTradingDate}
|
||||
onViewDetails={handleViewDetails}
|
||||
@@ -555,183 +341,73 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
</VStack>
|
||||
</Center>
|
||||
|
||||
{/* 增强版概念详情模态框 */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
|
||||
{/* 概念详情模态框 */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxH="90vh">
|
||||
<ModalHeader borderBottomWidth={1}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1}>
|
||||
<Text fontSize="xl">{selectedConcept?.concept}</Text>
|
||||
<HStack spacing={2}>
|
||||
<Badge colorScheme="purple">
|
||||
相关度: {selectedConcept?.score?.toFixed(2)}
|
||||
</Badge>
|
||||
<Badge colorScheme="teal">
|
||||
{selectedConcept?.stock_count} 只股票
|
||||
</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<Text>{selectedConcept?.concept}</Text>
|
||||
{selectedConcept?.price_info && (
|
||||
<VStack align="end" spacing={1}>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{selectedConcept.price_info.trade_date || '暂无数据'}
|
||||
</Text>
|
||||
<Badge
|
||||
size="lg"
|
||||
colorScheme={selectedConcept.price_info.avg_change_pct > 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'}%
|
||||
</Badge>
|
||||
</VStack>
|
||||
<Badge colorScheme={selectedConcept.price_info.avg_change_pct > 0 ? 'red' : 'green'}>
|
||||
{selectedConcept.price_info.avg_change_pct > 0 ? '+' : ''}
|
||||
{selectedConcept.price_info.avg_change_pct?.toFixed(2)}%
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody pb={6} overflowY="auto">
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 概念描述 - 完整版 */}
|
||||
<ModalBody pb={6}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* 显示交易日期 */}
|
||||
{effectiveTradingDate && (
|
||||
<HStack>
|
||||
<FaCalendarAlt />
|
||||
<Text fontSize="sm">
|
||||
数据日期:{effectiveTradingDate}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 概念描述 */}
|
||||
{selectedConcept?.description && (
|
||||
<Box>
|
||||
<HStack mb={3}>
|
||||
<Icon as={FaChartLine} color="blue.500" />
|
||||
<Text fontSize="md" fontWeight="bold">
|
||||
概念解析
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box
|
||||
p={4}
|
||||
bg={useColorModeValue('blue.50', 'blue.900')}
|
||||
borderRadius="md"
|
||||
>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={useColorModeValue('gray.700', 'gray.300')}
|
||||
lineHeight="1.8"
|
||||
>
|
||||
{selectedConcept.description}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
概念描述:
|
||||
</Text>
|
||||
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.400')}>
|
||||
{selectedConcept.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 历史触发时间线 */}
|
||||
{selectedConcept?.happened_times && selectedConcept.happened_times.length > 0 && (
|
||||
<Box>
|
||||
<HStack mb={3}>
|
||||
<Icon as={FaCalendarAlt} color="purple.500" />
|
||||
<Text fontSize="md" fontWeight="bold">
|
||||
历史触发时间
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={3} flexWrap="wrap">
|
||||
{selectedConcept.happened_times.map((time, idx) => (
|
||||
<Badge
|
||||
key={idx}
|
||||
colorScheme="purple"
|
||||
variant="subtle"
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{time}
|
||||
</Badge>
|
||||
))}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 相关股票详细列表 */}
|
||||
{/* 相关股票 */}
|
||||
{selectedConcept?.stocks && selectedConcept.stocks.length > 0 && (
|
||||
<Box>
|
||||
<HStack mb={3}>
|
||||
<Icon as={FaEye} color="green.500" />
|
||||
<Text fontSize="md" fontWeight="bold">
|
||||
核心相关股票 ({selectedConcept.stock_count}只)
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box
|
||||
maxH="300px"
|
||||
overflowY="auto"
|
||||
borderWidth={1}
|
||||
borderRadius="md"
|
||||
p={3}
|
||||
>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3}>
|
||||
{selectedConcept.stocks.map((stock, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
p={3}
|
||||
borderWidth={1}
|
||||
borderRadius="md"
|
||||
bg={useColorModeValue('white', 'gray.700')}
|
||||
_hover={{
|
||||
bg: useColorModeValue('gray.50', 'gray.600'),
|
||||
borderColor: 'blue.300'
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<HStack justify="space-between" mb={2}>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
{stock.stock_name}
|
||||
</Text>
|
||||
<Badge colorScheme="blue" fontSize="xs">
|
||||
{stock.stock_code}
|
||||
</Badge>
|
||||
</HStack>
|
||||
{stock.reason && (
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
{stock.reason}
|
||||
</Text>
|
||||
)}
|
||||
{(stock.行业 || stock.项目) && (
|
||||
<HStack spacing={2} mt={2}>
|
||||
{stock.行业 && (
|
||||
<Badge size="sm" variant="subtle">
|
||||
{stock.行业}
|
||||
</Badge>
|
||||
)}
|
||||
{stock.项目 && (
|
||||
<Badge size="sm" variant="subtle" colorScheme="green">
|
||||
{stock.项目}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
相关股票 ({selectedConcept.stock_count}只):
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing={2}>
|
||||
{selectedConcept.stocks.slice(0, 6).map((stock, index) => (
|
||||
<Badge key={index} p={2} variant="outline">
|
||||
{stock.stock_name} ({stock.stock_code})
|
||||
</Badge>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<HStack spacing={3} pt={4}>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
flex={1}
|
||||
onClick={() => {
|
||||
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(selectedConcept.concept)}.html`, '_blank');
|
||||
}}
|
||||
leftIcon={<FaExternalLinkAlt />}
|
||||
>
|
||||
查看概念详情页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
flex={1}
|
||||
onClick={onClose}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</HStack>
|
||||
{/* 查看详情按钮 */}
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={() => {
|
||||
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(selectedConcept.concept)}.html`, '_blank');
|
||||
}}
|
||||
leftIcon={<FaExternalLinkAlt />}
|
||||
>
|
||||
查看概念详情页
|
||||
</Button>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
|
||||
Reference in New Issue
Block a user