Merge branch 'feature_bugfix/20260106' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/20260106
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
// src/views/Community/components/HeroPanel.js
|
// src/views/Community/components/HeroPanel.js
|
||||||
// 顶部说明面板组件:事件中心标题 + 投资日历
|
// 顶部说明面板组件:事件中心标题 + 涨停日历
|
||||||
// 简化版本:移除了指数K线和热门概念,由右侧边栏提供
|
// 大尺寸日历,显示每日涨停数和主要板块
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback, useMemo, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
@@ -25,16 +25,11 @@ import {
|
|||||||
ModalCloseButton,
|
ModalCloseButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Badge,
|
Badge,
|
||||||
Grid,
|
SimpleGrid,
|
||||||
GridItem,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
import { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight, Flame, TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { logger } from '@utils/logger';
|
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
|
||||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||||
import InvestmentCalendar from '@components/InvestmentCalendar';
|
|
||||||
|
|
||||||
// 定义动画
|
// 定义动画
|
||||||
const animations = `
|
const animations = `
|
||||||
@@ -68,295 +63,466 @@ const isInTradingTime = () => {
|
|||||||
return timeInMinutes >= 570 && timeInMinutes <= 900;
|
return timeInMinutes >= 570 && timeInMinutes <= 900;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// 主题色配置
|
||||||
* 获取月度事件统计
|
const goldColors = {
|
||||||
*/
|
primary: '#D4AF37',
|
||||||
const fetchEventCounts = async (year, month) => {
|
light: '#F4D03F',
|
||||||
try {
|
dark: '#B8860B',
|
||||||
const response = await fetch(`${getApiBase()}/api/events/calendar/counts?year=${year}&month=${month}`);
|
glow: 'rgba(212, 175, 55, 0.4)',
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
const data = await response.json();
|
|
||||||
return data.success ? data.data : [];
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('HeroPanel', 'fetchEventCounts error', { year, month, error: error.message });
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const textColors = {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: 'rgba(255, 255, 255, 0.85)',
|
||||||
|
muted: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 热度级别配置
|
||||||
|
const HEAT_LEVELS = [
|
||||||
|
{ key: 'high', threshold: 80, colors: { bg: 'rgba(147, 51, 234, 0.55)', text: '#d8b4fe', border: 'rgba(147, 51, 234, 0.65)' } },
|
||||||
|
{ key: 'medium', threshold: 60, colors: { bg: 'rgba(239, 68, 68, 0.50)', text: '#fca5a5', border: 'rgba(239, 68, 68, 0.60)' } },
|
||||||
|
{ key: 'low', threshold: 40, colors: { bg: 'rgba(251, 146, 60, 0.45)', text: '#fed7aa', border: 'rgba(251, 146, 60, 0.55)' } },
|
||||||
|
{ key: 'cold', threshold: 0, colors: { bg: 'rgba(59, 130, 246, 0.35)', text: '#93c5fd', border: 'rgba(59, 130, 246, 0.45)' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_HEAT_COLORS = {
|
||||||
|
bg: 'rgba(60, 60, 70, 0.12)',
|
||||||
|
text: textColors.muted,
|
||||||
|
border: 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeatColor = (count) => {
|
||||||
|
if (!count) return DEFAULT_HEAT_COLORS;
|
||||||
|
const level = HEAT_LEVELS.find((l) => count >= l.threshold);
|
||||||
|
return level?.colors || DEFAULT_HEAT_COLORS;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日期格式化
|
||||||
|
const formatDateStr = (date) => {
|
||||||
|
if (!date) return '';
|
||||||
|
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 WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
|
const MONTH_NAMES = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 紧凑型投资日历组件 - 深色主题风格
|
* 趋势图标
|
||||||
* 特点:
|
|
||||||
* 1. 紧凑的月历视图
|
|
||||||
* 2. 事件数量用圆点颜色深浅表示
|
|
||||||
* 3. 悬停显示当日事件数
|
|
||||||
* 4. 点击打开完整日历弹窗
|
|
||||||
*/
|
*/
|
||||||
const CompactCalendar = () => {
|
const TrendIcon = memo(({ current, previous }) => {
|
||||||
const [currentMonth, setCurrentMonth] = useState(dayjs());
|
if (!current || !previous) return null;
|
||||||
const [eventCounts, setEventCounts] = useState([]);
|
const diff = current - previous;
|
||||||
const [loading, setLoading] = useState(true);
|
if (diff === 0) return null;
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
|
|
||||||
// 加载月度事件统计
|
|
||||||
const loadEventCounts = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
const data = await fetchEventCounts(currentMonth.year(), currentMonth.month() + 1);
|
|
||||||
setEventCounts(data);
|
|
||||||
setLoading(false);
|
|
||||||
}, [currentMonth]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadEventCounts();
|
|
||||||
}, [loadEventCounts]);
|
|
||||||
|
|
||||||
// 获取事件数量映射
|
|
||||||
const getEventCountForDate = (dateStr) => {
|
|
||||||
const found = eventCounts.find(item => item.date === dateStr);
|
|
||||||
return found ? found.count : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据事件数量获取颜色
|
|
||||||
const getEventColor = (count) => {
|
|
||||||
if (count === 0) return 'transparent';
|
|
||||||
if (count >= 15) return 'rgba(239, 68, 68, 0.9)'; // 深红
|
|
||||||
if (count >= 10) return 'rgba(249, 115, 22, 0.85)'; // 橙红
|
|
||||||
if (count >= 5) return 'rgba(234, 179, 8, 0.8)'; // 金黄
|
|
||||||
if (count >= 3) return 'rgba(34, 197, 94, 0.75)'; // 绿色
|
|
||||||
return 'rgba(59, 130, 246, 0.7)'; // 蓝色
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成日历网格
|
|
||||||
const generateCalendarDays = () => {
|
|
||||||
const startOfMonth = currentMonth.startOf('month');
|
|
||||||
const endOfMonth = currentMonth.endOf('month');
|
|
||||||
const startDay = startOfMonth.day(); // 0=周日
|
|
||||||
const daysInMonth = endOfMonth.date();
|
|
||||||
|
|
||||||
const days = [];
|
|
||||||
const today = dayjs().format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
// 填充月初空白
|
|
||||||
for (let i = 0; i < startDay; i++) {
|
|
||||||
days.push({ date: null, day: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充日期
|
|
||||||
for (let day = 1; day <= daysInMonth; day++) {
|
|
||||||
const date = currentMonth.date(day).format('YYYY-MM-DD');
|
|
||||||
const count = getEventCountForDate(date);
|
|
||||||
days.push({
|
|
||||||
date,
|
|
||||||
day,
|
|
||||||
count,
|
|
||||||
isToday: date === today,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return days;
|
|
||||||
};
|
|
||||||
|
|
||||||
const days = generateCalendarDays();
|
|
||||||
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
|
|
||||||
|
|
||||||
|
const isUp = diff > 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<Icon
|
||||||
|
as={isUp ? TrendingUp : TrendingDown}
|
||||||
|
boxSize={3}
|
||||||
|
color={isUp ? '#22c55e' : '#ef4444'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TrendIcon.displayName = 'TrendIcon';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日历单元格
|
||||||
|
*/
|
||||||
|
const CalendarCell = memo(({ date, dateData, previousData, isSelected, isToday, isWeekend, onClick }) => {
|
||||||
|
if (!date) {
|
||||||
|
return <Box />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasData = !!dateData;
|
||||||
|
const count = dateData?.count || 0;
|
||||||
|
const heatColors = getHeatColor(count);
|
||||||
|
const topSector = dateData?.top_sector || '';
|
||||||
|
|
||||||
|
// 周末无数据显示"休市"
|
||||||
|
if (isWeekend && !hasData) {
|
||||||
|
return (
|
||||||
<Box
|
<Box
|
||||||
bg="rgba(0,0,0,0.3)"
|
p={1.5}
|
||||||
backdropFilter={GLASS_BLUR.sm}
|
borderRadius="10px"
|
||||||
borderRadius="xl"
|
bg="rgba(30, 30, 40, 0.3)"
|
||||||
border="1px solid rgba(255,215,0,0.2)"
|
border="1px solid rgba(255, 255, 255, 0.03)"
|
||||||
p={3}
|
textAlign="center"
|
||||||
minW="280px"
|
minH="70px"
|
||||||
position="relative"
|
display="flex"
|
||||||
overflow="hidden"
|
flexDirection="column"
|
||||||
_before={{
|
justifyContent="center"
|
||||||
content: '""',
|
alignItems="center"
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: '1px',
|
|
||||||
background: 'linear-gradient(90deg, transparent, rgba(255,215,0,0.4), transparent)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* 头部:月份切换 */}
|
<Text fontSize="md" fontWeight="400" color="rgba(255, 255, 255, 0.25)">
|
||||||
<Flex justify="space-between" align="center" mb={2}>
|
{date.getDate()}
|
||||||
<HStack spacing={2}>
|
</Text>
|
||||||
<Box
|
<Text fontSize="9px" color="rgba(255, 255, 255, 0.2)">
|
||||||
p={1.5}
|
休市
|
||||||
bg="rgba(255,215,0,0.15)"
|
</Text>
|
||||||
borderRadius="lg"
|
</Box>
|
||||||
border="1px solid rgba(255,215,0,0.25)"
|
);
|
||||||
>
|
}
|
||||||
<Icon as={Calendar} color="gold" boxSize={4} />
|
|
||||||
</Box>
|
|
||||||
<Text fontSize="sm" fontWeight="bold" color="gold">
|
|
||||||
投资日历
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<IconButton
|
|
||||||
icon={<ChevronLeft size={14} />}
|
|
||||||
size="xs"
|
|
||||||
variant="ghost"
|
|
||||||
color="whiteAlpha.700"
|
|
||||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
|
||||||
onClick={() => setCurrentMonth(prev => prev.subtract(1, 'month'))}
|
|
||||||
aria-label="上个月"
|
|
||||||
/>
|
|
||||||
<Text fontSize="xs" color="whiteAlpha.800" fontWeight="medium" minW="70px" textAlign="center">
|
|
||||||
{currentMonth.format('YYYY年M月')}
|
|
||||||
</Text>
|
|
||||||
<IconButton
|
|
||||||
icon={<ChevronRight size={14} />}
|
|
||||||
size="xs"
|
|
||||||
variant="ghost"
|
|
||||||
color="whiteAlpha.700"
|
|
||||||
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
|
||||||
onClick={() => setCurrentMonth(prev => prev.add(1, 'month'))}
|
|
||||||
aria-label="下个月"
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* 星期头 */}
|
// 正常日期
|
||||||
<Grid templateColumns="repeat(7, 1fr)" gap={0.5} mb={1}>
|
return (
|
||||||
{weekDays.map((day, idx) => (
|
<Tooltip
|
||||||
<GridItem key={day}>
|
label={
|
||||||
<Text
|
<VStack spacing={1} align="start" p={1}>
|
||||||
fontSize="10px"
|
<Text fontWeight="bold">{`${date.getMonth() + 1}月${date.getDate()}日`}</Text>
|
||||||
color={idx === 0 || idx === 6 ? 'orange.300' : 'whiteAlpha.500'}
|
{hasData ? (
|
||||||
textAlign="center"
|
<>
|
||||||
fontWeight="medium"
|
<Text>涨停数: {count}家</Text>
|
||||||
>
|
{topSector && <Text>主线板块: {topSector}</Text>}
|
||||||
{day}
|
</>
|
||||||
</Text>
|
) : (
|
||||||
</GridItem>
|
<Text color="gray.400">暂无数据</Text>
|
||||||
))}
|
)}
|
||||||
</Grid>
|
</VStack>
|
||||||
|
}
|
||||||
{/* 日期网格 */}
|
placement="top"
|
||||||
{loading ? (
|
hasArrow
|
||||||
<Center h="140px">
|
bg="rgba(15, 15, 22, 0.95)"
|
||||||
<Spinner size="sm" color="gold" />
|
border="1px solid rgba(212, 175, 55, 0.3)"
|
||||||
</Center>
|
borderRadius="12px"
|
||||||
) : (
|
>
|
||||||
<Grid templateColumns="repeat(7, 1fr)" gap={0.5}>
|
<Box
|
||||||
{days.map((item, idx) => (
|
as="button"
|
||||||
<GridItem key={idx}>
|
p={1.5}
|
||||||
{item.day ? (
|
borderRadius="10px"
|
||||||
<Tooltip
|
bg={hasData ? heatColors.bg : 'rgba(40, 40, 50, 0.3)'}
|
||||||
label={item.count > 0 ? `${item.count}个事件` : '无事件'}
|
border={
|
||||||
placement="top"
|
isSelected
|
||||||
hasArrow
|
? `2px solid ${goldColors.primary}`
|
||||||
bg="gray.800"
|
: isToday
|
||||||
color="white"
|
? `2px solid ${goldColors.light}`
|
||||||
fontSize="xs"
|
: hasData
|
||||||
>
|
? `1px solid ${heatColors.border}`
|
||||||
<Flex
|
: '1px solid rgba(255, 255, 255, 0.08)'
|
||||||
direction="column"
|
}
|
||||||
align="center"
|
boxShadow={isSelected ? `0 0 12px ${goldColors.glow}` : isToday ? `0 0 8px ${goldColors.glow}` : 'none'}
|
||||||
justify="center"
|
position="relative"
|
||||||
h="28px"
|
cursor="pointer"
|
||||||
borderRadius="md"
|
transition="all 0.2s"
|
||||||
cursor="pointer"
|
_hover={{
|
||||||
position="relative"
|
transform: 'scale(1.03)',
|
||||||
bg={item.isToday ? 'rgba(255,215,0,0.2)' : 'transparent'}
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||||
border={item.isToday ? '1px solid rgba(255,215,0,0.5)' : '1px solid transparent'}
|
borderColor: goldColors.primary,
|
||||||
_hover={{
|
}}
|
||||||
bg: 'rgba(255,255,255,0.1)',
|
onClick={() => onClick && onClick(date)}
|
||||||
borderColor: 'rgba(255,255,255,0.2)',
|
w="full"
|
||||||
}}
|
minH="70px"
|
||||||
transition="all 0.15s"
|
>
|
||||||
onClick={() => setIsModalOpen(true)}
|
{/* 今天标记 */}
|
||||||
>
|
{isToday && (
|
||||||
<Text
|
<Badge
|
||||||
fontSize="11px"
|
position="absolute"
|
||||||
color={item.isToday ? 'gold' : 'whiteAlpha.800'}
|
top="2px"
|
||||||
fontWeight={item.isToday ? 'bold' : 'normal'}
|
right="2px"
|
||||||
>
|
bg="rgba(239, 68, 68, 0.9)"
|
||||||
{item.day}
|
color="white"
|
||||||
</Text>
|
fontSize="8px"
|
||||||
{/* 事件指示点 */}
|
px={1}
|
||||||
{item.count > 0 && (
|
borderRadius="sm"
|
||||||
<Box
|
>
|
||||||
position="absolute"
|
今天
|
||||||
bottom="2px"
|
</Badge>
|
||||||
w="4px"
|
|
||||||
h="4px"
|
|
||||||
borderRadius="full"
|
|
||||||
bg={getEventColor(item.count)}
|
|
||||||
boxShadow={`0 0 4px ${getEventColor(item.count)}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Box h="28px" />
|
|
||||||
)}
|
|
||||||
</GridItem>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 底部:查看更多 */}
|
<VStack spacing={0.5} align="center">
|
||||||
<Flex justify="center" mt={2}>
|
{/* 日期 */}
|
||||||
<Text
|
<Text
|
||||||
fontSize="10px"
|
fontSize="md"
|
||||||
color="whiteAlpha.500"
|
fontWeight={isSelected || isToday ? 'bold' : '500'}
|
||||||
cursor="pointer"
|
color={isSelected ? goldColors.primary : isToday ? goldColors.light : textColors.primary}
|
||||||
_hover={{ color: 'gold' }}
|
|
||||||
transition="color 0.2s"
|
|
||||||
onClick={() => setIsModalOpen(true)}
|
|
||||||
>
|
>
|
||||||
点击查看详细日历 →
|
{date.getDate()}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 完整日历弹窗 */}
|
{/* 涨停数 + 趋势 */}
|
||||||
<Modal
|
{hasData && (
|
||||||
isOpen={isModalOpen}
|
<HStack spacing={0.5} justify="center">
|
||||||
onClose={() => setIsModalOpen(false)}
|
<Text fontSize="xs" fontWeight="bold" color={heatColors.text}>
|
||||||
size="6xl"
|
{count}家
|
||||||
>
|
</Text>
|
||||||
<ModalOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
|
<TrendIcon current={count} previous={previousData?.count} />
|
||||||
<ModalContent
|
</HStack>
|
||||||
maxW="1200px"
|
)}
|
||||||
bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)"
|
|
||||||
border="1px solid rgba(255,215,0,0.2)"
|
{/* 主要板块 */}
|
||||||
borderRadius="2xl"
|
{hasData && topSector && (
|
||||||
>
|
<HStack spacing={0.5}>
|
||||||
<ModalHeader
|
{count >= 80 && <Icon as={Flame} boxSize={2.5} color="#f97316" />}
|
||||||
borderBottom="1px solid rgba(255,215,0,0.2)"
|
<Text fontSize="9px" color={textColors.secondary} noOfLines={1} maxW="65px">
|
||||||
color="white"
|
{topSector}
|
||||||
>
|
|
||||||
<HStack spacing={3}>
|
|
||||||
<Box
|
|
||||||
p={2}
|
|
||||||
bg="rgba(255,215,0,0.15)"
|
|
||||||
borderRadius="lg"
|
|
||||||
border="1px solid rgba(255,215,0,0.3)"
|
|
||||||
>
|
|
||||||
<Icon as={Calendar} color="gold" boxSize={5} />
|
|
||||||
</Box>
|
|
||||||
<Text
|
|
||||||
fontSize="lg"
|
|
||||||
fontWeight="bold"
|
|
||||||
bgGradient="linear(to-r, #FFD700, #FFA500)"
|
|
||||||
bgClip="text"
|
|
||||||
>
|
|
||||||
投资日历
|
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</ModalHeader>
|
)}
|
||||||
<ModalCloseButton color="whiteAlpha.600" _hover={{ color: 'white' }} />
|
</VStack>
|
||||||
<ModalBody pb={6}>
|
</Box>
|
||||||
<InvestmentCalendar />
|
</Tooltip>
|
||||||
</ModalBody>
|
);
|
||||||
</ModalContent>
|
});
|
||||||
</Modal>
|
|
||||||
</>
|
CalendarCell.displayName = 'CalendarCell';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 涨停日历组件 - 大尺寸版本
|
||||||
|
*/
|
||||||
|
const LimitUpCalendar = () => {
|
||||||
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
|
const [datesData, setDatesData] = useState([]);
|
||||||
|
const [dailyDetails, setDailyDetails] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// 加载 dates.json
|
||||||
|
useEffect(() => {
|
||||||
|
const loadDatesData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/data/zt/dates.json');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setDatesData(data.dates || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load dates.json:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadDatesData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 获取当月需要的日期详情(主要板块)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMonthDetails = async () => {
|
||||||
|
const year = currentMonth.getFullYear();
|
||||||
|
const month = currentMonth.getMonth();
|
||||||
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
const details = {};
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const dateStr = `${year}${String(month + 1).padStart(2, '0')}${String(day).padStart(2, '0')}`;
|
||||||
|
// 检查是否有这天的数据
|
||||||
|
const hasData = datesData.some(d => d.date === dateStr);
|
||||||
|
if (hasData && !dailyDetails[dateStr]) {
|
||||||
|
promises.push(
|
||||||
|
fetch(`/data/zt/daily/${dateStr}.json`)
|
||||||
|
.then(res => res.ok ? res.json() : null)
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.sector_data) {
|
||||||
|
// 找出涨停数最多的板块
|
||||||
|
let maxSector = '';
|
||||||
|
let maxCount = 0;
|
||||||
|
Object.entries(data.sector_data).forEach(([sector, info]) => {
|
||||||
|
if (info.count > maxCount) {
|
||||||
|
maxCount = info.count;
|
||||||
|
maxSector = sector;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
details[dateStr] = { top_sector: maxSector };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promises.length > 0) {
|
||||||
|
await Promise.all(promises);
|
||||||
|
setDailyDetails(prev => ({ ...prev, ...details }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (datesData.length > 0) {
|
||||||
|
loadMonthDetails();
|
||||||
|
}
|
||||||
|
}, [currentMonth, datesData]);
|
||||||
|
|
||||||
|
// 构建日期数据映射
|
||||||
|
const dateDataMap = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
datesData.forEach(d => {
|
||||||
|
const detail = dailyDetails[d.date] || {};
|
||||||
|
map.set(d.date, {
|
||||||
|
date: d.date,
|
||||||
|
count: d.count,
|
||||||
|
top_sector: detail.top_sector,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [datesData, dailyDetails]);
|
||||||
|
|
||||||
|
// 生成日历天数
|
||||||
|
const days = useMemo(() => {
|
||||||
|
const year = currentMonth.getFullYear();
|
||||||
|
const month = currentMonth.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const daysInMonth = lastDay.getDate();
|
||||||
|
const startingDayOfWeek = firstDay.getDay();
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
// 前置空白天数
|
||||||
|
for (let i = 0; i < startingDayOfWeek; i++) {
|
||||||
|
result.push(null);
|
||||||
|
}
|
||||||
|
// 当月天数
|
||||||
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
|
result.push(new Date(year, month, i));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [currentMonth]);
|
||||||
|
|
||||||
|
// 预计算日历格子数据
|
||||||
|
const calendarCellsData = useMemo(() => {
|
||||||
|
const today = new Date();
|
||||||
|
const todayStr = today.toDateString();
|
||||||
|
|
||||||
|
return days.map((date, index) => {
|
||||||
|
if (!date) {
|
||||||
|
return { key: `empty-${index}`, date: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateStr = formatDateStr(date);
|
||||||
|
const dateData = dateDataMap.get(dateStr) || null;
|
||||||
|
const dayOfWeek = date.getDay();
|
||||||
|
|
||||||
|
// 获取前一天数据
|
||||||
|
const prevDate = new Date(date);
|
||||||
|
prevDate.setDate(prevDate.getDate() - 1);
|
||||||
|
const previousData = dateDataMap.get(formatDateStr(prevDate)) || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: dateStr || `day-${index}`,
|
||||||
|
date,
|
||||||
|
dateData,
|
||||||
|
previousData,
|
||||||
|
isToday: date.toDateString() === todayStr,
|
||||||
|
isWeekend: dayOfWeek === 0 || dayOfWeek === 6,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [days, dateDataMap]);
|
||||||
|
|
||||||
|
// 月份导航
|
||||||
|
const handlePrevMonth = useCallback(() => {
|
||||||
|
setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() - 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNextMonth = useCallback(() => {
|
||||||
|
setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() + 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg="rgba(15, 15, 22, 0.6)"
|
||||||
|
backdropFilter={GLASS_BLUR.md}
|
||||||
|
borderRadius="16px"
|
||||||
|
border="1px solid rgba(212, 175, 55, 0.2)"
|
||||||
|
p={4}
|
||||||
|
position="relative"
|
||||||
|
overflow="hidden"
|
||||||
|
flex="1"
|
||||||
|
>
|
||||||
|
{/* 顶部装饰条 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
h="2px"
|
||||||
|
bgGradient="linear(to-r, transparent, #D4AF37, #F4D03F, #D4AF37, transparent)"
|
||||||
|
animation="shimmer 3s linear infinite"
|
||||||
|
backgroundSize="200% 100%"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 月份导航 */}
|
||||||
|
<HStack justify="space-between" mb={3}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ChevronLeft size={18} />}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
color={textColors.secondary}
|
||||||
|
_hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||||
|
onClick={handlePrevMonth}
|
||||||
|
aria-label="上个月"
|
||||||
|
/>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Icon as={Calendar} boxSize={4} color={goldColors.primary} />
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color={textColors.primary}>
|
||||||
|
{currentMonth.getFullYear()}年{MONTH_NAMES[currentMonth.getMonth()]}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color={textColors.muted}>
|
||||||
|
涨停日历
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<IconButton
|
||||||
|
icon={<ChevronRight size={18} />}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
color={textColors.secondary}
|
||||||
|
_hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||||
|
onClick={handleNextMonth}
|
||||||
|
aria-label="下个月"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 星期标题 */}
|
||||||
|
<SimpleGrid columns={7} spacing={1.5} mb={2}>
|
||||||
|
{WEEK_DAYS.map((day, idx) => (
|
||||||
|
<Text
|
||||||
|
key={day}
|
||||||
|
textAlign="center"
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="600"
|
||||||
|
color={idx === 0 || idx === 6 ? 'orange.300' : textColors.secondary}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* 日历格子 */}
|
||||||
|
{loading ? (
|
||||||
|
<Center h="200px">
|
||||||
|
<Spinner size="lg" color={goldColors.primary} />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<SimpleGrid columns={7} spacing={1.5}>
|
||||||
|
{calendarCellsData.map((cellData) => (
|
||||||
|
<CalendarCell
|
||||||
|
key={cellData.key}
|
||||||
|
date={cellData.date}
|
||||||
|
dateData={cellData.dateData}
|
||||||
|
previousData={cellData.previousData}
|
||||||
|
isSelected={false}
|
||||||
|
isToday={cellData.isToday}
|
||||||
|
isWeekend={cellData.isWeekend}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 图例 */}
|
||||||
|
<HStack spacing={4} mt={3} justify="center" flexWrap="wrap">
|
||||||
|
{[
|
||||||
|
{ label: '超高潮 ≥80', color: 'rgba(147, 51, 234, 0.7)' },
|
||||||
|
{ label: '高潮 ≥60', color: 'rgba(239, 68, 68, 0.65)' },
|
||||||
|
{ label: '温和 ≥40', color: 'rgba(251, 146, 60, 0.6)' },
|
||||||
|
{ label: '偏冷 <40', color: 'rgba(59, 130, 246, 0.5)' },
|
||||||
|
].map(({ label, color }) => (
|
||||||
|
<HStack key={label} spacing={1}>
|
||||||
|
<Box w="10px" h="10px" borderRadius="sm" bg={color} />
|
||||||
|
<Text fontSize="10px" color={textColors.muted}>{label}</Text>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -567,10 +733,9 @@ const HeroPanel = () => {
|
|||||||
filter="blur(50px)"
|
filter="blur(50px)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardBody p={{ base: 3, md: 4 }}>
|
<CardBody p={{ base: 4, md: 5 }}>
|
||||||
{/* 标题行:标题 + 使用说明 + 日历 + 交易状态 */}
|
{/* 标题行 */}
|
||||||
<Flex align="center" justify="space-between" wrap="wrap" gap={3}>
|
<Flex align="center" justify="space-between" mb={4} wrap="wrap" gap={2}>
|
||||||
{/* 左侧:标题 + 使用说明 */}
|
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
<Heading size="lg">
|
<Heading size="lg">
|
||||||
<Text
|
<Text
|
||||||
@@ -583,7 +748,6 @@ const HeroPanel = () => {
|
|||||||
事件中心
|
事件中心
|
||||||
</Text>
|
</Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
{/* 使用说明 - 弹窗 */}
|
|
||||||
<InfoModal />
|
<InfoModal />
|
||||||
{/* 交易状态 */}
|
{/* 交易状态 */}
|
||||||
{isInTradingTime() && (
|
{isInTradingTime() && (
|
||||||
@@ -609,10 +773,10 @@ const HeroPanel = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 右侧:紧凑型日历 */}
|
|
||||||
<CompactCalendar />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{/* 涨停日历 */}
|
||||||
|
<LimitUpCalendar />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user