Files
vf_react/src/components/EventDetailPanel/RelatedConceptsSection/DetailedConceptCard.js

230 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/EventDetailPanel/RelatedConceptsSection/DetailedConceptCard.js
// 详细概念卡片组件
import React from 'react';
import {
Box,
HStack,
VStack,
Text,
Badge,
Card,
CardBody,
Divider,
SimpleGrid,
useColorModeValue,
} from '@chakra-ui/react';
import ConceptStockItem from './ConceptStockItem';
/**
* 详细概念卡片组件
* @param {Object} props
* @param {Object} props.concept - 概念对象(兼容 v1/v2 API 和 related_concepts 表数据)
* - concept: 概念名称
* - reason: 关联原因(来自 related_concepts 表)
* - stock_count: 相关股票数量
* - score: 相关度0-1
* - price_info.avg_change_pct: 平均涨跌幅
* - description: 概念描述
* - outbreak_dates / happened_times: 爆发日期数组
* - stocks: 相关股票数组 [{ name/stock_name, code/stock_code }]
* @param {Function} props.onClick - 点击回调
*/
const DetailedConceptCard = ({ concept, onClick }) => {
const cardBg = useColorModeValue('white', 'gray.700');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const headingColor = useColorModeValue('gray.700', 'gray.200');
const stockCountColor = useColorModeValue('gray.500', 'gray.400');
const reasonBg = useColorModeValue('blue.50', 'blue.900');
const reasonColor = useColorModeValue('gray.700', 'gray.200');
// 计算相关度百分比
const relevanceScore = Math.round((concept.score || 0) * 100);
// 计算涨跌幅颜色
const changePct = parseFloat(concept.price_info?.avg_change_pct);
const changeColor = changePct > 0 ? 'red' : changePct < 0 ? 'green' : 'gray';
const changeSymbol = changePct > 0 ? '+' : '';
// 判断是否来自数据库(有 reason 字段)
const isFromDatabase = !!concept.reason;
return (
<Card
bg={cardBg}
borderColor={borderColor}
borderWidth="2px"
cursor="pointer"
transition="all 0.3s"
_hover={{
transform: 'translateY(-2px)',
boxShadow: 'xl',
borderColor: 'blue.400'
}}
onClick={() => onClick(concept)}
>
<CardBody p={4}>
<VStack spacing={3} align="stretch">
{/* 头部信息 */}
<HStack justify="space-between" align="flex-start">
{/* 左侧:概念名称 + Badge */}
<VStack align="start" spacing={2} flex={1}>
<Text fontSize="md" fontWeight="bold" color="blue.600">
{concept.concept}
</Text>
<HStack spacing={2} flexWrap="wrap">
{/* 数据库数据显示"AI分析"标签,搜索数据显示相关度 */}
{isFromDatabase ? (
<Badge colorScheme="green" fontSize="xs">
AI 分析
</Badge>
) : (
<Badge colorScheme="purple" fontSize="xs">
相关度: {relevanceScore}%
</Badge>
)}
{/* 只有搜索数据才显示股票数量 */}
{!isFromDatabase && concept.stock_count > 0 && (
<Badge colorScheme="orange" fontSize="xs">
{concept.stock_count} 只股票
</Badge>
)}
</HStack>
</VStack>
{/* 右侧:涨跌幅(仅搜索数据有) */}
{!isFromDatabase && concept.price_info?.avg_change_pct && (
<Box textAlign="right">
<Text fontSize="xs" color={stockCountColor} mb={1}>
平均涨跌幅
</Text>
<Badge
size="lg"
colorScheme={changeColor}
fontSize="md"
px={3}
py={1}
>
{changeSymbol}{changePct.toFixed(2)}%
</Badge>
</Box>
)}
</HStack>
<Divider />
{/* 关联原因(来自数据库,突出显示) */}
{concept.reason && (
<Box
bg={reasonBg}
p={3}
borderRadius="md"
borderLeft="4px solid"
borderLeftColor="blue.400"
>
<Text fontSize="xs" fontWeight="bold" color="blue.500" mb={1}>
关联原因
</Text>
<Text
fontSize="sm"
color={reasonColor}
lineHeight="1.8"
>
{concept.reason}
</Text>
</Box>
)}
{/* 概念描述(仅搜索数据有,且没有 reason 时显示) */}
{!concept.reason && concept.description && (
<Text
fontSize="sm"
color={stockCountColor}
lineHeight="1.6"
noOfLines={3}
>
{concept.description}
</Text>
)}
{/* 爆发日期(兼容 happened_times 和 outbreak_dates */}
{((concept.outbreak_dates && concept.outbreak_dates.length > 0) ||
(concept.happened_times && concept.happened_times.length > 0)) && (
<Box>
<Text fontSize="xs" fontWeight="semibold" mb={2} color={stockCountColor}>
爆发日期
</Text>
<HStack spacing={2} flexWrap="wrap">
{(concept.outbreak_dates || concept.happened_times).map((date, idx) => (
<Badge key={idx} variant="subtle" colorScheme="orange" fontSize="xs">
{date}
</Badge>
))}
</HStack>
</Box>
)}
{/* 核心相关股票 */}
{concept.stocks && concept.stocks.length > 0 && (
<Box>
<HStack justify="space-between" mb={2}>
<Text fontSize="sm" fontWeight="semibold" color={headingColor}>
核心相关股票
</Text>
<Text fontSize="xs" color={stockCountColor}>
{concept.stock_count}
</Text>
</HStack>
{/* 可滚动容器 - 默认显示4条可滚动查看全部 */}
<Box
maxH="300px"
overflowY="auto"
pr={2}
onWheel={(e) => {
const element = e.currentTarget;
const scrollTop = element.scrollTop;
const scrollHeight = element.scrollHeight;
const clientHeight = element.clientHeight;
// 如果在滚动范围内,阻止事件冒泡到父容器
if (
(e.deltaY < 0 && scrollTop > 0) || // 向上滚动且未到顶部
(e.deltaY > 0 && scrollTop + clientHeight < scrollHeight) // 向下滚动且未到底部
) {
e.stopPropagation();
}
}}
css={{
overscrollBehavior: 'contain', // 防止滚动链
'&::-webkit-scrollbar': {
width: '6px',
},
'&::-webkit-scrollbar-track': {
background: '#f1f1f1',
},
'&::-webkit-scrollbar-thumb': {
background: '#888',
borderRadius: '3px',
},
'&::-webkit-scrollbar-thumb:hover': {
background: '#555',
},
}}
>
<SimpleGrid columns={{ base: 1 }} spacing={2}>
{concept.stocks.map((stock, idx) => (
<ConceptStockItem key={idx} stock={stock} />
))}
</SimpleGrid>
</Box>
</Box>
)}
</VStack>
</CardBody>
</Card>
);
};
export default DetailedConceptCard;