feat: 10.10线上最新代码提交
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -36,7 +36,9 @@ import {
|
||||
WrapItem,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { getFormattedTextProps } from '../../../utils/textUtils';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import './WordCloud.css';
|
||||
import {
|
||||
BarChart, Bar,
|
||||
PieChart, Pie, Cell,
|
||||
@@ -48,7 +50,7 @@ import {
|
||||
Treemap,
|
||||
Area, AreaChart,
|
||||
} from 'recharts';
|
||||
|
||||
import ReactWordcloud from 'react-wordcloud';
|
||||
// 颜色配置
|
||||
const CHART_COLORS = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD',
|
||||
@@ -67,70 +69,34 @@ const WordCloud = ({ data }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const treemapData = data.slice(0, 30).map((item, index) => ({
|
||||
name: item.name,
|
||||
size: item.value,
|
||||
color: CHART_COLORS[index % CHART_COLORS.length]
|
||||
const words = data.slice(0, 100).map(item => ({
|
||||
text: item.name || item.text,
|
||||
value: item.value || item.count || 1
|
||||
}));
|
||||
|
||||
const CustomContent = (props) => {
|
||||
const { x, y, width, height, name, size, color } = props;
|
||||
const fontSize = Math.min(Math.max(Math.sqrt(width * height) / 4, 10), 24);
|
||||
const options = {
|
||||
rotations: 2,
|
||||
rotationAngles: [-90, 0],
|
||||
fontFamily: 'Microsoft YaHei, sans-serif',
|
||||
fontSizes: [16, 80],
|
||||
fontWeight: 'bold',
|
||||
padding: 3,
|
||||
scale: 'sqrt',
|
||||
};
|
||||
|
||||
if (width < 30 || height < 20) return null;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
fill: color,
|
||||
stroke: '#fff',
|
||||
strokeWidth: 2,
|
||||
strokeOpacity: 1,
|
||||
}}
|
||||
/>
|
||||
<text
|
||||
x={x + width / 2}
|
||||
y={y + height / 2}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="white"
|
||||
fontSize={fontSize}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{name}
|
||||
</text>
|
||||
{width > 50 && height > 40 && (
|
||||
<text
|
||||
x={x + width / 2}
|
||||
y={y + height / 2 + fontSize + 2}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="white"
|
||||
fontSize={fontSize * 0.6}
|
||||
opacity={0.8}
|
||||
>
|
||||
{size}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
const callbacks = {
|
||||
getWordColor: () => {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD'];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<Treemap
|
||||
data={treemapData}
|
||||
dataKey="size"
|
||||
aspectRatio={4 / 3}
|
||||
stroke="#fff"
|
||||
content={<CustomContent />}
|
||||
/>
|
||||
</ResponsiveContainer>
|
||||
<ReactWordcloud
|
||||
words={words}
|
||||
options={options}
|
||||
callbacks={callbacks}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -610,7 +576,9 @@ export const StockDetailModal = ({ isOpen, onClose, selectedStock }) => {
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>详细分析</Text>
|
||||
<Box p={4} bg="gray.50" borderRadius="md">
|
||||
<Text whiteSpace="pre-wrap">{selectedStock.summary}</Text>
|
||||
<Text {...getFormattedTextProps(selectedStock.summary).props}>
|
||||
{getFormattedTextProps(selectedStock.summary).children}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -18,9 +18,30 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronLeftIcon, ChevronRightIcon, CalendarIcon } from '@chakra-ui/icons';
|
||||
|
||||
const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
const EnhancedCalendar = ({
|
||||
selectedDate,
|
||||
onDateChange,
|
||||
availableDates,
|
||||
compact = false,
|
||||
hideSelectionInfo = false,
|
||||
hideLegend = false,
|
||||
width,
|
||||
cellHeight,
|
||||
}) => {
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
|
||||
// 当外部选择日期变化时,如果不在当前月,则切换到对应月份
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
const isSameMonth =
|
||||
currentMonth.getFullYear() === selectedDate.getFullYear() &&
|
||||
currentMonth.getMonth() === selectedDate.getMonth();
|
||||
if (!isSameMonth) {
|
||||
setCurrentMonth(new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1));
|
||||
}
|
||||
}
|
||||
}, [selectedDate]);
|
||||
|
||||
const getDaysInMonth = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
@@ -79,34 +100,38 @@ const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
|
||||
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
|
||||
const calendarWidth = width ? width : (compact ? '420px' : '500px');
|
||||
const dayCellHeight = cellHeight ? cellHeight : (compact ? 12 : 16); // Chakra units
|
||||
const headerSize = compact ? 'md' : 'lg';
|
||||
|
||||
return (
|
||||
<Card bg="white" boxShadow="2xl" borderRadius="xl" w="500px">
|
||||
<CardHeader pb={3}>
|
||||
<Card bg="white" boxShadow="2xl" borderRadius="xl" w={calendarWidth}>
|
||||
<CardHeader pb={compact ? 2 : 3}>
|
||||
<VStack spacing={3}>
|
||||
<HStack justify="space-between" w="full">
|
||||
<IconButton
|
||||
icon={<ChevronLeftIcon />}
|
||||
size="md"
|
||||
size={compact ? 'sm' : 'md'}
|
||||
variant="ghost"
|
||||
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))}
|
||||
aria-label="上个月"
|
||||
/>
|
||||
<HStack spacing={2}>
|
||||
<CalendarIcon boxSize={5} />
|
||||
<Heading size="lg">
|
||||
<Heading size={headerSize}>
|
||||
{currentMonth.getFullYear()}年 {monthNames[currentMonth.getMonth()]}
|
||||
</Heading>
|
||||
</HStack>
|
||||
<IconButton
|
||||
icon={<ChevronRightIcon />}
|
||||
size="md"
|
||||
size={compact ? 'sm' : 'md'}
|
||||
variant="ghost"
|
||||
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))}
|
||||
aria-label="下个月"
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{selectedDate && (
|
||||
{!hideSelectionInfo && selectedDate && (
|
||||
<Alert status="info" borderRadius="md" fontSize="sm">
|
||||
<AlertIcon />
|
||||
<Text>
|
||||
@@ -121,10 +146,10 @@ const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
</CardHeader>
|
||||
|
||||
<CardBody pt={2}>
|
||||
<SimpleGrid columns={7} spacing={2}>
|
||||
<SimpleGrid columns={7} spacing={compact ? 1 : 2}>
|
||||
{weekDays.map(day => (
|
||||
<Box key={day} textAlign="center" p={3}>
|
||||
<Text fontSize="md" fontWeight="bold" color="gray.600">
|
||||
<Box key={day} textAlign="center" p={compact ? 2 : 3}>
|
||||
<Text fontSize={compact ? 'sm' : 'md'} fontWeight="bold" color="gray.600">
|
||||
{day}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -155,7 +180,7 @@ const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
<Box
|
||||
as="button"
|
||||
w="full"
|
||||
h={16}
|
||||
h={dayCellHeight}
|
||||
borderRadius="lg"
|
||||
position="relative"
|
||||
bg={hasData ? getDateColor(dateData.count) : 'transparent'}
|
||||
@@ -172,7 +197,7 @@ const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
cursor="pointer"
|
||||
>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontSize={compact ? 'md' : 'lg'}
|
||||
fontWeight={isToday || isSelected ? 'bold' : 'normal'}
|
||||
color={isSelected ? 'blue.600' : 'gray.700'}
|
||||
>
|
||||
@@ -183,11 +208,11 @@ const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
position="absolute"
|
||||
top="2px"
|
||||
right="2px"
|
||||
size="md"
|
||||
size={compact ? 'sm' : 'md'}
|
||||
colorScheme={getDateBadgeColor(dateData.count)}
|
||||
fontSize="11px"
|
||||
px={2}
|
||||
minW="28px"
|
||||
fontSize={compact ? '10px' : '11px'}
|
||||
px={compact ? 1 : 2}
|
||||
minW={compact ? '22px' : '28px'}
|
||||
borderRadius="full"
|
||||
>
|
||||
{dateData.count}
|
||||
@@ -199,7 +224,7 @@ const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
bottom="2px"
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
fontSize="10px"
|
||||
fontSize={compact ? '9px' : '10px'}
|
||||
color="blue.500"
|
||||
fontWeight="bold"
|
||||
>
|
||||
@@ -214,25 +239,28 @@ const EnhancedCalendar = ({ selectedDate, onDateChange, availableDates }) => {
|
||||
})}
|
||||
</SimpleGrid>
|
||||
|
||||
<Divider my={4} />
|
||||
|
||||
<VStack spacing={3}>
|
||||
<Text fontSize="sm" fontWeight="bold" color="gray.600">涨停数量图例</Text>
|
||||
<HStack justify="center" spacing={4}>
|
||||
<HStack spacing={2}>
|
||||
<Box w={5} h={5} bg="green.100" borderRadius="md" border="1px solid" borderColor="green.300" />
|
||||
<Text fontSize="sm">少量 (≤50只)</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Box w={5} h={5} bg="yellow.100" borderRadius="md" border="1px solid" borderColor="yellow.400" />
|
||||
<Text fontSize="sm">中等 (51-80只)</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Box w={5} h={5} bg="red.100" borderRadius="md" border="1px solid" borderColor="red.300" />
|
||||
<Text fontSize="sm">大量 (>80只)</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
{!hideLegend && (
|
||||
<>
|
||||
<Divider my={4} />
|
||||
<VStack spacing={3}>
|
||||
<Text fontSize="sm" fontWeight="bold" color="gray.600">涨停数量图例</Text>
|
||||
<HStack justify="center" spacing={4}>
|
||||
<HStack spacing={2}>
|
||||
<Box w={5} h={5} bg="green.100" borderRadius="md" border="1px solid" borderColor="green.300" />
|
||||
<Text fontSize="sm">少量 (≤50只)</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Box w={5} h={5} bg="yellow.100" borderRadius="md" border="1px solid" borderColor="yellow.400" />
|
||||
<Text fontSize="sm">中等 (51-80只)</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Box w={5} h={5} bg="red.100" borderRadius="md" border="1px solid" borderColor="red.300" />
|
||||
<Text fontSize="sm">大量 (>80只)</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
} from '@chakra-ui/react';
|
||||
import { formatTooltipText, getFormattedTextProps } from '../../../utils/textUtils';
|
||||
import { SearchIcon, CalendarIcon, ViewIcon, ExternalLinkIcon, DownloadIcon } from '@chakra-ui/icons';
|
||||
|
||||
// 高级搜索组件
|
||||
@@ -286,9 +287,26 @@ export const SearchResultsModal = ({ isOpen, onClose, searchResults, onStockClic
|
||||
)}
|
||||
</Td>
|
||||
<Td maxW="300px">
|
||||
<Tooltip label={stock.brief || stock.summary} placement="top">
|
||||
<Text fontSize="sm" noOfLines={2}>
|
||||
{stock.brief || stock.summary || '-'}
|
||||
<Tooltip
|
||||
label={formatTooltipText(stock.brief || stock.summary)}
|
||||
placement="top"
|
||||
hasArrow
|
||||
bg="gray.800"
|
||||
color="white"
|
||||
px={3}
|
||||
py={2}
|
||||
borderRadius="md"
|
||||
fontSize="sm"
|
||||
maxW="400px"
|
||||
whiteSpace="pre-line"
|
||||
>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
noOfLines={2}
|
||||
{...getFormattedTextProps(stock.brief || stock.summary || '-').props}
|
||||
_hover={{ cursor: 'help' }}
|
||||
>
|
||||
{getFormattedTextProps(stock.brief || stock.summary || '-').children}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
|
||||
@@ -23,14 +23,19 @@ import {
|
||||
WrapItem,
|
||||
Button,
|
||||
useColorModeValue,
|
||||
Collapse,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { StarIcon, ViewIcon, TimeIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
|
||||
import { getFormattedTextProps } from '../../../utils/textUtils';
|
||||
|
||||
const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
|
||||
const SectorDetails = ({ sortedSectors, totalStocks }) => {
|
||||
// 使用 useRef 来维持展开状态,避免重新渲染时重置
|
||||
const expandedSectorsRef = useRef([]);
|
||||
const [expandedSectors, setExpandedSectors] = useState([]);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
// 新增:管理每个股票涨停原因的展开状态
|
||||
const [expandedStockReasons, setExpandedStockReasons] = useState({});
|
||||
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
|
||||
@@ -56,6 +61,14 @@ const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 新增:切换股票涨停原因的展开状态
|
||||
const toggleStockReason = (stockCode) => {
|
||||
setExpandedStockReasons(prev => ({
|
||||
...prev,
|
||||
[stockCode]: !prev[stockCode]
|
||||
}));
|
||||
};
|
||||
|
||||
const getSectorColorScheme = (sector) => {
|
||||
if (sector === '公告') return 'orange';
|
||||
if (sector === '其他') return 'gray';
|
||||
@@ -91,15 +104,6 @@ const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack spacing={3}>
|
||||
<Heading size="md">板块详情</Heading>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="whiteAlpha"
|
||||
onClick={toggleAllSectors}
|
||||
leftIcon={expandedSectors.length === sortedSectors.length ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
>
|
||||
{expandedSectors.length === sortedSectors.length ? '全部收起' : '全部展开'}
|
||||
</Button>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Badge bg="whiteAlpha.900" color="blue.500" fontSize="md" px={3}>
|
||||
@@ -190,8 +194,6 @@ const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
|
||||
bg: 'gray.50'
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
cursor="pointer"
|
||||
onClick={() => onStockClick(stock)}
|
||||
>
|
||||
<Flex justify="space-between" align="start">
|
||||
<VStack align="start" spacing={2} flex={1}>
|
||||
@@ -209,9 +211,21 @@ const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Text fontSize="sm" color="gray.600" noOfLines={2}>
|
||||
{stock.brief || stock.summary || '暂无涨停原因'}
|
||||
</Text>
|
||||
<Collapse in={expandedStockReasons[stock.scode]}>
|
||||
<Box mt={2} p={3} bg="gray.50" borderRadius="md" border="1px solid" borderColor="gray.200">
|
||||
<Text fontSize="sm" color="gray.700" fontWeight="bold">
|
||||
涨停原因:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color="gray.600"
|
||||
noOfLines={3}
|
||||
{...getFormattedTextProps(stock.brief || stock.summary || '暂无涨停原因').props}
|
||||
>
|
||||
{getFormattedTextProps(stock.brief || stock.summary || '暂无涨停原因').children}
|
||||
</Text>
|
||||
</Box>
|
||||
</Collapse>
|
||||
|
||||
<HStack spacing={4} fontSize="xs" color="gray.500">
|
||||
<HStack spacing={1}>
|
||||
@@ -255,11 +269,12 @@ const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
|
||||
</VStack>
|
||||
|
||||
<IconButton
|
||||
icon={<ViewIcon />}
|
||||
icon={expandedStockReasons[stock.scode] ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme={colorScheme}
|
||||
aria-label="查看详情"
|
||||
aria-label={expandedStockReasons[stock.scode] ? "收起原因" : "展开原因"}
|
||||
onClick={() => toggleStockReason(stock.scode)}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
229
src/views/LimitAnalyse/index.js
Normal file → Executable file
229
src/views/LimitAnalyse/index.js
Normal file → Executable file
@@ -21,6 +21,9 @@ import {
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
StatArrow,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Link,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
RepeatIcon,
|
||||
@@ -32,24 +35,29 @@ import {
|
||||
// 这里为了演示,我们假设它们已经被正确导出
|
||||
|
||||
// API配置
|
||||
const API_URL = process.env.NODE_ENV === 'production' ? '/report-api' : 'http://111.198.58.126:8811';
|
||||
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(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(null);
|
||||
const [dateStr, setDateStr] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dailyData, setDailyData] = useState(null);
|
||||
const [availableDates, setAvailableDates] = useState([]);
|
||||
const [selectedStock, setSelectedStock] = useState(null);
|
||||
const [wordCloudData, setWordCloudData] = useState([]);
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState(null);
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
|
||||
@@ -64,14 +72,37 @@ export default function LimitAnalyse() {
|
||||
fetchAvailableDates();
|
||||
}, []);
|
||||
|
||||
// 加载初始数据
|
||||
// 初始进入展示骨架屏,直到选中日期的数据加载完成
|
||||
useEffect(() => {
|
||||
const today = new Date();
|
||||
const dateString = formatDateStr(today);
|
||||
setDateStr(dateString);
|
||||
fetchDailyAnalysis(dateString);
|
||||
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 {
|
||||
@@ -178,12 +209,6 @@ export default function LimitAnalyse() {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理股票详情
|
||||
const handleStockDetail = (stock) => {
|
||||
setSelectedStock(stock);
|
||||
setIsDetailOpen(true);
|
||||
};
|
||||
|
||||
// 处理板块数据排序
|
||||
const getSortedSectorData = () => {
|
||||
if (!dailyData?.sector_data) return [];
|
||||
@@ -261,35 +286,130 @@ export default function LimitAnalyse() {
|
||||
</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">
|
||||
<Flex justify="space-between" align="center" wrap="wrap" gap={6}>
|
||||
<VStack align="start" spacing={2}>
|
||||
<HStack>
|
||||
<Badge colorScheme="whiteAlpha" fontSize="sm" px={2}>
|
||||
AI驱动
|
||||
</Badge>
|
||||
<Badge colorScheme="yellow" fontSize="sm" px={2}>
|
||||
实时更新
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Heading size="xl">涨停板块分析平台</Heading>
|
||||
<Text fontSize="lg" opacity={0.9}>
|
||||
智能分析每日涨停板块,精准捕捉市场热点
|
||||
</Text>
|
||||
</VStack>
|
||||
<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>
|
||||
|
||||
<Box mt={{ base: 4, md: 0 }}>
|
||||
<EnhancedCalendar
|
||||
selectedDate={selectedDate}
|
||||
onDateChange={handleDateChange}
|
||||
availableDates={availableDates}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<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>
|
||||
|
||||
@@ -317,11 +437,13 @@ export default function LimitAnalyse() {
|
||||
<SectorDetails
|
||||
sortedSectors={getSortedSectorData()}
|
||||
totalStocks={dailyData?.total_stocks || 0}
|
||||
onStockClick={handleStockDetail}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 高位股统计 */}
|
||||
<HighPositionStocks dateStr={dateStr} />
|
||||
|
||||
{/* 数据分析 */}
|
||||
{loading ? (
|
||||
<Skeleton height="500px" borderRadius="xl" />
|
||||
@@ -334,17 +456,11 @@ export default function LimitAnalyse() {
|
||||
</Container>
|
||||
|
||||
{/* 弹窗 */}
|
||||
<StockDetailModal
|
||||
isOpen={isDetailOpen}
|
||||
onClose={() => setIsDetailOpen(false)}
|
||||
selectedStock={selectedStock}
|
||||
/>
|
||||
|
||||
<SearchResultsModal
|
||||
isOpen={isSearchOpen}
|
||||
onClose={() => setIsSearchOpen(false)}
|
||||
searchResults={searchResults}
|
||||
onStockClick={handleStockDetail}
|
||||
onStockClick={() => {}}
|
||||
/>
|
||||
|
||||
{/* 浮动按钮 */}
|
||||
@@ -373,6 +489,27 @@ export default function LimitAnalyse() {
|
||||
</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