743 lines
33 KiB
JavaScript
743 lines
33 KiB
JavaScript
// 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 (
|
||
<Card
|
||
bg={cardBg}
|
||
borderColor={borderColor}
|
||
borderWidth={2}
|
||
_hover={{
|
||
transform: 'translateY(-2px)',
|
||
shadow: 'xl',
|
||
borderColor: 'blue.400'
|
||
}}
|
||
transition="all 0.3s"
|
||
>
|
||
<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}
|
||
>
|
||
{changeSymbol}{concept.price_info.avg_change_pct?.toFixed(2)}%
|
||
</Badge>
|
||
</Box>
|
||
)}
|
||
</HStack>
|
||
</Box>
|
||
|
||
<Divider />
|
||
|
||
{/* 概念描述 */}
|
||
<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}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setIsExpanded(!isExpanded);
|
||
}}
|
||
>
|
||
{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>
|
||
</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([]);
|
||
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 (
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||
{[1, 2, 3, 4].map((i) => (
|
||
<Box key={i}>
|
||
<Skeleton height="200px" borderRadius="lg" />
|
||
</Box>
|
||
))}
|
||
</SimpleGrid>
|
||
);
|
||
}
|
||
|
||
// 错误状态
|
||
if (displayError) {
|
||
return (
|
||
<Alert status="error" borderRadius="lg">
|
||
<AlertIcon />
|
||
加载相关概念失败: {displayError}
|
||
</Alert>
|
||
);
|
||
}
|
||
|
||
// 无数据状态
|
||
if (!concepts || concepts.length === 0) {
|
||
return (
|
||
<Box textAlign="center" py={8}>
|
||
<Text color="gray.500" mb={4}>
|
||
{eventTitle ? '未找到相关概念' : '暂无相关概念数据'}
|
||
</Text>
|
||
<Button
|
||
colorScheme="blue"
|
||
size="lg"
|
||
leftIcon={<FaChartLine />}
|
||
onClick={() => window.open('https://valuefrontier.cn/concepts', '_blank')}
|
||
>
|
||
进入概念中心
|
||
</Button>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{/* 如果有交易日期,显示日期信息 */}
|
||
{effectiveTradingDate && (
|
||
<Box mb={4} p={3} bg={bgColor} borderRadius="md">
|
||
<HStack spacing={2}>
|
||
<FaCalendarAlt color={textColor} />
|
||
<Text fontSize="sm" color={textColor}>
|
||
涨跌幅数据日期:{effectiveTradingDate}
|
||
{eventTime && effectiveTradingDate !== moment(eventTime).format('YYYY-MM-DD') && (
|
||
<Text as="span" ml={2} fontSize="xs">
|
||
(事件发生于 {typeof eventTime === 'object' ? moment(eventTime).format('YYYY-MM-DD HH:mm') : eventTime},显示下一交易日数据)
|
||
</Text>
|
||
)}
|
||
</Text>
|
||
</HStack>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 概念卡片网格 */}
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||
{concepts.map((concept, index) => (
|
||
<ConceptCard
|
||
key={concept.concept_id || index}
|
||
concept={concept}
|
||
tradingDate={effectiveTradingDate}
|
||
onViewDetails={handleViewDetails}
|
||
/>
|
||
))}
|
||
</SimpleGrid>
|
||
|
||
{/* 进入概念中心按钮 */}
|
||
<Center mt={8}>
|
||
<VStack spacing={3}>
|
||
<Button
|
||
colorScheme="blue"
|
||
size="lg"
|
||
leftIcon={<FaChartLine />}
|
||
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"
|
||
>
|
||
进入概念中心
|
||
</Button>
|
||
<Text fontSize="sm" color="gray.500">
|
||
探索更多概念板块,发现投资机会
|
||
</Text>
|
||
</VStack>
|
||
</Center>
|
||
|
||
{/* 增强版概念详情模态框 */}
|
||
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
|
||
<ModalOverlay />
|
||
<ModalContent maxH="90vh">
|
||
<ModalHeader borderBottomWidth={1}>
|
||
<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>
|
||
{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>
|
||
)}
|
||
</HStack>
|
||
</ModalHeader>
|
||
<ModalCloseButton />
|
||
|
||
<ModalBody pb={6} overflowY="auto">
|
||
<VStack spacing={6} align="stretch">
|
||
{/* 概念描述 - 完整版 */}
|
||
{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>
|
||
</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>
|
||
</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>
|
||
</VStack>
|
||
</ModalBody>
|
||
</ModalContent>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default RelatedConcepts; |