Initial commit
This commit is contained in:
515
src/views/LimitAnalyse/index.js
Normal file
515
src/views/LimitAnalyse/index.js
Normal file
@@ -0,0 +1,515 @@
|
||||
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">大量 (>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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user