import React, { useState, useEffect } from 'react'; import { Box, VStack, HStack, Text, Button, Select, Grid, GridItem, Spinner, Center, Alert, AlertIcon, AlertTitle, AlertDescription, useToast, useColorModeValue, Badge, Stat, StatLabel, StatNumber, StatHelpText, StatArrow, Divider, Flex, Icon, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, useDisclosure, Tag, Container, } from '@chakra-ui/react'; import { FiTrendingUp, FiBarChart2, FiPieChart, FiActivity, FiDownload, FiCalendar, FiTarget, FiZap, } from 'react-icons/fi'; import 'echarts-wordcloud'; // 导入现有的卡片组件 import Card from '../../../components/Card/Card'; import CardBody from '../../../components/Card/CardBody'; import CardHeader from '../../../components/Card/CardHeader'; // 导入图表组件 import ReactECharts from 'echarts-for-react'; // 导入导航栏组件 import HomeNavbar from '../../../components/Navbars/HomeNavbar'; // 板块关联TOP10数据计算 function getSectorRelationTop10(sectorData) { // 股票代码 -> 所属板块集合 const stockSectorMap = new Map(); Object.entries(sectorData).forEach(([sector, sectorInfo]) => { (sectorInfo.stocks || []).forEach(stock => { const stockKey = stock.scode; if (!stockSectorMap.has(stockKey)) { stockSectorMap.set(stockKey, new Set()); } (stock.core_sectors || []).forEach(s => stockSectorMap.get(stockKey).add(s)); }); }); // 统计板块对 const relations = new Map(); stockSectorMap.forEach(sectors => { const arr = Array.from(sectors); for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { const pair = [arr[i], arr[j]].sort().join(' - '); relations.set(pair, (relations.get(pair) || 0) + 1); } } }); // Top10 const sorted = Array.from(relations.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10); return { labels: sorted.map(([pair]) => pair), counts: sorted.map(([_, count]) => count) }; } // 板块关联关系图数据计算 function getSectorRelationGraph(sectorData) { // 节点 const nodes = []; const nodeMap = new Map(); const links = []; const relationMap = new Map(); const stockInSectorMap = new Map(); let idx = 0; Object.entries(sectorData).forEach(([sector, data]) => { if (sector !== '其他' && sector !== '公告') { nodes.push({ name: sector, value: data.count, symbolSize: Math.sqrt(data.count) * 8, category: idx % 10, label: { show: true, formatter: `{b}\n(${data.count}只)` } }); nodeMap.set(sector, nodes.length - 1); idx++; (data.stocks || []).forEach(stock => { const stockKey = stock.scode; if (!stockInSectorMap.has(stockKey)) { stockInSectorMap.set(stockKey, new Set()); } stockInSectorMap.get(stockKey).add(sector); }); } }); // 统计边 stockInSectorMap.forEach(sectors => { const arr = Array.from(sectors); for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { const key = [arr[i], arr[j]].sort().join('-'); relationMap.set(key, (relationMap.get(key) || 0) + 1); } } }); relationMap.forEach((value, key) => { const [source, target] = key.split('-'); if (nodeMap.has(source) && nodeMap.has(target) && value >= 2) { links.push({ source: nodeMap.get(source), target: nodeMap.get(target), value, lineStyle: { width: Math.log(value) * 2, opacity: 0.6, curveness: 0.3 } }); } }); return { nodes, links }; } // 只取前10大板块,其余合并为“其他” function getTop10Sectors(chartData) { if (!chartData || !chartData.labels || !chartData.counts) return {labels: [], counts: []}; const zipped = chartData.labels.map((label, i) => ({label, count: chartData.counts[i]})); const sorted = zipped.sort((a, b) => b.count - a.count); const top10 = sorted.slice(0, 10); const rest = sorted.slice(10); let otherCount = 0; rest.forEach(item => { otherCount += item.count; }); const labels = top10.map(item => item.label); const counts = top10.map(item => item.count); if (otherCount > 0) { labels.push('其他'); counts.push(otherCount); } return {labels, counts}; } const isProduction = process.env.NODE_ENV === 'production'; const API_BASE = isProduction ? "" : process.env.REACT_APP_API_URL; // const API_BASE = 'http://49.232.185.254:5001'; // 改回5001端口,确保和后端一致 // 涨停分析服务 const limitAnalyseService = { async getAvailableDates() { try { const response = await fetch(`${API_BASE}/api/limit-analyse/available-dates`); const text = await response.text(); try { const data = JSON.parse(text); return data.data || []; } catch (e) { throw new Error('接口返回内容不是有效的 JSON,实际返回:' + text.slice(0, 100)); } } catch (error) { console.error('Error fetching available dates:', error); throw error; } }, async getAnalysisData(date) { try { const response = await fetch(`${API_BASE}/api/limit-analyse/data?date=${date}`); const data = await response.json(); return data; // 修正:直接返回整个对象 } catch (error) { console.error('Error fetching analysis data:', error); throw error; } }, async getSectorData(date) { try { const response = await fetch(`${API_BASE}/api/limit-analyse/sector-data?date=${date}`); const data = await response.json(); return data.data || []; } catch (error) { console.error('Error fetching sector data:', error); throw error; } }, async getWordCloudData(date) { try { const response = await fetch(`${API_BASE}/api/limit-analyse/word-cloud?date=${date}`); const data = await response.json(); return data.data || []; } catch (error) { console.error('Error fetching word cloud data:', error); throw error; } }, async exportData(date, exportType = 'excel') { try { const response = await fetch(`${API_BASE}/api/limit-analyse/export-data`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ date, type: exportType }), }); const data = await response.json(); return data.data; } catch (error) { console.error('Error exporting data:', error); throw error; } }, }; const LimitAnalyse = () => { const [selectedDate, setSelectedDate] = useState(''); const [availableDates, setAvailableDates] = useState([]); const [analysisData, setAnalysisData] = useState(null); const [sectorData, setSectorData] = useState({}); // 改为对象 const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const toast = useToast(); const bgColor = useColorModeValue('gray.50', 'gray.900'); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); // 加载可用日期 useEffect(() => { const loadAvailableDates = async () => { try { setIsLoading(true); const dates = await limitAnalyseService.getAvailableDates(); setAvailableDates(dates); if (dates.length > 0 && !selectedDate) { setSelectedDate(dates[0]); } } catch (error) { setError('加载日期列表失败'); toast({ title: '错误', description: '加载日期列表失败', status: 'error', duration: 5000, isClosable: true, }); } finally { setIsLoading(false); } }; loadAvailableDates(); }, []); // 加载分析数据 useEffect(() => { const loadAnalysisData = async () => { if (!selectedDate) return; try { setIsLoading(true); setError(null); const analysis = await limitAnalyseService.getAnalysisData(selectedDate); setAnalysisData(analysis); setSectorData(analysis.sector_data || {}); // sector_data为对象 // 不再用 wordCloudData } catch (error) { setError('加载分析数据失败'); toast({ title: '错误', description: '加载分析数据失败', status: 'error', duration: 5000, isClosable: true, }); } finally { setIsLoading(false); } }; loadAnalysisData(); }, [selectedDate]); // 由 sectorData 生成词云数据 const wordCloudData = analysisData?.word_freq_data || []; // 统计卡片数据 const totalStocks = Object.values(sectorData || {}).reduce((sum, data) => sum + data.count, 0); const sectorCount = Object.keys(sectorData || {}).length; // 平均涨幅、最大涨幅 const avgChange = analysisData?.avg_change || 0; const maxChange = analysisData?.max_change || 0; // 饼图数据 const chartOption = { title: { text: '涨停股票分布', left: 'center', textStyle: { fontSize: 16, fontWeight: 'bold', }, }, tooltip: { trigger: 'item', formatter: '{a}
{b}: {c} ({d}%)', }, legend: { orient: 'vertical', left: 'left', top: 'middle', }, series: [ { name: '板块分布', type: 'pie', radius: ['40%', '70%'], center: ['60%', '50%'], data: Object.entries(sectorData || {}).map(([name, data]) => ({ name, value: data.count, })), emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)', }, }, }, ], }; const wordCloudOption = { title: { text: '热点词汇', left: 'center', textStyle: { fontSize: 16, fontWeight: 'bold', }, }, series: [ { type: 'wordCloud', shape: 'circle', left: 'center', top: 'center', width: '70%', height: '80%', right: null, bottom: null, sizeRange: [12, 60], rotationRange: [-90, 90], rotationStep: 45, gridSize: 8, drawOutOfBound: false, textStyle: { fontFamily: 'sans-serif', fontWeight: 'bold', color: function () { return 'rgb(' + [ Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160), ].join(',') + ')'; }, }, emphasis: { focus: 'self', textStyle: { shadowBlur: 10, shadowColor: '#333', }, }, data: wordCloudData.map((word) => ({ name: word.name, value: Number(word.value) || 0, })), }, ], }; // 统计卡片组件 const StatCard = ({ icon, label, value, color, change }) => { const iconColor = useColorModeValue(`${color}.500`, `${color}.300`); const bgColor = useColorModeValue(`${color}.50`, `${color}.900`); return ( {label} {value} {change && ( 0 ? 'increase' : 'decrease'} /> 0 ? 'green.500' : 'red.500'}> {Math.abs(change)}% )} ); }; if (error) { return ( 错误 {error} ); } return ( {/* 添加导航栏 */} {/* 添加容器和边距 */} {/* 标题和日期选择 */} 涨停分析 分析每日涨停股票的数据分布和热点词汇 {isLoading ? (
加载数据中...
) : ( <> {/* 板块分布图表 */} 板块分布 {b}: {c} ({d}%)' }, legend: { orient: 'vertical', left: 'left', top: 'middle' }, series: [{ name: '板块分布', type: 'pie', radius: ['40%', '70%'], center: ['60%', '50%'], data: getTop10Sectors(analysisData?.chart_data).labels.map((name, i) => ({ name, value: getTop10Sectors(analysisData?.chart_data).counts[i] })), emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }, }], }} style={{ height: '300px' }} /> {/* 板块股票数量柱状图 */} 板块股票数量 {/* 板块关联TOP10横向条形图 */} 板块关联TOP10 10) return value.replace(/(.{10})/g, '$1\n'); return value; } } }, series: [{ data: getSectorRelationTop10(sectorData).counts, type: 'bar', itemStyle: { color: '#45b7d1' }, label: { show: true, position: 'right' } }], grid: { left: 120, right: 20, top: 40, bottom: 40 } }} style={{ height: '300px' }} /> {/* 板块关联关系图+词云 */} 板块关联关系图 热点词云 {/* 数据统计 */} 数据统计 {/* 板块手风琴 */} {Object.keys(sectorData).length > 0 && ( 板块详细数据 {Object.entries(sectorData).map(([sector, data], idx) => (

{sector} {data.count}

{(data.stocks || []).map((stock, sidx) => ( ))}
))}
)}
)}
); }; // 新增股票卡片组件和弹窗 const StockCard = ({ stock, idx }) => { const { isOpen, onOpen, onClose } = useDisclosure(); return ( {stock.sname} ({stock.scode}) {stock.continuous_days && ( {stock.continuous_days} )} 涨停时间:{stock.formatted_time} {stock.brief} {stock.summary && ( )} {(stock.core_sectors || []).map((sector) => ( {sector} ))} {/* 详细摘要弹窗美化 */} {stock.summary && ( {stock.sname} ({stock.scode}) - 详细分析 (\s*)/g, '\n').replace(/\n{2,}/g, '\n').replace(/\n/g, '
') }} />
)}
); }; export default LimitAnalyse;