import React, { useMemo, useState, useEffect } from 'react'; import { Box, Card, CardHeader, CardBody, Center, VStack, HStack, Heading, Text, Tabs, TabList, TabPanels, Tab, TabPanel, Stat, StatLabel, StatNumber, StatHelpText, SimpleGrid, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, Button, Badge, Divider, Avatar, Tag, TagLabel, Wrap, WrapItem, useColorModeValue, } from '@chakra-ui/react'; import { getFormattedTextProps } from '../../../utils/textUtils'; import { ExternalLinkIcon } from '@chakra-ui/icons'; import RiskDisclaimer from '../../../components/RiskDisclaimer'; import './WordCloud.css'; import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer, Treemap, Area, AreaChart, } from 'recharts'; import ReactWordcloud from 'react-wordcloud'; // 颜色配置 const CHART_COLORS = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD', '#D4A5A5', '#9B6B6B', '#E9967A', '#B19CD9', '#87CEEB' ]; // 词云图组件 const WordCloud = ({ data }) => { if (!data || data.length === 0) { return (
暂无词云数据
); } const words = data.slice(0, 100).map(item => ({ text: item.name || item.text, value: item.value || item.count || 1 })); const options = { rotations: 2, rotationAngles: [-90, 0], fontFamily: 'Microsoft YaHei, sans-serif', fontSizes: [16, 80], fontWeight: 'bold', padding: 3, scale: 'sqrt', }; const callbacks = { getWordColor: () => { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD']; return colors[Math.floor(Math.random() * colors.length)]; } }; return ( ); }; // 板块热力图组件 const SectorHeatMap = ({ data }) => { if (!data) return null; const sectors = Object.entries(data) .filter(([name]) => name !== '其他') .map(([name, info]) => ({ name, count: info.count, stocks: info.stocks || [] })) .sort((a, b) => b.count - a.count) .slice(0, 20); const maxCount = Math.max(...sectors.map(s => s.count), 1); const getColor = (count) => { const intensity = count / maxCount; if (intensity < 0.1) return '#E6F3FF'; if (intensity < 0.2) return '#CCE7FF'; if (intensity < 0.3) return '#99CFFF'; if (intensity < 0.4) return '#66B7FF'; if (intensity < 0.5) return '#339FFF'; if (intensity < 0.6) return '#0087FF'; if (intensity < 0.7) return '#0069CC'; if (intensity < 0.8) return '#004C99'; if (intensity < 0.9) return '#003066'; return '#001433'; }; const cols = 5; const rows = Math.ceil(sectors.length / cols); const cellWidth = 150; const cellHeight = 100; const padding = 10; return ( {sectors.map((sector, index) => { const row = Math.floor(index / cols); const col = index % cols; const x = col * (cellWidth + padding) + padding; const y = row * (cellHeight + padding) + padding; const color = getColor(sector.count); return ( { e.target.style.opacity = '0.8'; e.target.style.transform = 'scale(1.05)'; }} onMouseLeave={(e) => { e.target.style.opacity = '1'; e.target.style.transform = 'scale(1)'; }} > {`${sector.name}: ${sector.count}只涨停`} 0.5 ? 'white' : '#2D3748'} fontSize="14" fontWeight="bold" > {sector.name.length > 8 ? sector.name.slice(0, 8) + '...' : sector.name} 0.5 ? 'white' : '#2D3748'} fontSize="24" fontWeight="bold" > {sector.count} 0.5 ? 'rgba(255,255,255,0.8)' : '#718096'} fontSize="12" > 只涨停 ); })} {[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1].map((intensity) => ( ))} ); }; // 板块关联图组件 const SectorRelationMap = ({ data }) => { if (!data || !data.length) return null; const sectors = [...new Set(data.flatMap(item => item.core_sectors || []))]; const sectorPairs = {}; data.forEach(stock => { const stockSectors = stock.core_sectors || []; for (let i = 0; i < stockSectors.length; i++) { for (let j = i + 1; j < stockSectors.length; j++) { const key = [stockSectors[i], stockSectors[j]].sort().join('-'); sectorPairs[key] = (sectorPairs[key] || 0) + 1; } } }); const topSectors = sectors .map(s => ({ name: s, count: data.filter(d => (d.core_sectors || []).includes(s)).length })) .sort((a, b) => b.count - a.count) .slice(0, 15) .map(s => s.name); const matrix = []; const maxValue = Math.max(...Object.values(sectorPairs), 1); topSectors.forEach((sector1, i) => { topSectors.forEach((sector2, j) => { if (i <= j) { const key = [sector1, sector2].sort().join('-'); const value = i === j ? data.filter(d => (d.core_sectors || []).includes(sector1)).length : sectorPairs[key] || 0; matrix.push({ x: j, y: i, value, sector1, sector2, intensity: value / maxValue }); } }); }); const getColor = (intensity) => { if (intensity === 0) return '#f7fafc'; if (intensity < 0.2) return '#bee3f8'; if (intensity < 0.4) return '#90cdf4'; if (intensity < 0.6) return '#63b3ed'; if (intensity < 0.8) return '#4299e1'; return '#3182ce'; }; const cellSize = 40; const fontSize = 10; return ( {topSectors.map((sector, i) => ( {sector} ))} {topSectors.map((sector, i) => ( {sector} ))} {matrix.map((cell, i) => ( {`${cell.sector1} - ${cell.sector2}: ${cell.value}`} {cell.value > 0 && ( 0.5 ? "white" : "#2d3748"} fontWeight="bold" > {cell.value} )} ))} 强度 {[0, 0.2, 0.4, 0.6, 0.8, 1].map((intensity, i) => ( {intensity === 0 ? '0' : `${(intensity * maxValue).toFixed(0)}`} ))} ); }; // 数据分析主组件 export const DataAnalysis = ({ dailyData, wordCloudData }) => { const cardBg = useColorModeValue('white', 'gray.800'); const pieData = useMemo(() => { if (!dailyData?.chart_data) return []; return dailyData.chart_data.labels.slice(0, 10).map((label, index) => ({ name: label, value: dailyData.chart_data.counts[index], })); }, [dailyData]); const timeDistributionData = useMemo(() => { if (!dailyData?.summary?.zt_time_distribution) return []; const dist = dailyData.summary.zt_time_distribution; return [ { name: '早盘(9:30-11:30)', value: dist.morning || 0, color: '#48BB78' }, { name: '午盘(11:30-13:00)', value: dist.midday || 0, color: '#ED8936' }, { name: '尾盘(13:00-15:00)', value: dist.afternoon || 0, color: '#4299E1' } ]; }, [dailyData]); const allStocks = useMemo(() => { if (!dailyData?.sector_data) return []; return Object.values(dailyData.sector_data).flatMap(sector => sector.stocks || []); }, [dailyData]); return ( 数据分析 词云图 板块分布 板块热力图 板块关联 时间分布 {wordCloudData && wordCloudData.length > 0 ? ( ) : dailyData?.word_freq_data && dailyData.word_freq_data.length > 0 ? ( ) : (
暂无词云数据
)}
`${name} ${(percent * 100).toFixed(0)}%`} outerRadius={120} fill="#8884d8" dataKey="value" animationBegin={0} animationDuration={800} > {pieData.map((entry, index) => ( ))} {dailyData?.sector_data ? ( ) : (
暂无数据
)}
{allStocks.length > 0 ? ( ) : (
暂无数据
)}
{timeDistributionData.map((entry, index) => ( ))} {timeDistributionData.map((item, index) => ( {item.name} {item.value} {((item.value / (dailyData?.total_stocks || 1)) * 100).toFixed(1)}% ))}
); }; // 股票详情弹窗组件 export const StockDetailModal = ({ isOpen, onClose, selectedStock }) => { const accentColor = useColorModeValue('blue.500', 'blue.300'); if (!selectedStock) return null; return ( {selectedStock.sname} {selectedStock.scode} 涨停信息 涨停时间: {selectedStock.formatted_time || selectedStock.zt_time} {selectedStock.continuous_days && ( {selectedStock.continuous_days} )} 涨停原因 {selectedStock.brief || '暂无涨停原因'} {selectedStock.summary && ( <> 详细分析 {getFormattedTextProps(selectedStock.summary).children} )} 所属板块 {selectedStock.core_sectors?.map((sector, i) => ( {sector} ))} {/* 风险提示 */} ); }; export default { DataAnalysis, StockDetailModal };