Files
vf_react/src/components/EventDetailPanel/RelatedConceptsSection/DetailedConceptCard.js
zdl ee33f7ffd7 refactor: 重构 Community 目录,将公共组件迁移到 src/components/
- 迁移 klineDataCache.js 到 src/utils/stock/(被 StockChart 使用)
- 迁移 InvestmentCalendar 到 src/components/InvestmentCalendar/(被 Navbar、Dashboard 使用)
- 迁移 DynamicNewsDetail 到 src/components/EventDetailPanel/(被 EventDetail 使用)
- 更新所有相关导入路径,使用路径别名
- 保持 Community 目录其余结构不变

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 12:09:24 +08:00

192 lines
6.3 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
* - concept: 概念名称
* - 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 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 ? '+' : '';
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">
<Badge colorScheme="purple" fontSize="xs">
相关度: {relevanceScore}%
</Badge>
<Badge colorScheme="orange" fontSize="xs">
{concept.stock_count} 只股票
</Badge>
</HStack>
</VStack>
{/* 右侧:涨跌幅 */}
{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.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;