updated
This commit is contained in:
0
src/views/EventDetail/components/ChainAnalysis.js
Normal file
0
src/views/EventDetail/components/ChainAnalysis.js
Normal file
0
src/views/EventDetail/components/ConceptCards.js
Normal file
0
src/views/EventDetail/components/ConceptCards.js
Normal file
@@ -33,8 +33,7 @@ import {
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Link
|
||||
TableContainer
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FaExclamationTriangle,
|
||||
@@ -145,47 +144,6 @@ const HistoricalEvents = ({
|
||||
return `${Math.floor(diffDays / 365)}年前`;
|
||||
};
|
||||
|
||||
// 可展开的文本组件
|
||||
const ExpandableText = ({ text, maxLength = 20 }) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
const [shouldTruncate, setShouldTruncate] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (text && text.length > maxLength) {
|
||||
setShouldTruncate(true);
|
||||
} else {
|
||||
setShouldTruncate(false);
|
||||
}
|
||||
}, [text, maxLength]);
|
||||
|
||||
if (!text) return <Text fontSize="xs">--</Text>;
|
||||
|
||||
const displayText = shouldTruncate && !isOpen
|
||||
? text.substring(0, maxLength) + '...'
|
||||
: text;
|
||||
|
||||
return (
|
||||
<VStack align="flex-start" spacing={1}>
|
||||
<Text fontSize="xs" noOfLines={isOpen ? undefined : 2} maxW="300px">
|
||||
{displayText}{text.includes('AI合成') ? '' : '(AI合成)'}
|
||||
</Text>
|
||||
{shouldTruncate && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="link"
|
||||
color="blue.500"
|
||||
onClick={onToggle}
|
||||
height="auto"
|
||||
py={0}
|
||||
minH={0}
|
||||
>
|
||||
{isOpen ? '收起' : '展开'}
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -248,7 +206,7 @@ const HistoricalEvents = ({
|
||||
超预期得分: {expectationScore}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="yellow.700">
|
||||
基于历史事件判断当前事件的超预期情况,满分100分(AI合成)
|
||||
基于历史事件判断当前事件的超预期情况,满分100分
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
@@ -359,10 +317,10 @@ const HistoricalEvents = ({
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 事件简介 */}
|
||||
<Text fontSize="sm" color={textSecondary} lineHeight="1.5">
|
||||
{event.content ? `${event.content}(AI合成)` : '暂无内容'}
|
||||
</Text>
|
||||
{/* 事件简介 */}
|
||||
<Text fontSize="sm" color={textSecondary} lineHeight="1.5">
|
||||
{event.content || '暂无内容'}
|
||||
</Text>
|
||||
|
||||
{/* 展开的详细信息 */}
|
||||
<Collapse in={isExpanded} animateOpacity>
|
||||
@@ -401,9 +359,9 @@ const HistoricalEvents = ({
|
||||
</VStack>
|
||||
|
||||
{/* 事件相关股票模态框 */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW="80vw" maxH="85vh">
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<VStack align="flex-start" spacing={1}>
|
||||
<Text>{selectedEvent?.title || '历史事件'}</Text>
|
||||
@@ -414,7 +372,7 @@ const HistoricalEvents = ({
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody overflowY="auto" maxH="calc(85vh - 180px)">
|
||||
<ModalBody>
|
||||
{loadingStocks ? (
|
||||
<VStack spacing={4} py={8}>
|
||||
<Spinner size="lg" color="blue.500" />
|
||||
@@ -423,7 +381,6 @@ const HistoricalEvents = ({
|
||||
) : (
|
||||
<StocksList
|
||||
stocks={selectedEvent ? eventStocks[selectedEvent.id] || [] : []}
|
||||
eventTradingDate={selectedEvent ? selectedEvent.event_date : null}
|
||||
/>
|
||||
)}
|
||||
</ModalBody>
|
||||
@@ -438,14 +395,8 @@ const HistoricalEvents = ({
|
||||
};
|
||||
|
||||
// 股票列表子组件
|
||||
const StocksList = ({ stocks, eventTradingDate }) => {
|
||||
const StocksList = ({ stocks }) => {
|
||||
const textSecondary = useColorModeValue('gray.600', 'gray.400');
|
||||
|
||||
// 处理股票代码,移除.SZ/.SH后缀
|
||||
const formatStockCode = (stockCode) => {
|
||||
if (!stockCode) return '';
|
||||
return stockCode.replace(/\.(SZ|SH)$/i, '');
|
||||
};
|
||||
|
||||
if (!stocks || stocks.length === 0) {
|
||||
return (
|
||||
@@ -458,81 +409,50 @@ const StocksList = ({ stocks, eventTradingDate }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{eventTradingDate && (
|
||||
<Box mb={4} p={3} bg={useColorModeValue('blue.50', 'blue.900')} borderRadius="md">
|
||||
<Text fontSize="sm" color={useColorModeValue('blue.700', 'blue.300')}>
|
||||
📅 事件对应交易日:{new Date(eventTradingDate).toLocaleDateString('zh-CN')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<TableContainer>
|
||||
<Table size="md">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>股票代码</Th>
|
||||
<Th>股票名称</Th>
|
||||
<Th>板块</Th>
|
||||
<Th isNumeric>相关度</Th>
|
||||
<Th isNumeric>事件日涨幅</Th>
|
||||
<Th>关联原因</Th>
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>股票代码</Th>
|
||||
<Th>股票名称</Th>
|
||||
<Th>板块</Th>
|
||||
<Th isNumeric>相关度</Th>
|
||||
<Th>关联原因</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{stocks.map((stock, index) => (
|
||||
<Tr key={stock.id || index}>
|
||||
<Td fontFamily="mono" fontWeight="medium">
|
||||
{stock.stock_code}
|
||||
</Td>
|
||||
<Td>{stock.stock_name || '--'}</Td>
|
||||
<Td>
|
||||
<Badge size="sm" variant="outline">
|
||||
{stock.sector || '未知'}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Badge
|
||||
colorScheme={
|
||||
stock.correlation >= 0.8 ? 'red' :
|
||||
stock.correlation >= 0.6 ? 'orange' : 'green'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{Math.round((stock.correlation || 0) * 100)}%
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Text fontSize="xs" noOfLines={2} maxW="200px">
|
||||
{stock.relation_desc || '--'}
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{stocks.map((stock, index) => (
|
||||
<Tr key={stock.id || index}>
|
||||
<Td fontFamily="mono" fontWeight="medium">
|
||||
<Link
|
||||
href={`https://valuefrontier.cn/company?scode=${stock.stock_code ? stock.stock_code.replace(/\.(SZ|SH)$/i, '') : ''}`}
|
||||
isExternal
|
||||
color="blue.500"
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{stock.stock_code ? stock.stock_code.replace(/\.(SZ|SH)$/i, '') : ''}
|
||||
</Link>
|
||||
</Td>
|
||||
<Td>{stock.stock_name || '--'}</Td>
|
||||
<Td>
|
||||
<Badge size="sm" variant="outline">
|
||||
{stock.sector || '未知'}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<Badge
|
||||
colorScheme={
|
||||
stock.correlation >= 0.8 ? 'red' :
|
||||
stock.correlation >= 0.6 ? 'orange' : 'green'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{Math.round((stock.correlation || 0) * 100)}%
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
{stock.event_day_change_pct !== null && stock.event_day_change_pct !== undefined ? (
|
||||
<Text
|
||||
fontWeight="medium"
|
||||
color={stock.event_day_change_pct >= 0 ? 'red.500' : 'green.500'}
|
||||
>
|
||||
{stock.event_day_change_pct >= 0 ? '+' : ''}{stock.event_day_change_pct.toFixed(2)}%
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={textSecondary} fontSize="sm">--</Text>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<VStack align="flex-start" spacing={1}>
|
||||
<Text fontSize="xs" noOfLines={2} maxW="300px">
|
||||
{stock.relation_desc ? `${stock.relation_desc}(AI合成)` : '--'}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// src/views/EventDetail/components/RelatedConcepts.js - 支持概念API调用
|
||||
// src/views/EventDetail/components/RelatedConcepts.js - 支持交易日计算
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Icon, // 明确导入 Icon 组件
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
@@ -26,40 +25,37 @@ import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Button,
|
||||
Center,
|
||||
Divider
|
||||
Center
|
||||
} 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';
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'production' ? '/concept-api' : 'http://111.198.58.126:16801';
|
||||
|
||||
// 增强版 ConceptCard 组件 - 展示更多数据细节
|
||||
// ConceptCard 组件 - 修改为使用新的数据结构
|
||||
const ConceptCard = ({ concept, tradingDate, onViewDetails }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
const [imageError, setImageError] = 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 changeColor = concept.price_info?.avg_change_pct > 0 ? 'red.500' : 'green.500';
|
||||
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 handleImageLoad = () => {
|
||||
setImageLoading(false);
|
||||
};
|
||||
|
||||
// 处理概念点击
|
||||
const handleImageError = () => {
|
||||
setImageLoading(false);
|
||||
setImageError(true);
|
||||
};
|
||||
|
||||
// 处理概念点击 - 跳转到概念详情页
|
||||
const handleConceptClick = () => {
|
||||
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(concept.concept)}.html`, '_blank');
|
||||
};
|
||||
@@ -68,196 +64,100 @@ const ConceptCard = ({ concept, tradingDate, onViewDetails }) => {
|
||||
<Card
|
||||
bg={cardBg}
|
||||
borderColor={borderColor}
|
||||
borderWidth={2}
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
shadow: 'xl',
|
||||
borderColor: 'blue.400'
|
||||
shadow: 'lg',
|
||||
borderColor: 'blue.300'
|
||||
}}
|
||||
transition="all 0.3s"
|
||||
transition="all 0.2s"
|
||||
onClick={handleConceptClick}
|
||||
>
|
||||
<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}
|
||||
>
|
||||
<CardBody p={4}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{/* 概念信息 */}
|
||||
<VStack spacing={2} align="stretch">
|
||||
<HStack justify="space-between" align="flex-start">
|
||||
<Text fontSize="md" fontWeight="bold" noOfLines={2}>
|
||||
{concept.concept}
|
||||
</Text>
|
||||
{hasValidPriceInfo ? (
|
||||
<Tooltip label={`${tradingDate} 平均涨跌幅`}>
|
||||
<Badge size="sm" colorScheme={concept.price_info.avg_change_pct > 0 ? 'red' : 'green'}>
|
||||
{changeSymbol}{concept.price_info.avg_change_pct?.toFixed(2)}%
|
||||
</Badge>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label={tradingDate ? `${tradingDate} 暂无数据` : '暂无涨跌数据'}>
|
||||
<Badge size="sm" variant="outline" colorScheme="gray">
|
||||
--%
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
{/* 股票数量和相关度 */}
|
||||
<HStack spacing={2}>
|
||||
<Badge variant="subtle" colorScheme="blue">
|
||||
{concept.stock_count} 只股票
|
||||
</Badge>
|
||||
<Badge variant="subtle" colorScheme="purple">
|
||||
相关度: {(concept.score * 10).toFixed(1)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
{/* 概念描述 */}
|
||||
<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}
|
||||
{/* 概念描述 */}
|
||||
{concept.description && (
|
||||
<Text fontSize="sm" color={textColor} noOfLines={3}>
|
||||
{concept.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 部分股票展示 */}
|
||||
{concept.stocks && concept.stocks.length > 0 && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color={textColor} mb={1}>
|
||||
相关股票:
|
||||
</Text>
|
||||
<HStack spacing={1} flexWrap="wrap">
|
||||
{concept.stocks.slice(0, 3).map((stock, idx) => (
|
||||
<Badge key={idx} size="sm" variant="outline">
|
||||
{stock.stock_name}
|
||||
</Badge>
|
||||
))}
|
||||
{concept.stocks.length > 3 && (
|
||||
<Text fontSize="xs" color={textColor}>
|
||||
等{concept.stock_count}只
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<HStack justify="space-between" align="center" pt={2}>
|
||||
<Text fontSize="xs" color={textColor}>
|
||||
点击查看概念详情
|
||||
</Text>
|
||||
<IconButton
|
||||
icon={<FaExternalLinkAlt />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="查看详情"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
handleConceptClick();
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</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([]);
|
||||
@@ -267,63 +167,37 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
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');
|
||||
// 计算有效交易日
|
||||
useEffect(() => {
|
||||
if (eventTime) {
|
||||
const tradingDate = tradingDayUtils.getEffectiveTradingDay(eventTime);
|
||||
setEffectiveTradingDate(tradingDate);
|
||||
console.log('事件时间:', eventTime, '-> 有效交易日:', tradingDate);
|
||||
}
|
||||
|
||||
// 验证新的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;
|
||||
};
|
||||
}, [eventTime]);
|
||||
|
||||
// 搜索概念函数
|
||||
const searchConcepts = async (title, tradingDate) => {
|
||||
if (!title) {
|
||||
setConcepts([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 搜索相关概念
|
||||
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,
|
||||
size: 4, // 返回前4个最相关的概念
|
||||
page: 1,
|
||||
sort_by: "_score",
|
||||
trade_date: formattedTradeDate
|
||||
sort_by: "_score" // 按相关度排序
|
||||
};
|
||||
|
||||
console.log('Searching concepts with:', requestBody);
|
||||
// 如果有交易日期,添加到请求中
|
||||
if (tradingDate) {
|
||||
requestBody.trade_date = tradingDate;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/search`, {
|
||||
method: 'POST',
|
||||
@@ -334,35 +208,18 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
throw new Error(`搜索失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Concept search response:', data);
|
||||
setConcepts(data.results || []);
|
||||
|
||||
// 数据验证
|
||||
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');
|
||||
// 如果返回了价格日期,更新显示
|
||||
if (data.price_date && data.price_date !== tradingDate) {
|
||||
console.log('API返回的实际价格日期:', data.price_date);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to search concepts:', err);
|
||||
console.error('概念搜索错误:', err);
|
||||
setError(err.message);
|
||||
setConcepts([]);
|
||||
} finally {
|
||||
@@ -370,92 +227,23 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
}
|
||||
};
|
||||
|
||||
// 当事件信息变化时,调用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([]);
|
||||
if (eventTitle && effectiveTradingDate) {
|
||||
searchConcepts(eventTitle, effectiveTradingDate);
|
||||
} else if (eventTitle) {
|
||||
// 如果没有交易日期,仍然搜索但不带日期参数
|
||||
searchConcepts(eventTitle, null);
|
||||
}
|
||||
}, [eventTitle, eventTime]);
|
||||
}, [eventTitle, effectiveTradingDate]);
|
||||
|
||||
const handleViewDetails = (concept) => {
|
||||
setSelectedConcept(concept);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
// 合并加载状态
|
||||
const isLoading = externalLoading || loading;
|
||||
const displayError = externalError || error;
|
||||
|
||||
// 加载状态
|
||||
if (isLoading) {
|
||||
if (loading || externalLoading) {
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
@@ -468,11 +256,11 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (displayError) {
|
||||
if (error || externalError) {
|
||||
return (
|
||||
<Alert status="error" borderRadius="lg">
|
||||
<AlertIcon />
|
||||
加载相关概念失败: {displayError}
|
||||
加载相关概念失败: {error || externalError}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -481,9 +269,7 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
if (!concepts || concepts.length === 0) {
|
||||
return (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="gray.500" mb={4}>
|
||||
{eventTitle ? '未找到相关概念' : '暂无相关概念数据'}
|
||||
</Text>
|
||||
<Text color="gray.500" mb={4}>暂无相关概念数据</Text>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
@@ -505,9 +291,9 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
<FaCalendarAlt color={textColor} />
|
||||
<Text fontSize="sm" color={textColor}>
|
||||
涨跌幅数据日期:{effectiveTradingDate}
|
||||
{eventTime && effectiveTradingDate !== moment(eventTime).format('YYYY-MM-DD') && (
|
||||
{eventTime && effectiveTradingDate !== eventTime.split(' ')[0] && (
|
||||
<Text as="span" ml={2} fontSize="xs">
|
||||
(事件发生于 {typeof eventTime === 'object' ? moment(eventTime).format('YYYY-MM-DD HH:mm') : eventTime},显示下一交易日数据)
|
||||
(事件发生于 {eventTime},显示下一交易日数据)
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
@@ -517,9 +303,9 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
|
||||
{/* 概念卡片网格 */}
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{concepts.map((concept, index) => (
|
||||
{concepts.map((concept) => (
|
||||
<ConceptCard
|
||||
key={concept.concept_id || index}
|
||||
key={concept.concept_id}
|
||||
concept={concept}
|
||||
tradingDate={effectiveTradingDate}
|
||||
onViewDetails={handleViewDetails}
|
||||
@@ -555,183 +341,73 @@ const RelatedConcepts = ({ eventTitle, eventTime, eventId, loading: externalLoad
|
||||
</VStack>
|
||||
</Center>
|
||||
|
||||
{/* 增强版概念详情模态框 */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
|
||||
{/* 概念详情模态框 */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxH="90vh">
|
||||
<ModalHeader borderBottomWidth={1}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<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>
|
||||
<Text>{selectedConcept?.concept}</Text>
|
||||
{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>
|
||||
<Badge colorScheme={selectedConcept.price_info.avg_change_pct > 0 ? 'red' : 'green'}>
|
||||
{selectedConcept.price_info.avg_change_pct > 0 ? '+' : ''}
|
||||
{selectedConcept.price_info.avg_change_pct?.toFixed(2)}%
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody pb={6} overflowY="auto">
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 概念描述 - 完整版 */}
|
||||
<ModalBody pb={6}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* 显示交易日期 */}
|
||||
{effectiveTradingDate && (
|
||||
<HStack>
|
||||
<FaCalendarAlt />
|
||||
<Text fontSize="sm">
|
||||
数据日期:{effectiveTradingDate}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 概念描述 */}
|
||||
{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>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
概念描述:
|
||||
</Text>
|
||||
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.400')}>
|
||||
{selectedConcept.description}
|
||||
</Text>
|
||||
</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>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
相关股票 ({selectedConcept.stock_count}只):
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing={2}>
|
||||
{selectedConcept.stocks.slice(0, 6).map((stock, index) => (
|
||||
<Badge key={index} p={2} variant="outline">
|
||||
{stock.stock_name} ({stock.stock_code})
|
||||
</Badge>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</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>
|
||||
{/* 查看详情按钮 */}
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={() => {
|
||||
window.open(`https://valuefrontier.cn/htmls/${encodeURIComponent(selectedConcept.concept)}.html`, '_blank');
|
||||
}}
|
||||
leftIcon={<FaExternalLinkAlt />}
|
||||
>
|
||||
查看概念详情页
|
||||
</Button>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
FaSearch
|
||||
} from 'react-icons/fa';
|
||||
import * as echarts from 'echarts';
|
||||
import StockChartModal from '../../../components/StockChart/StockChartModal';
|
||||
|
||||
import { eventService, stockService } from '../../../services/eventService';
|
||||
|
||||
@@ -71,12 +70,22 @@ const RelatedStocks = ({
|
||||
const [sortOrder, setSortOrder] = useState('desc');
|
||||
|
||||
// 模态框状态
|
||||
const {
|
||||
isOpen: isAddModalOpen,
|
||||
onOpen: onAddModalOpen,
|
||||
onClose: onAddModalClose
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isChartModalOpen,
|
||||
onOpen: onChartModalOpen,
|
||||
onClose: onChartModalClose
|
||||
} = useDisclosure();
|
||||
|
||||
// 添加股票表单状态
|
||||
const [addStockForm, setAddStockForm] = useState({
|
||||
stock_code: '',
|
||||
relation_desc: ''
|
||||
});
|
||||
|
||||
// 主题和工具
|
||||
const toast = useToast();
|
||||
@@ -132,6 +141,41 @@ const RelatedStocks = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddStock = async () => {
|
||||
if (!addStockForm.stock_code.trim() || !addStockForm.relation_desc.trim()) {
|
||||
toast({
|
||||
title: '请填写完整信息',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await eventService.addRelatedStock(eventId, addStockForm);
|
||||
|
||||
toast({
|
||||
title: '添加成功',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
setAddStockForm({ stock_code: '', relation_desc: '' });
|
||||
onAddModalClose();
|
||||
onStockAdded();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: '添加失败',
|
||||
description: err.message,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteStock = async (stockId, stockCode) => {
|
||||
if (!window.confirm(`确定要删除股票 ${stockCode} 吗?`)) {
|
||||
@@ -288,6 +332,14 @@ const RelatedStocks = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
leftIcon={<FaPlus />}
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
onClick={onAddModalOpen}
|
||||
>
|
||||
添加股票
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
@@ -329,6 +381,7 @@ const RelatedStocks = ({
|
||||
}}>
|
||||
涨跌幅 {sortField === 'change' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</Th>
|
||||
<Th textAlign="center">分时图</Th>
|
||||
<Th textAlign="center" cursor="pointer" onClick={() => {
|
||||
if (sortField === 'correlation') {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
@@ -414,6 +467,16 @@ const RelatedStocks = ({
|
||||
</Badge>
|
||||
</Td>
|
||||
|
||||
{/* 分时图 */}
|
||||
<Td>
|
||||
<Box width="120px" height="40px">
|
||||
<MiniChart
|
||||
stockCode={stock.stock_code}
|
||||
eventTime={eventTime}
|
||||
onChartClick={() => handleShowChart(stock)}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
|
||||
{/* 相关度 */}
|
||||
<Td textAlign="center">
|
||||
@@ -438,20 +501,6 @@ const RelatedStocks = ({
|
||||
{/* 操作 */}
|
||||
<Td>
|
||||
<HStack spacing={1}>
|
||||
<Tooltip label="股票详情">
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
const stockCode = stock.stock_code.split('.')[0];
|
||||
window.open(`https://valuefrontier.cn/company?scode=${stockCode}`, '_blank');
|
||||
}}
|
||||
>
|
||||
股票详情
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="查看K线图">
|
||||
<IconButton
|
||||
icon={<FaChartLine />}
|
||||
@@ -482,6 +531,14 @@ const RelatedStocks = ({
|
||||
</TableContainer>
|
||||
</Box>
|
||||
|
||||
{/* 添加股票模态框 */}
|
||||
<AddStockModal
|
||||
isOpen={isAddModalOpen}
|
||||
onClose={onAddModalClose}
|
||||
formData={addStockForm}
|
||||
setFormData={setAddStockForm}
|
||||
onSubmit={handleAddStock}
|
||||
/>
|
||||
|
||||
{/* 股票图表模态框 */}
|
||||
<StockChartModal
|
||||
@@ -496,8 +553,401 @@ const RelatedStocks = ({
|
||||
|
||||
// ==================== 子组件 ====================
|
||||
|
||||
// 迷你分时图组件
|
||||
const MiniChart = ({ stockCode, eventTime, onChartClick }) => {
|
||||
const chartRef = useRef(null);
|
||||
const chartInstanceRef = useRef(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (chartRef.current && stockCode) {
|
||||
loadChartData();
|
||||
}
|
||||
return () => {
|
||||
if (chartInstanceRef.current) {
|
||||
chartInstanceRef.current.dispose();
|
||||
chartInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [stockCode, eventTime]);
|
||||
|
||||
// 现在使用统一的StockChartModal组件,无需重复代码
|
||||
const loadChartData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await stockService.getKlineData(stockCode, 'timeline', eventTime);
|
||||
|
||||
if (!response.data || response.data.length === 0) {
|
||||
setError('无数据');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
if (!chartInstanceRef.current && chartRef.current) {
|
||||
chartInstanceRef.current = echarts.init(chartRef.current);
|
||||
}
|
||||
|
||||
const option = generateMiniChartOption(response.data);
|
||||
chartInstanceRef.current.setOption(option, true);
|
||||
} catch (err) {
|
||||
console.error('加载迷你图表失败:', err);
|
||||
setError('加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateMiniChartOption = (data) => {
|
||||
const prices = data.map(item => item.close);
|
||||
const times = data.map(item => item.time);
|
||||
|
||||
// 计算最高最低价格
|
||||
const minPrice = Math.min(...prices);
|
||||
const maxPrice = Math.max(...prices);
|
||||
|
||||
// 判断是上涨还是下跌
|
||||
const isUp = prices[prices.length - 1] >= prices[0];
|
||||
const lineColor = isUp ? '#ef5350' : '#26a69a';
|
||||
|
||||
return {
|
||||
grid: {
|
||||
left: 2,
|
||||
right: 2,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: times,
|
||||
show: false,
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
min: minPrice * 0.995,
|
||||
max: maxPrice * 1.005
|
||||
},
|
||||
series: [{
|
||||
data: prices,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: lineColor,
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: lineColor === '#ef5350' ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: lineColor === '#ef5350' ? 'rgba(239, 83, 80, 0.05)' : 'rgba(38, 166, 154, 0.05)'
|
||||
}
|
||||
])
|
||||
},
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
label: { show: false },
|
||||
lineStyle: {
|
||||
color: '#aaa',
|
||||
type: 'dashed',
|
||||
width: 1
|
||||
},
|
||||
data: [{
|
||||
yAxis: prices[0] // 参考价
|
||||
}]
|
||||
}
|
||||
}],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
if (!params || params.length === 0) return '';
|
||||
const price = params[0].value.toFixed(2);
|
||||
const time = params[0].axisValue;
|
||||
const percentChange = ((price - prices[0]) / prices[0] * 100).toFixed(2);
|
||||
const sign = percentChange >= 0 ? '+' : '';
|
||||
return `${time}<br/>价格: ${price}<br/>变动: ${sign}${percentChange}%`;
|
||||
},
|
||||
position: function (pos, params, el, elRect, size) {
|
||||
const obj = { top: 10 };
|
||||
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 30;
|
||||
return obj;
|
||||
}
|
||||
},
|
||||
animation: false
|
||||
};
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Flex align="center" justify="center" h="100%" w="100%">
|
||||
<Text fontSize="xs" color="gray.500">加载中...</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Flex align="center" justify="center" h="100%" w="100%">
|
||||
<Text fontSize="xs" color="gray.500">{error}</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={chartRef}
|
||||
w="100%"
|
||||
h="100%"
|
||||
cursor="pointer"
|
||||
onClick={onChartClick}
|
||||
_hover={{ opacity: 0.8 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 添加股票模态框组件
|
||||
const AddStockModal = ({ isOpen, onClose, formData, setFormData, onSubmit }) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>添加相关股票</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody>
|
||||
<VStack spacing={4}>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>股票代码</FormLabel>
|
||||
<Input
|
||||
placeholder="请输入股票代码,如:000001.SZ"
|
||||
value={formData.stock_code}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
stock_code: e.target.value.toUpperCase()
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl isRequired>
|
||||
<FormLabel>关联描述</FormLabel>
|
||||
<Textarea
|
||||
placeholder="请描述该股票与事件的关联原因..."
|
||||
value={formData.relation_desc}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
relation_desc: e.target.value
|
||||
}))}
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button colorScheme="blue" onClick={onSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// 股票图表模态框组件
|
||||
const StockChartModal = ({ isOpen, onClose, stock, eventTime }) => {
|
||||
const chartRef = useRef(null);
|
||||
const chartInstanceRef = useRef(null);
|
||||
const [chartType, setChartType] = useState('timeline');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [chartData, setChartData] = useState(null);
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && chartRef.current) {
|
||||
loadChartData(chartType);
|
||||
}
|
||||
return () => {
|
||||
if (chartInstanceRef.current) {
|
||||
window.removeEventListener('resize', chartInstanceRef.current.resizeHandler);
|
||||
chartInstanceRef.current.dispose();
|
||||
chartInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && chartRef.current) {
|
||||
loadChartData(chartType);
|
||||
}
|
||||
}, [chartType, stock, eventTime]);
|
||||
|
||||
const loadChartData = async (type) => {
|
||||
if (!stock || !chartRef.current) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
if (chartInstanceRef.current) {
|
||||
chartInstanceRef.current.showLoading();
|
||||
}
|
||||
const response = await stockService.getKlineData(stock.stock_code, type, eventTime);
|
||||
setChartData(response);
|
||||
if (!chartRef.current) return;
|
||||
if (!chartInstanceRef.current) {
|
||||
const chart = echarts.init(chartRef.current);
|
||||
chart.resizeHandler = () => chart.resize();
|
||||
window.addEventListener('resize', chart.resizeHandler);
|
||||
chartInstanceRef.current = chart;
|
||||
}
|
||||
chartInstanceRef.current.hideLoading();
|
||||
const option = generateChartOption(response, type);
|
||||
chartInstanceRef.current.setOption(option, true);
|
||||
} catch (err) {
|
||||
console.error('加载图表数据失败:', err);
|
||||
toast({ title: '加载图表数据失败', description: err.message, status: 'error', duration: 3000, isClosable: true, });
|
||||
if (chartInstanceRef.current) {
|
||||
chartInstanceRef.current.hideLoading();
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateChartOption = (data, type) => {
|
||||
if (!data || !data.data || data.data.length === 0) {
|
||||
return { title: { text: '暂无数据', left: 'center', top: 'center', textStyle: { color: '#999', fontSize: 16 } } };
|
||||
}
|
||||
const stockData = data.data;
|
||||
|
||||
if (type === 'timeline' || type === 'minute') {
|
||||
const times = stockData.map(item => item.time);
|
||||
const prices = stockData.map(item => item.close);
|
||||
const isUp = prices[prices.length - 1] >= prices[0];
|
||||
const lineColor = isUp ? '#ef5350' : '#26a69a';
|
||||
return {
|
||||
title: { text: `${data.name} (${data.code}) - ${type === 'timeline' ? '分时图' : '分钟线'}`, left: 'center', textStyle: { fontSize: 16, fontWeight: 'bold' } },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
if (!params || params.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const point = params[0];
|
||||
if (!point) return '';
|
||||
return `时间: ${point.axisValue}<br/>价格: ¥${point.value.toFixed(2)}`;
|
||||
}
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: times, boundaryGap: false },
|
||||
yAxis: { type: 'value', scale: true },
|
||||
series: [{ data: prices, type: 'line', smooth: true, symbol: 'none', lineStyle: { color: lineColor, width: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: isUp ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)' }, { offset: 1, color: isUp ? 'rgba(239, 83, 80, 0.1)' : 'rgba(38, 166, 154, 0.1)' }]) } }]
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'daily') {
|
||||
const dates = stockData.map(item => item.time);
|
||||
const klineData = stockData.map(item => [item.open, item.close, item.low, item.high]);
|
||||
const volumes = stockData.map(item => item.volume);
|
||||
return {
|
||||
title: { text: `${data.name} (${data.code}) - 日K线`, left: 'center', textStyle: { fontSize: 16, fontWeight: 'bold' } },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: function(params) {
|
||||
if (!params || !Array.isArray(params) || params.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const kline = params[0];
|
||||
const volume = params[1];
|
||||
|
||||
if (!kline || !kline.data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let tooltipHtml = `日期: ${kline.axisValue}<br/>
|
||||
开盘: ${kline.data[0]}<br/>
|
||||
收盘: ${kline.data[1]}<br/>
|
||||
最低: ${kline.data[2]}<br/>
|
||||
最高: ${kline.data[3]}`;
|
||||
|
||||
if (volume && typeof volume.value !== 'undefined') {
|
||||
tooltipHtml += `<br/>成交量: ${volume.value}`;
|
||||
}
|
||||
|
||||
return tooltipHtml;
|
||||
}
|
||||
},
|
||||
grid: [{ left: '10%', right: '10%', height: '60%' }, { left: '10%', right: '10%', top: '70%', height: '16%' }],
|
||||
xAxis: [{ type: 'category', data: dates, scale: true, boundaryGap: false, axisLine: { onZero: false }, splitLine: { show: false }, min: 'dataMin', max: 'dataMax' }, { type: 'category', gridIndex: 1, data: dates, scale: true, boundaryGap: false, axisLine: { onZero: false }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false }, min: 'dataMin', max: 'dataMax' }],
|
||||
yAxis: [{ scale: true, splitArea: { show: true } }, { scale: true, gridIndex: 1, splitNumber: 2, axisLabel: { show: false }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false } }],
|
||||
dataZoom: [{ type: 'inside', xAxisIndex: [0, 1], start: 80, end: 100 }, { show: true, xAxisIndex: [0, 1], type: 'slider', bottom: 10, start: 80, end: 100 }],
|
||||
series: [{ name: 'K线', type: 'candlestick', data: klineData, itemStyle: { color: '#ef5350', color0: '#26a69a', borderColor: '#ef5350', borderColor0: '#26a69a' } }, { name: '成交量', type: 'bar', xAxisIndex: 1, yAxisIndex: 1, data: volumes, itemStyle: { color: function(params) { const dataIndex = params.dataIndex; if (dataIndex === 0) return '#ef5350'; return stockData[dataIndex].close >= stockData[dataIndex - 1].close ? '#ef5350' : '#26a69a'; } } }]
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
if (!stock) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW="90vw" maxH="90vh">
|
||||
<ModalHeader>
|
||||
<VStack align="flex-start" spacing={2}>
|
||||
<HStack>
|
||||
<Text fontSize="lg" fontWeight="bold">{stock.stock_code} - 股票详情</Text>
|
||||
{chartData && (<Badge colorScheme="blue">{chartData.trade_date}</Badge>)}
|
||||
</HStack>
|
||||
<ButtonGroup size="sm">
|
||||
<Button variant={chartType === 'timeline' ? 'solid' : 'outline'} onClick={() => setChartType('timeline')} colorScheme="blue">分时图</Button>
|
||||
<Button variant={chartType === 'minute' ? 'solid' : 'outline'} onClick={() => setChartType('minute')} colorScheme="blue">分钟线</Button>
|
||||
<Button variant={chartType === 'daily' ? 'solid' : 'outline'} onClick={() => setChartType('daily')} colorScheme="blue">日K线</Button>
|
||||
</ButtonGroup>
|
||||
</VStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody p={0}>
|
||||
<Box h="500px" w="100%" position="relative">
|
||||
{loading && (
|
||||
<Flex position="absolute" top="0" left="0" right="0" bottom="0" bg="rgba(255, 255, 255, 0.7)" zIndex="10" alignItems="center" justifyContent="center" >
|
||||
<VStack spacing={4}>
|
||||
<CircularProgress isIndeterminate color="blue.300" />
|
||||
<Text>加载图表数据...</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
)}
|
||||
<div ref={chartRef} style={{ height: '100%', width: '100%', minHeight: '500px' }}/>
|
||||
</Box>
|
||||
{stock?.relation_desc && (
|
||||
<Box p={4} borderTop="1px solid" borderTopColor="gray.200">
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>关联描述:</Text>
|
||||
<Text fontSize="sm" color="gray.600">{stock.relation_desc}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{process.env.NODE_ENV === 'development' && chartData && (
|
||||
<Box p={4} bg="gray.50" fontSize="xs" color="gray.600">
|
||||
<Text fontWeight="bold">调试信息:</Text>
|
||||
<Text>数据条数: {chartData.data ? chartData.data.length : 0}</Text>
|
||||
<Text>交易日期: {chartData.trade_date}</Text>
|
||||
<Text>图表类型: {chartData.type}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelatedStocks;
|
||||
1
src/views/EventDetail/components/SectorDetailTable.js
Normal file
1
src/views/EventDetail/components/SectorDetailTable.js
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
43
src/views/EventDetail/components/SectorPieChart.js
Normal file
43
src/views/EventDetail/components/SectorPieChart.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
const SectorPieChart = ({ sectorData }) => {
|
||||
const data = Object.entries(sectorData || {}).map(([name, d]) => ({
|
||||
name,
|
||||
value: d.count,
|
||||
}));
|
||||
const option = {
|
||||
title: {
|
||||
text: '涨停股票分布',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 16, fontWeight: 'bold' },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
top: 'middle',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '板块分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['60%', '50%'],
|
||||
data,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
return <ReactECharts option={option} style={{ height: 400 }} />;
|
||||
};
|
||||
export default SectorPieChart;
|
||||
24
src/views/EventDetail/components/StatisticsCards.js
Normal file
24
src/views/EventDetail/components/StatisticsCards.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Grid, Stat, StatLabel, StatNumber, StatHelpText, StatArrow, Box } from '@chakra-ui/react';
|
||||
import { FiTarget, FiBarChart2, FiTrendingUp, FiZap } from 'react-icons/fi';
|
||||
|
||||
const StatCard = ({ icon, label, value, color }) => (
|
||||
<Box p={4} bg="white" borderRadius="lg" boxShadow="md" display="flex" alignItems="center">
|
||||
<Box as={icon} boxSize={6} color={color} mr={3} />
|
||||
<Box>
|
||||
<StatLabel>{label}</StatLabel>
|
||||
<StatNumber>{value}</StatNumber>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const StatisticsCards = ({ data }) => (
|
||||
<Grid templateColumns="repeat(4, 1fr)" gap={6} mb={6}>
|
||||
<StatCard icon={FiTarget} label="涨停股票总数" value={data.total_stocks} color="blue.400" />
|
||||
<StatCard icon={FiBarChart2} label="涉及板块数" value={data.sector_count} color="green.400" />
|
||||
<StatCard icon={FiTrendingUp} label="平均涨幅" value={data.avg_change + '%'} color="orange.400" />
|
||||
<StatCard icon={FiZap} label="最大涨幅" value={data.max_change + '%'} color="red.400" />
|
||||
</Grid>
|
||||
);
|
||||
|
||||
export default StatisticsCards;
|
||||
File diff suppressed because it is too large
Load Diff
55
src/views/EventDetail/components/WordCloudChart.js
Normal file
55
src/views/EventDetail/components/WordCloudChart.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
const WordCloudChart = ({ sectorData }) => {
|
||||
const data = Object.entries(sectorData || {}).map(([name, d]) => ({
|
||||
name,
|
||||
value: d.count,
|
||||
}));
|
||||
const option = {
|
||||
title: {
|
||||
text: '热点词汇',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 16, fontWeight: 'bold' },
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'wordCloud',
|
||||
shape: 'circle',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
width: '90%',
|
||||
height: '90%',
|
||||
sizeRange: [16, 60],
|
||||
rotationRange: [-30, 30],
|
||||
gridSize: 12,
|
||||
drawOutOfBound: false,
|
||||
layoutAnimation: true,
|
||||
textStyle: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 'bold',
|
||||
color: () => {
|
||||
const colors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD',
|
||||
'#D4A5A5', '#9B6B6B', '#E9967A', '#B19CD9', '#87CEEB'
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
textStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#333',
|
||||
},
|
||||
},
|
||||
data,
|
||||
},
|
||||
],
|
||||
};
|
||||
return <ReactECharts option={option} style={{ height: 400 }} />;
|
||||
};
|
||||
export default WordCloudChart;
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user