feat: 添加相关概念组件

This commit is contained in:
zdl
2025-10-31 20:08:53 +08:00
parent 57c4c3c959
commit fc251ede05
5 changed files with 456 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
// src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/ConceptStockItem.js
// 概念股票列表项组件
import React from 'react';
import {
Box,
HStack,
Text,
Badge,
useColorModeValue,
} from '@chakra-ui/react';
/**
* 概念股票列表项组件
* @param {Object} props
* @param {Object} props.stock - 股票对象
* - stock_name: 股票名称
* - stock_code: 股票代码
* - change_pct: 涨跌幅
* - reason: 关联原因
*/
const ConceptStockItem = ({ stock }) => {
const sectionBg = useColorModeValue('gray.50', 'gray.750');
const conceptNameColor = useColorModeValue('gray.800', 'gray.100');
const stockCountColor = useColorModeValue('gray.500', 'gray.400');
const stockChangePct = parseFloat(stock.change_pct);
const stockChangeColor = stockChangePct > 0 ? 'red' : stockChangePct < 0 ? 'green' : 'gray';
const stockChangeSymbol = stockChangePct > 0 ? '+' : '';
return (
<Box
p={2}
borderRadius="md"
bg={sectionBg}
fontSize="xs"
>
<HStack justify="space-between" mb={1}>
<HStack spacing={2}>
<Text fontWeight="semibold" color={conceptNameColor}>
{stock.stock_name}
</Text>
<Badge size="sm" variant="outline">
{stock.stock_code}
</Badge>
</HStack>
{stock.change_pct && (
<Badge
colorScheme={stockChangeColor}
fontSize="xs"
>
{stockChangeSymbol}{stockChangePct.toFixed(2)}%
</Badge>
)}
</HStack>
{stock.reason && (
<Text fontSize="xs" color={stockCountColor} mt={1} noOfLines={2}>
{stock.reason}
</Text>
)}
</Box>
);
};
export default ConceptStockItem;

View File

@@ -0,0 +1,150 @@
// src/views/Community/components/DynamicNewsDetail/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 - 概念对象
* - name: 概念名称
* - stock_count: 相关股票数量
* - relevance: 相关度0-100
* - avg_change_pct: 平均涨跌幅
* - description: 概念描述
* - happened_times: 历史触发时间数组
* - stocks: 相关股票数组
* @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 changePct = parseFloat(concept.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.name}
</Text>
<HStack spacing={2} flexWrap="wrap">
<Badge colorScheme="purple" fontSize="xs">
相关度: {concept.relevance}%
</Badge>
<Badge colorScheme="orange" fontSize="xs">
{concept.stock_count} 只股票
</Badge>
</HStack>
</VStack>
{/* 右侧:涨跌幅 */}
{concept.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>
)}
{/* 历史触发时间 */}
{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.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={headingColor}>
核心相关股票
</Text>
<Text fontSize="xs" color={stockCountColor}>
{concept.stock_count}
</Text>
</HStack>
<SimpleGrid columns={{ base: 1 }} spacing={2}>
{concept.stocks.slice(0, 4).map((stock, idx) => (
<ConceptStockItem key={idx} stock={stock} />
))}
</SimpleGrid>
</Box>
)}
</VStack>
</CardBody>
</Card>
);
};
export default DetailedConceptCard;

View File

@@ -0,0 +1,73 @@
// src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/SimpleConceptCard.js
// 简单概念卡片组件(横向卡片)
import React from 'react';
import {
Flex,
Box,
Text,
useColorModeValue,
} from '@chakra-ui/react';
/**
* 简单概念卡片组件
* @param {Object} props
* @param {Object} props.concept - 概念对象
* - name: 概念名称
* - stock_count: 相关股票数量
* - relevance: 相关度0-100
* @param {Function} props.onClick - 点击回调
* @param {Function} props.getRelevanceColor - 获取相关度颜色的函数
*/
const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
const cardBg = useColorModeValue('white', 'gray.700');
const conceptNameColor = useColorModeValue('gray.800', 'gray.100');
const borderColor = useColorModeValue('gray.300', 'gray.600');
const relevanceColors = getRelevanceColor(concept.relevance);
return (
<Flex
align="center"
justify="space-between"
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
px={4}
py={2}
cursor="pointer"
transition="all 0.2s"
minW="200px"
_hover={{
transform: 'translateY(-1px)',
boxShadow: 'md',
}}
onClick={() => onClick(concept)}
>
{/* 左侧:概念名 + 数量 */}
<Text fontSize="sm" fontWeight="normal" color={conceptNameColor} mr={3}>
{concept.name}{' '}
<Text as="span" color="gray.500">
({concept.stock_count})
</Text>
</Text>
{/* 右侧:相关度标签 */}
<Box
bg={relevanceColors.bg}
color={relevanceColors.color}
px={3}
py={1}
borderRadius="md"
flexShrink={0}
>
<Text fontSize="xs" fontWeight="medium" whiteSpace="nowrap">
相关度: {concept.relevance}%
</Text>
</Box>
</Flex>
);
};
export default SimpleConceptCard;

View File

@@ -0,0 +1,46 @@
// src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/TradingDateInfo.js
// 交易日期信息提示组件
import React from 'react';
import {
Box,
HStack,
Text,
useColorModeValue,
} from '@chakra-ui/react';
import { FaCalendarAlt } from 'react-icons/fa';
import moment from 'moment';
/**
* 交易日期信息提示组件
* @param {Object} props
* @param {string} props.effectiveTradingDate - 有效交易日期(涨跌幅数据日期)
* @param {string|Object} props.eventTime - 事件发生时间
*/
const TradingDateInfo = ({ effectiveTradingDate, eventTime }) => {
const sectionBg = useColorModeValue('gray.50', 'gray.750');
const headingColor = useColorModeValue('gray.700', 'gray.200');
const stockCountColor = useColorModeValue('gray.500', 'gray.400');
if (!effectiveTradingDate) {
return null;
}
return (
<Box mb={4} p={3} bg={sectionBg} borderRadius="md">
<HStack spacing={2}>
<FaCalendarAlt color="gray" />
<Text fontSize="sm" color={headingColor}>
涨跌幅数据日期{effectiveTradingDate}
{eventTime && effectiveTradingDate !== moment(eventTime).format('YYYY-MM-DD') && (
<Text as="span" ml={2} fontSize="xs" color={stockCountColor}>
(事件发生于 {typeof eventTime === 'object' ? moment(eventTime).format('YYYY-MM-DD HH:mm') : moment(eventTime).format('YYYY-MM-DD HH:mm')}显示下一交易日数据)
</Text>
)}
</Text>
</HStack>
</Box>
);
};
export default TradingDateInfo;

View File

@@ -0,0 +1,122 @@
// src/views/Community/components/DynamicNewsDetail/RelatedConceptsSection/index.js
// 相关概念区组件(主组件)
import React, { useState } from 'react';
import {
Box,
SimpleGrid,
Flex,
Button,
Collapse,
Heading,
useColorModeValue,
} from '@chakra-ui/react';
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
import { useNavigate } from 'react-router-dom';
import SimpleConceptCard from './SimpleConceptCard';
import DetailedConceptCard from './DetailedConceptCard';
import TradingDateInfo from './TradingDateInfo';
/**
* 相关概念区组件
* @param {Object} props
* @param {Array<Object>} props.keywords - 相关概念数组
* - name: 概念名称
* - stock_count: 相关股票数量
* - relevance: 相关度0-100
* @param {string} props.effectiveTradingDate - 有效交易日期(涨跌幅数据日期)
* @param {string|Object} props.eventTime - 事件发生时间
*/
const RelatedConceptsSection = ({ keywords, effectiveTradingDate, eventTime }) => {
const [isExpanded, setIsExpanded] = useState(false);
const navigate = useNavigate();
// 颜色配置
const sectionBg = useColorModeValue('gray.50', 'gray.750');
const headingColor = useColorModeValue('gray.700', 'gray.200');
// 如果没有关键词,不渲染
if (!keywords || keywords.length === 0) {
return null;
}
/**
* 根据相关度获取颜色(浅色背景 + 深色文字)
* @param {number} relevance - 相关度0-100
* @returns {Object} 包含背景色和文字色
*/
const getRelevanceColor = (relevance) => {
if (relevance >= 90) {
return { bg: 'purple.50', color: 'purple.800' }; // 极高相关
} else if (relevance >= 80) {
return { bg: 'pink.50', color: 'pink.800' }; // 高相关
} else if (relevance >= 70) {
return { bg: 'orange.50', color: 'orange.800' }; // 中等相关
} else {
return { bg: 'gray.100', color: 'gray.700' }; // 低相关
}
};
/**
* 处理概念点击
* @param {Object} concept - 概念对象
*/
const handleConceptClick = (concept) => {
// 跳转到概念详情页
navigate(`/concept/${concept.name}`);
};
return (
<Box bg={sectionBg} p={3} borderRadius="md">
{/* 标题栏 */}
<Flex justify="space-between" align="center" mb={3}>
<Heading size="sm" color={headingColor}>
相关概念
</Heading>
<Button
size="sm"
variant="ghost"
colorScheme="blue"
rightIcon={isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? '收起' : '查看详细描述'}
</Button>
</Flex>
{/* 简单模式:横向卡片列表(总是显示) */}
<Flex gap={2} flexWrap="wrap" mb={isExpanded ? 3 : 0}>
{keywords.map((concept, index) => (
<SimpleConceptCard
key={index}
concept={concept}
onClick={handleConceptClick}
getRelevanceColor={getRelevanceColor}
/>
))}
</Flex>
{/* 详细模式:卡片网格(可折叠) */}
<Collapse in={isExpanded} animateOpacity>
{/* 交易日期信息 */}
<TradingDateInfo
effectiveTradingDate={effectiveTradingDate}
eventTime={eventTime}
/>
{/* 详细概念卡片网格 */}
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{keywords.map((concept, index) => (
<DetailedConceptCard
key={index}
concept={concept}
onClick={handleConceptClick}
/>
))}
</SimpleGrid>
</Collapse>
</Box>
);
};
export default RelatedConceptsSection;