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 (
{/* 添加导航栏 */}
{/* 添加容器和边距 */}
{/* 标题和日期选择 */}
涨停分析
分析每日涨停股票的数据分布和热点词汇
}
colorScheme="blue"
variant="outline"
onClick={() => handleExport('excel')}
size="md"
>
导出
{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;