feat: 添加相关概念组件
This commit is contained in:
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user