Files
vf_react/src/views/LimitAnalyse/index.js
2025-10-11 16:16:02 +08:00

515 lines
20 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
Box,
Container,
VStack,
HStack,
Heading,
Text,
Badge,
useToast,
Skeleton,
IconButton,
Flex,
useColorModeValue,
SimpleGrid,
Tooltip,
Card,
CardBody,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
Alert,
AlertIcon,
Link,
} from '@chakra-ui/react';
import {
RepeatIcon,
ChevronUpIcon,
} from '@chakra-ui/icons';
// 导入拆分的组件
// 注意:在实际使用中,这些组件应该被拆分到独立的文件中
// 这里为了演示,我们假设它们已经被正确导出
// API配置
const API_URL = process.env.NODE_ENV === 'production' ? '/report-api' : 'http://111.198.58.126:5001';
// 导入的组件(实际使用时应该从独立文件导入)
// 恢复使用本页自带的轻量日历
import EnhancedCalendar from './components/EnhancedCalendar';
import SectorDetails from './components/SectorDetails';
import { DataAnalysis, StockDetailModal } from './components/DataVisualizationComponents';
import { AdvancedSearch, SearchResultsModal } from './components/SearchComponents';
// 导入导航栏组件
import HomeNavbar from '../../components/Navbars/HomeNavbar';
// 导入高位股统计组件
import HighPositionStocks from './components/HighPositionStocks';
// 主组件
export default function LimitAnalyse() {
const [selectedDate, setSelectedDate] = useState(null);
const [dateStr, setDateStr] = useState('');
const [loading, setLoading] = useState(false);
const [dailyData, setDailyData] = useState(null);
const [availableDates, setAvailableDates] = useState([]);
const [wordCloudData, setWordCloudData] = useState([]);
const [searchResults, setSearchResults] = useState(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const toast = useToast();
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
const accentColor = useColorModeValue('blue.500', 'blue.300');
// 获取可用日期
useEffect(() => {
fetchAvailableDates();
}, []);
// 初始进入展示骨架屏,直到选中日期的数据加载完成
useEffect(() => {
setLoading(true);
}, []);
// 根据可用日期加载最近一个有数据的日期
useEffect(() => {
if (availableDates && availableDates.length > 0) {
// 选择日期字符串最大的那一天(格式为 YYYYMMDD
const latest = availableDates.reduce((max, cur) =>
(!max || (cur.date && cur.date > max)) ? cur.date : max
, null);
if (latest) {
setDateStr(latest);
const year = parseInt(latest.slice(0, 4), 10);
const month = parseInt(latest.slice(4, 6), 10) - 1;
const day = parseInt(latest.slice(6, 8), 10);
setSelectedDate(new Date(year, month, day));
fetchDailyAnalysis(latest);
}
} else {
// 如果暂无可用日期,回退到今日,避免页面长时间空白
const today = new Date();
const dateString = formatDateStr(today);
setDateStr(dateString);
setSelectedDate(today);
fetchDailyAnalysis(dateString);
}
}, [availableDates]);
// API调用函数
const fetchAvailableDates = async () => {
try {
const response = await fetch(`${API_URL}/api/v1/dates/available`);
const data = await response.json();
if (data.success) {
setAvailableDates(data.events);
}
} catch (error) {
console.error('Failed to fetch available dates:', error);
}
};
const fetchDailyAnalysis = async (date) => {
setLoading(true);
try {
const response = await fetch(`${API_URL}/api/v1/analysis/daily/${date}`);
const data = await response.json();
if (data.success) {
setDailyData(data.data);
// 获取词云数据
fetchWordCloudData(date);
toast({
title: '数据加载成功',
description: `${date} 的数据已加载`,
status: 'success',
duration: 2000,
isClosable: true,
});
}
} catch (error) {
console.error('Failed to fetch daily analysis:', error);
toast({
title: '网络错误',
description: '无法加载数据,请稍后重试',
status: 'error',
duration: 3000,
});
} finally {
setLoading(false);
}
};
const fetchWordCloudData = async (date) => {
try {
const response = await fetch(`${API_URL}/api/v1/analysis/wordcloud/${date}`);
const data = await response.json();
if (data.success) {
setWordCloudData(data.data);
}
} catch (error) {
console.error('Failed to fetch wordcloud data:', error);
}
};
// 格式化日期
const formatDateStr = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
};
// 处理日期选择
const handleDateChange = (date) => {
setSelectedDate(date);
const dateString = formatDateStr(date);
setDateStr(dateString);
fetchDailyAnalysis(dateString);
};
// 处理搜索
const handleSearch = async (searchParams) => {
setLoading(true);
try {
const response = await fetch(`${API_URL}/api/v1/stocks/search/hybrid`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(searchParams),
});
const data = await response.json();
if (data.success) {
setSearchResults(data.data);
setIsSearchOpen(true);
toast({
title: '搜索完成',
description: `找到 ${data.data.total} 只相关股票`,
status: 'success',
duration: 3000,
});
}
} catch (error) {
console.error('Search failed:', error);
toast({
title: '搜索失败',
description: '请稍后重试',
status: 'error',
duration: 3000,
});
} finally {
setLoading(false);
}
};
// 处理板块数据排序
const getSortedSectorData = () => {
if (!dailyData?.sector_data) return [];
const sectors = Object.entries(dailyData.sector_data);
const announcement = sectors.find(([name]) => name === '公告');
const others = sectors.filter(([name]) => name !== '公告' && name !== '其他');
const other = sectors.find(([name]) => name === '其他');
// 按数量排序
others.sort((a, b) => b[1].count - a[1].count);
// 组合:公告在最前,其他在最后
let result = [];
if (announcement) result.push(announcement);
result = result.concat(others);
if (other) result.push(other);
return result;
};
// 渲染统计卡片
const StatsCards = () => (
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4} mb={6}>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">今日涨停</StatLabel>
<StatNumber fontSize="2xl" color="red.500">
{dailyData?.total_stocks || 0}
</StatNumber>
<StatHelpText>
<StatArrow type="increase" />
较昨日 +23%
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">最热板块</StatLabel>
<StatNumber fontSize="xl" color={accentColor}>
{dailyData?.summary?.top_sector || '-'}
</StatNumber>
<StatHelpText>{dailyData?.summary?.top_sector_count || 0} </StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">公告涨停</StatLabel>
<StatNumber fontSize="2xl" color="orange.500">
{dailyData?.summary?.announcement_stocks || 0}
</StatNumber>
<StatHelpText>重大利好</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} borderRadius="lg" boxShadow="md">
<CardBody>
<Stat>
<StatLabel fontSize="sm">早盘强势</StatLabel>
<StatNumber fontSize="2xl" color="green.500">
{dailyData?.summary?.zt_time_distribution?.morning || 0}
</StatNumber>
<StatHelpText>开盘涨停</StatHelpText>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
);
const formatDisplayDate = (date) => {
if (!date) return '';
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}${month}${day}`;
};
const getSelectedDateCount = () => {
if (!selectedDate || !availableDates?.length) return null;
const date = formatDateStr(selectedDate);
const found = availableDates.find(d => d.date === date);
return found ? found.count : null;
};
return (
<Box minH="100vh" bg={bgColor}>
{/* 导航栏 */}
<HomeNavbar />
{/* 顶部Header */}
<Box bgGradient="linear(to-br, blue.500, purple.600)" color="white" py={8}>
<Container maxW="container.xl">
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6} alignItems="stretch">
{/* 左侧:标题置顶,注释与图例贴底 */}
<Flex direction="column" minH="420px" justify="space-between">
<VStack align="start" spacing={4}>
<HStack>
<Badge colorScheme="whiteAlpha" fontSize="sm" px={2}>
AI驱动
</Badge>
<Badge colorScheme="yellow" fontSize="sm" px={2}>
实时更新
</Badge>
</HStack>
<Heading
size="3xl"
fontWeight="extrabold"
letterSpacing="-0.5px"
lineHeight="shorter"
textShadow="0 6px 24px rgba(0,0,0,0.25)"
>
涨停板块分析平台
</Heading>
<Text fontSize="xl" opacity={0.98} fontWeight="semibold" textShadow="0 4px 16px rgba(0,0,0,0.2)">
以大模型辅助整理海量信息结合领域知识图谱与分析师复核呈现涨停板块关键线索
</Text>
</VStack>
<VStack align="stretch" spacing={3}>
<Alert
status="info"
borderRadius="xl"
bg="whiteAlpha.200"
color="whiteAlpha.900"
borderWidth="1px"
borderColor="whiteAlpha.300"
backdropFilter="saturate(180%) blur(10px)"
boxShadow="0 8px 32px rgba(0,0,0,0.2)"
>
<AlertIcon />
<Text fontSize="md" fontWeight="medium">
{selectedDate ? `当前选择:${formatDisplayDate(selectedDate)}` : '当前选择:--'}
{getSelectedDateCount() != null ? ` - ${getSelectedDateCount()}只涨停` : ''}
</Text>
</Alert>
<Card
bg="whiteAlpha.200"
color="whiteAlpha.900"
borderRadius="xl"
boxShadow="0 8px 32px rgba(0,0,0,0.2)"
borderWidth="1px"
borderColor="whiteAlpha.300"
backdropFilter="saturate(180%) blur(10px)"
w="full"
>
<CardBody>
<VStack align="stretch" spacing={2}>
<Heading size="sm" color="whiteAlpha.900">涨停数量图例</Heading>
<VStack align="stretch" spacing={2}>
<HStack>
<Box w={5} h={5} bg="green.200" borderRadius="md" border="1px solid" borderColor="whiteAlpha.400" />
<Text fontSize="sm">少量 (50)</Text>
</HStack>
<HStack>
<Box w={5} h={5} bg="yellow.200" borderRadius="md" border="1px solid" borderColor="whiteAlpha.400" />
<Text fontSize="sm">中等 (51-80)</Text>
</HStack>
<HStack>
<Box w={5} h={5} bg="red.200" borderRadius="md" border="1px solid" borderColor="whiteAlpha.400" />
<Text fontSize="sm">大量 (&gt;80)</Text>
</HStack>
</VStack>
</VStack>
</CardBody>
</Card>
</VStack>
</Flex>
{/* 右侧:半屏日历 */}
<Card
bg="whiteAlpha.200"
borderRadius="xl"
boxShadow="0 8px 32px rgba(0,0,0,0.2)"
borderWidth="1px"
borderColor="whiteAlpha.300"
backdropFilter="saturate(180%) blur(10px)"
w="full"
minH="420px"
>
<CardBody p={4}>
<EnhancedCalendar
selectedDate={selectedDate}
onDateChange={handleDateChange}
availableDates={availableDates}
compact
hideSelectionInfo
width="100%"
cellHeight={10}
/>
</CardBody>
</Card>
</SimpleGrid>
</Container>
</Box>
{/* 主内容区 */}
<Container maxW="container.xl" py={8}>
{/* 统计卡片 */}
{loading ? (
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4} mb={6}>
{[...Array(4)].map((_, i) => (
<Skeleton key={i} height="100px" borderRadius="lg" />
))}
</SimpleGrid>
) : (
<StatsCards />
)}
{/* 高级搜索 */}
<AdvancedSearch onSearch={handleSearch} loading={loading} />
{/* 板块详情 - 核心内容 */}
{loading ? (
<Skeleton height="600px" borderRadius="xl" mb={6} />
) : (
<Box mb={6}>
<SectorDetails
sortedSectors={getSortedSectorData()}
totalStocks={dailyData?.total_stocks || 0}
/>
</Box>
)}
{/* 高位股统计 */}
<HighPositionStocks dateStr={dateStr} />
{/* 数据分析 */}
{loading ? (
<Skeleton height="500px" borderRadius="xl" />
) : (
<DataAnalysis
dailyData={dailyData}
wordCloudData={wordCloudData}
/>
)}
</Container>
{/* 弹窗 */}
<SearchResultsModal
isOpen={isSearchOpen}
onClose={() => setIsSearchOpen(false)}
searchResults={searchResults}
onStockClick={() => {}}
/>
{/* 浮动按钮 */}
<Box position="fixed" bottom={8} right={8} zIndex={1000}>
<VStack spacing={3}>
<Tooltip label="刷新数据" placement="left">
<IconButton
icon={<RepeatIcon />}
colorScheme="blue"
size="lg"
borderRadius="full"
boxShadow="2xl"
onClick={() => fetchDailyAnalysis(dateStr)}
isLoading={loading}
/>
</Tooltip>
<Tooltip label="回到顶部" placement="left">
<IconButton
icon={<ChevronUpIcon />}
colorScheme="gray"
size="lg"
borderRadius="full"
boxShadow="2xl"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
/>
</Tooltip>
</VStack>
</Box>
{/* Footer区域 */}
<Box bg={useColorModeValue('gray.100', 'gray.800')} py={6} mt={8}>
<Container maxW="7xl">
<VStack spacing={2}>
<Text color="gray.500" fontSize="sm">
© 2024 价值前沿. 保留所有权利.
</Text>
<HStack spacing={4} fontSize="xs" color="gray.400">
<Link
href="https://beian.mps.gov.cn/#/query/webSearch?code=11010802046286"
isExternal
_hover={{ color: 'gray.600' }}
>
京公网安备11010802046286号
</Link>
<Text>京ICP备2025107343号-1</Text>
</HStack>
</VStack>
</Container>
</Box>
</Box>
);
}