community增加事件详情
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// src/views/Community/components/HeroPanel.js
|
||||
// 顶部说明面板组件:事件中心标题 + 涨停日历
|
||||
// 大尺寸日历,显示每日涨停数和主要板块
|
||||
// 综合日历面板:融合涨停分析 + 投资日历
|
||||
// 左侧详情面板(TAB切换历史涨停/未来事件)+ 右侧日历
|
||||
|
||||
import React, { useEffect, useState, useCallback, useMemo, memo } from 'react';
|
||||
import {
|
||||
@@ -27,9 +27,21 @@ import {
|
||||
Badge,
|
||||
SimpleGrid,
|
||||
IconButton,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Tag,
|
||||
Divider,
|
||||
Button,
|
||||
Wrap,
|
||||
WrapItem,
|
||||
} from '@chakra-ui/react';
|
||||
import { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight, Flame, TrendingUp, TrendingDown } from 'lucide-react';
|
||||
import { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight, Flame, TrendingUp, TrendingDown, Zap, FileText, Star } from 'lucide-react';
|
||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||
import { eventService } from '@services/eventService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 定义动画
|
||||
const animations = `
|
||||
@@ -121,7 +133,7 @@ const TrendIcon = memo(({ current, previous }) => {
|
||||
return (
|
||||
<Icon
|
||||
as={isUp ? TrendingUp : TrendingDown}
|
||||
boxSize={3}
|
||||
boxSize={2.5}
|
||||
color={isUp ? '#22c55e' : '#ef4444'}
|
||||
/>
|
||||
);
|
||||
@@ -130,34 +142,35 @@ const TrendIcon = memo(({ current, previous }) => {
|
||||
TrendIcon.displayName = 'TrendIcon';
|
||||
|
||||
/**
|
||||
* 日历单元格
|
||||
* 日历单元格 - 显示涨停数和事件数
|
||||
*/
|
||||
const CalendarCell = memo(({ date, dateData, previousData, isSelected, isToday, isWeekend, onClick }) => {
|
||||
const CalendarCell = memo(({ date, ztData, eventCount, previousZtData, isSelected, isToday, isWeekend, onClick }) => {
|
||||
if (!date) {
|
||||
return <Box />;
|
||||
return <Box minH="60px" />;
|
||||
}
|
||||
|
||||
const hasData = !!dateData;
|
||||
const count = dateData?.count || 0;
|
||||
const heatColors = getHeatColor(count);
|
||||
const topSector = dateData?.top_sector || '';
|
||||
const hasZtData = !!ztData;
|
||||
const hasEventData = eventCount > 0;
|
||||
const ztCount = ztData?.count || 0;
|
||||
const heatColors = getHeatColor(ztCount);
|
||||
const topSector = ztData?.top_sector || '';
|
||||
|
||||
// 周末无数据显示"休市"
|
||||
if (isWeekend && !hasData) {
|
||||
if (isWeekend && !hasZtData && !hasEventData) {
|
||||
return (
|
||||
<Box
|
||||
p={1.5}
|
||||
borderRadius="10px"
|
||||
p={1}
|
||||
borderRadius="8px"
|
||||
bg="rgba(30, 30, 40, 0.3)"
|
||||
border="1px solid rgba(255, 255, 255, 0.03)"
|
||||
textAlign="center"
|
||||
minH="70px"
|
||||
minH="60px"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text fontSize="md" fontWeight="400" color="rgba(255, 255, 255, 0.25)">
|
||||
<Text fontSize="sm" fontWeight="400" color="rgba(255, 255, 255, 0.25)">
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
<Text fontSize="9px" color="rgba(255, 255, 255, 0.2)">
|
||||
@@ -173,34 +186,31 @@ const CalendarCell = memo(({ date, dateData, previousData, isSelected, isToday,
|
||||
label={
|
||||
<VStack spacing={1} align="start" p={1}>
|
||||
<Text fontWeight="bold">{`${date.getMonth() + 1}月${date.getDate()}日`}</Text>
|
||||
{hasData ? (
|
||||
<>
|
||||
<Text>涨停数: {count}家</Text>
|
||||
{topSector && <Text>主线板块: {topSector}</Text>}
|
||||
</>
|
||||
) : (
|
||||
<Text color="gray.400">暂无数据</Text>
|
||||
)}
|
||||
{hasZtData && <Text>涨停: {ztCount}家 {topSector && `| ${topSector}`}</Text>}
|
||||
{hasEventData && <Text>未来事件: {eventCount}个</Text>}
|
||||
{!hasZtData && !hasEventData && <Text color="gray.400">暂无数据</Text>}
|
||||
</VStack>
|
||||
}
|
||||
placement="top"
|
||||
hasArrow
|
||||
bg="rgba(15, 15, 22, 0.95)"
|
||||
border="1px solid rgba(212, 175, 55, 0.3)"
|
||||
borderRadius="12px"
|
||||
borderRadius="10px"
|
||||
>
|
||||
<Box
|
||||
as="button"
|
||||
p={1.5}
|
||||
borderRadius="10px"
|
||||
bg={hasData ? heatColors.bg : 'rgba(40, 40, 50, 0.3)'}
|
||||
p={1}
|
||||
borderRadius="8px"
|
||||
bg={hasZtData ? heatColors.bg : hasEventData ? 'rgba(34, 197, 94, 0.2)' : 'rgba(40, 40, 50, 0.3)'}
|
||||
border={
|
||||
isSelected
|
||||
? `2px solid ${goldColors.primary}`
|
||||
: isToday
|
||||
? `2px solid ${goldColors.light}`
|
||||
: hasData
|
||||
: hasZtData
|
||||
? `1px solid ${heatColors.border}`
|
||||
: hasEventData
|
||||
? '1px solid rgba(34, 197, 94, 0.4)'
|
||||
: '1px solid rgba(255, 255, 255, 0.08)'
|
||||
}
|
||||
boxShadow={isSelected ? `0 0 12px ${goldColors.glow}` : isToday ? `0 0 8px ${goldColors.glow}` : 'none'}
|
||||
@@ -214,28 +224,28 @@ const CalendarCell = memo(({ date, dateData, previousData, isSelected, isToday,
|
||||
}}
|
||||
onClick={() => onClick && onClick(date)}
|
||||
w="full"
|
||||
minH="70px"
|
||||
minH="60px"
|
||||
>
|
||||
{/* 今天标记 */}
|
||||
{isToday && (
|
||||
<Badge
|
||||
position="absolute"
|
||||
top="2px"
|
||||
right="2px"
|
||||
top="1px"
|
||||
right="1px"
|
||||
bg="rgba(239, 68, 68, 0.9)"
|
||||
color="white"
|
||||
fontSize="8px"
|
||||
px={1}
|
||||
fontSize="7px"
|
||||
px={0.5}
|
||||
borderRadius="sm"
|
||||
>
|
||||
今天
|
||||
今
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<VStack spacing={0.5} align="center">
|
||||
<VStack spacing={0} align="center">
|
||||
{/* 日期 */}
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontSize="sm"
|
||||
fontWeight={isSelected || isToday ? 'bold' : '500'}
|
||||
color={isSelected ? goldColors.primary : isToday ? goldColors.light : textColors.primary}
|
||||
>
|
||||
@@ -243,23 +253,31 @@ const CalendarCell = memo(({ date, dateData, previousData, isSelected, isToday,
|
||||
</Text>
|
||||
|
||||
{/* 涨停数 + 趋势 */}
|
||||
{hasData && (
|
||||
{hasZtData && (
|
||||
<HStack spacing={0.5} justify="center">
|
||||
<Text fontSize="xs" fontWeight="bold" color={heatColors.text}>
|
||||
{count}家
|
||||
<Icon as={Flame} boxSize={2.5} color={heatColors.text} />
|
||||
<Text fontSize="10px" fontWeight="bold" color={heatColors.text}>
|
||||
{ztCount}
|
||||
</Text>
|
||||
<TrendIcon current={ztCount} previous={previousZtData?.count} />
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 事件数 */}
|
||||
{hasEventData && (
|
||||
<HStack spacing={0.5} justify="center">
|
||||
<Icon as={FileText} boxSize={2.5} color="#22c55e" />
|
||||
<Text fontSize="10px" fontWeight="bold" color="#22c55e">
|
||||
{eventCount}
|
||||
</Text>
|
||||
<TrendIcon current={count} previous={previousData?.count} />
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 主要板块 */}
|
||||
{hasData && topSector && (
|
||||
<HStack spacing={0.5}>
|
||||
{count >= 80 && <Icon as={Flame} boxSize={2.5} color="#f97316" />}
|
||||
<Text fontSize="9px" color={textColors.secondary} noOfLines={1} maxW="65px">
|
||||
{hasZtData && topSector && (
|
||||
<Text fontSize="8px" color={textColors.secondary} noOfLines={1} maxW="55px">
|
||||
{topSector}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
@@ -270,35 +288,254 @@ const CalendarCell = memo(({ date, dateData, previousData, isSelected, isToday,
|
||||
CalendarCell.displayName = 'CalendarCell';
|
||||
|
||||
/**
|
||||
* 涨停日历组件 - 大尺寸版本
|
||||
* 左侧详情面板 - 显示选中日期的详细信息
|
||||
*/
|
||||
const LimitUpCalendar = () => {
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
const [datesData, setDatesData] = useState([]);
|
||||
const [dailyDetails, setDailyDetails] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const DetailPanel = memo(({ selectedDate, ztDetail, events, loading }) => {
|
||||
const dateStr = selectedDate ? `${selectedDate.getMonth() + 1}月${selectedDate.getDate()}日` : '请选择日期';
|
||||
const isPastDate = selectedDate && selectedDate < new Date(new Date().setHours(0, 0, 0, 0));
|
||||
|
||||
// 加载 dates.json
|
||||
// 板块数据处理
|
||||
const sectorList = useMemo(() => {
|
||||
if (!ztDetail?.sector_data) return [];
|
||||
return Object.entries(ztDetail.sector_data)
|
||||
.map(([name, data]) => ({ name, count: data.count, stocks: data.stock_codes || [] }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 12);
|
||||
}, [ztDetail]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
w="280px"
|
||||
minW="280px"
|
||||
bg="rgba(15, 15, 22, 0.8)"
|
||||
backdropFilter={GLASS_BLUR.sm}
|
||||
borderRadius="12px"
|
||||
border="1px solid rgba(212, 175, 55, 0.15)"
|
||||
p={3}
|
||||
h="100%"
|
||||
>
|
||||
{/* 日期标题 */}
|
||||
<HStack justify="space-between" mb={3}>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Calendar} boxSize={4} color={goldColors.primary} />
|
||||
<Text fontSize="md" fontWeight="bold" color={textColors.primary}>
|
||||
{dateStr}
|
||||
</Text>
|
||||
</HStack>
|
||||
{selectedDate && (
|
||||
<Badge
|
||||
colorScheme={isPastDate ? 'purple' : 'green'}
|
||||
variant="subtle"
|
||||
fontSize="10px"
|
||||
>
|
||||
{isPastDate ? '历史' : '未来'}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{!selectedDate ? (
|
||||
<Center h="200px">
|
||||
<VStack spacing={2}>
|
||||
<Icon as={Calendar} boxSize={8} color={textColors.muted} />
|
||||
<Text color={textColors.muted} fontSize="sm">点击日历选择日期</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : loading ? (
|
||||
<Center h="200px">
|
||||
<Spinner size="lg" color={goldColors.primary} />
|
||||
</Center>
|
||||
) : (
|
||||
<Tabs variant="soft-rounded" colorScheme="yellow" size="sm">
|
||||
<TabList mb={2}>
|
||||
<Tab
|
||||
fontSize="xs"
|
||||
_selected={{ bg: 'rgba(212, 175, 55, 0.3)', color: goldColors.primary }}
|
||||
isDisabled={!ztDetail}
|
||||
>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Flame} boxSize={3} />
|
||||
<Text>涨停 {ztDetail?.total_stocks || 0}</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
<Tab
|
||||
fontSize="xs"
|
||||
_selected={{ bg: 'rgba(34, 197, 94, 0.3)', color: '#22c55e' }}
|
||||
isDisabled={!events?.length}
|
||||
>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={FileText} boxSize={3} />
|
||||
<Text>事件 {events?.length || 0}</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* 涨停分类 Tab */}
|
||||
<TabPanel p={0}>
|
||||
{sectorList.length > 0 ? (
|
||||
<VStack spacing={2} align="stretch" maxH="280px" overflowY="auto" pr={1}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': { width: '4px' },
|
||||
'&::-webkit-scrollbar-track': { background: 'rgba(255,255,255,0.05)' },
|
||||
'&::-webkit-scrollbar-thumb': { background: 'rgba(212,175,55,0.3)', borderRadius: '4px' },
|
||||
}}
|
||||
>
|
||||
{sectorList.map((sector, idx) => (
|
||||
<HStack
|
||||
key={sector.name}
|
||||
justify="space-between"
|
||||
p={2}
|
||||
bg="rgba(255, 255, 255, 0.03)"
|
||||
borderRadius="md"
|
||||
border="1px solid rgba(255, 255, 255, 0.05)"
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.06)' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Text
|
||||
fontSize="10px"
|
||||
color={idx < 3 ? goldColors.primary : textColors.muted}
|
||||
fontWeight="bold"
|
||||
w="16px"
|
||||
>
|
||||
{idx + 1}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={textColors.secondary} noOfLines={1} maxW="120px">
|
||||
{sector.name}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge
|
||||
bg={getHeatColor(sector.count * 5).bg}
|
||||
color={getHeatColor(sector.count * 5).text}
|
||||
fontSize="10px"
|
||||
px={1.5}
|
||||
borderRadius="full"
|
||||
>
|
||||
{sector.count}家
|
||||
</Badge>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<Center h="100px">
|
||||
<Text color={textColors.muted} fontSize="sm">暂无涨停数据</Text>
|
||||
</Center>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* 未来事件 Tab */}
|
||||
<TabPanel p={0}>
|
||||
{events?.length > 0 ? (
|
||||
<VStack spacing={2} align="stretch" maxH="280px" overflowY="auto" pr={1}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': { width: '4px' },
|
||||
'&::-webkit-scrollbar-track': { background: 'rgba(255,255,255,0.05)' },
|
||||
'&::-webkit-scrollbar-thumb': { background: 'rgba(34,197,94,0.3)', borderRadius: '4px' },
|
||||
}}
|
||||
>
|
||||
{events.slice(0, 10).map((event, idx) => (
|
||||
<Box
|
||||
key={event.id || idx}
|
||||
p={2}
|
||||
bg="rgba(255, 255, 255, 0.03)"
|
||||
borderRadius="md"
|
||||
border="1px solid rgba(255, 255, 255, 0.05)"
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.06)' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<HStack justify="space-between" mb={1}>
|
||||
<HStack spacing={1}>
|
||||
{[...Array(event.star || 0)].map((_, i) => (
|
||||
<Icon key={i} as={Star} boxSize={2.5} color="yellow.400" fill="yellow.400" />
|
||||
))}
|
||||
</HStack>
|
||||
<Text fontSize="9px" color={textColors.muted}>
|
||||
{dayjs(event.calendar_time).format('HH:mm')}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={textColors.secondary} noOfLines={2}>
|
||||
{event.title}
|
||||
</Text>
|
||||
{event.related_stocks?.length > 0 && (
|
||||
<Text fontSize="9px" color="#22c55e" mt={1}>
|
||||
{event.related_stocks.length}只相关股票
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<Center h="100px">
|
||||
<Text color={textColors.muted} fontSize="sm">暂无事件数据</Text>
|
||||
</Center>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
DetailPanel.displayName = 'DetailPanel';
|
||||
|
||||
/**
|
||||
* 综合日历组件
|
||||
*/
|
||||
const CombinedCalendar = () => {
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(null);
|
||||
|
||||
// 涨停数据
|
||||
const [ztDatesData, setZtDatesData] = useState([]);
|
||||
const [ztDailyDetails, setZtDailyDetails] = useState({});
|
||||
const [selectedZtDetail, setSelectedZtDetail] = useState(null);
|
||||
|
||||
// 投资日历数据
|
||||
const [eventCounts, setEventCounts] = useState([]);
|
||||
const [selectedEvents, setSelectedEvents] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
// 加载涨停 dates.json
|
||||
useEffect(() => {
|
||||
const loadDatesData = async () => {
|
||||
const loadZtDatesData = async () => {
|
||||
try {
|
||||
const response = await fetch('/data/zt/dates.json');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDatesData(data.dates || []);
|
||||
setZtDatesData(data.dates || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load dates.json:', error);
|
||||
console.error('Failed to load zt dates.json:', error);
|
||||
}
|
||||
};
|
||||
loadZtDatesData();
|
||||
}, []);
|
||||
|
||||
// 加载投资日历事件数量
|
||||
useEffect(() => {
|
||||
const loadEventCounts = async () => {
|
||||
try {
|
||||
const year = currentMonth.getFullYear();
|
||||
const month = currentMonth.getMonth() + 1;
|
||||
const response = await eventService.calendar.getEventCounts(year, month);
|
||||
if (response.success) {
|
||||
setEventCounts(response.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load event counts:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadDatesData();
|
||||
}, []);
|
||||
loadEventCounts();
|
||||
}, [currentMonth]);
|
||||
|
||||
// 获取当月需要的日期详情(主要板块)
|
||||
// 获取当月涨停板块详情
|
||||
useEffect(() => {
|
||||
const loadMonthDetails = async () => {
|
||||
const loadMonthZtDetails = async () => {
|
||||
const year = currentMonth.getFullYear();
|
||||
const month = currentMonth.getMonth();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
@@ -308,15 +545,13 @@ const LimitUpCalendar = () => {
|
||||
|
||||
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]) {
|
||||
const hasData = ztDatesData.some(d => d.date === dateStr);
|
||||
if (hasData && !ztDailyDetails[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]) => {
|
||||
@@ -325,7 +560,7 @@ const LimitUpCalendar = () => {
|
||||
maxSector = sector;
|
||||
}
|
||||
});
|
||||
details[dateStr] = { top_sector: maxSector };
|
||||
details[dateStr] = { top_sector: maxSector, fullData: data };
|
||||
}
|
||||
})
|
||||
.catch(() => null)
|
||||
@@ -335,20 +570,20 @@ const LimitUpCalendar = () => {
|
||||
|
||||
if (promises.length > 0) {
|
||||
await Promise.all(promises);
|
||||
setDailyDetails(prev => ({ ...prev, ...details }));
|
||||
setZtDailyDetails(prev => ({ ...prev, ...details }));
|
||||
}
|
||||
};
|
||||
|
||||
if (datesData.length > 0) {
|
||||
loadMonthDetails();
|
||||
if (ztDatesData.length > 0) {
|
||||
loadMonthZtDetails();
|
||||
}
|
||||
}, [currentMonth, datesData]);
|
||||
}, [currentMonth, ztDatesData]);
|
||||
|
||||
// 构建日期数据映射
|
||||
const dateDataMap = useMemo(() => {
|
||||
const ztDataMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
datesData.forEach(d => {
|
||||
const detail = dailyDetails[d.date] || {};
|
||||
ztDatesData.forEach(d => {
|
||||
const detail = ztDailyDetails[d.date] || {};
|
||||
map.set(d.date, {
|
||||
date: d.date,
|
||||
count: d.count,
|
||||
@@ -356,7 +591,15 @@ const LimitUpCalendar = () => {
|
||||
});
|
||||
});
|
||||
return map;
|
||||
}, [datesData, dailyDetails]);
|
||||
}, [ztDatesData, ztDailyDetails]);
|
||||
|
||||
const eventCountMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
eventCounts.forEach(d => {
|
||||
map.set(d.date, d.count);
|
||||
});
|
||||
return map;
|
||||
}, [eventCounts]);
|
||||
|
||||
// 生成日历天数
|
||||
const days = useMemo(() => {
|
||||
@@ -368,11 +611,9 @@ const LimitUpCalendar = () => {
|
||||
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));
|
||||
}
|
||||
@@ -383,31 +624,81 @@ const LimitUpCalendar = () => {
|
||||
const calendarCellsData = useMemo(() => {
|
||||
const today = new Date();
|
||||
const todayStr = today.toDateString();
|
||||
const selectedDateStr = selectedDate?.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 ztDateStr = formatDateStr(date);
|
||||
const eventDateStr = dayjs(date).format('YYYY-MM-DD');
|
||||
const ztData = ztDataMap.get(ztDateStr) || null;
|
||||
const eventCount = eventCountMap.get(eventDateStr) || 0;
|
||||
const dayOfWeek = date.getDay();
|
||||
|
||||
// 获取前一天数据
|
||||
const prevDate = new Date(date);
|
||||
prevDate.setDate(prevDate.getDate() - 1);
|
||||
const previousData = dateDataMap.get(formatDateStr(prevDate)) || null;
|
||||
const previousZtData = ztDataMap.get(formatDateStr(prevDate)) || null;
|
||||
|
||||
return {
|
||||
key: dateStr || `day-${index}`,
|
||||
key: ztDateStr || `day-${index}`,
|
||||
date,
|
||||
dateData,
|
||||
previousData,
|
||||
ztData,
|
||||
eventCount,
|
||||
previousZtData,
|
||||
isSelected: date.toDateString() === selectedDateStr,
|
||||
isToday: date.toDateString() === todayStr,
|
||||
isWeekend: dayOfWeek === 0 || dayOfWeek === 6,
|
||||
};
|
||||
});
|
||||
}, [days, dateDataMap]);
|
||||
}, [days, ztDataMap, eventCountMap, selectedDate]);
|
||||
|
||||
// 处理日期点击
|
||||
const handleDateClick = useCallback(async (date) => {
|
||||
setSelectedDate(date);
|
||||
setDetailLoading(true);
|
||||
|
||||
const ztDateStr = formatDateStr(date);
|
||||
const eventDateStr = dayjs(date).format('YYYY-MM-DD');
|
||||
|
||||
// 加载涨停详情
|
||||
const detail = ztDailyDetails[ztDateStr];
|
||||
if (detail?.fullData) {
|
||||
setSelectedZtDetail(detail.fullData);
|
||||
} else {
|
||||
// 尝试加载
|
||||
try {
|
||||
const response = await fetch(`/data/zt/daily/${ztDateStr}.json`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSelectedZtDetail(data);
|
||||
setZtDailyDetails(prev => ({
|
||||
...prev,
|
||||
[ztDateStr]: { ...prev[ztDateStr], fullData: data }
|
||||
}));
|
||||
} else {
|
||||
setSelectedZtDetail(null);
|
||||
}
|
||||
} catch {
|
||||
setSelectedZtDetail(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载事件详情
|
||||
try {
|
||||
const response = await eventService.calendar.getEventsForDate(eventDateStr);
|
||||
if (response.success) {
|
||||
setSelectedEvents(response.data || []);
|
||||
} else {
|
||||
setSelectedEvents([]);
|
||||
}
|
||||
} catch {
|
||||
setSelectedEvents([]);
|
||||
}
|
||||
|
||||
setDetailLoading(false);
|
||||
}, [ztDailyDetails]);
|
||||
|
||||
// 月份导航
|
||||
const handlePrevMonth = useCallback(() => {
|
||||
@@ -419,7 +710,18 @@ const LimitUpCalendar = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HStack spacing={4} align="stretch">
|
||||
{/* 左侧详情面板 */}
|
||||
<DetailPanel
|
||||
selectedDate={selectedDate}
|
||||
ztDetail={selectedZtDetail}
|
||||
events={selectedEvents}
|
||||
loading={detailLoading}
|
||||
/>
|
||||
|
||||
{/* 右侧日历 */}
|
||||
<Box
|
||||
flex="1"
|
||||
bg="rgba(15, 15, 22, 0.6)"
|
||||
backdropFilter={GLASS_BLUR.md}
|
||||
borderRadius="16px"
|
||||
@@ -427,7 +729,6 @@ const LimitUpCalendar = () => {
|
||||
p={4}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
flex="1"
|
||||
>
|
||||
{/* 顶部装饰条 */}
|
||||
<Box
|
||||
@@ -454,12 +755,9 @@ const LimitUpCalendar = () => {
|
||||
/>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Calendar} boxSize={4} color={goldColors.primary} />
|
||||
<Text fontSize="lg" fontWeight="bold" color={textColors.primary}>
|
||||
<Text fontSize="md" 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} />}
|
||||
@@ -473,7 +771,7 @@ const LimitUpCalendar = () => {
|
||||
</HStack>
|
||||
|
||||
{/* 星期标题 */}
|
||||
<SimpleGrid columns={7} spacing={1.5} mb={2}>
|
||||
<SimpleGrid columns={7} spacing={1} mb={2}>
|
||||
{WEEK_DAYS.map((day, idx) => (
|
||||
<Text
|
||||
key={day}
|
||||
@@ -493,36 +791,44 @@ const LimitUpCalendar = () => {
|
||||
<Spinner size="lg" color={goldColors.primary} />
|
||||
</Center>
|
||||
) : (
|
||||
<SimpleGrid columns={7} spacing={1.5}>
|
||||
<SimpleGrid columns={7} spacing={1}>
|
||||
{calendarCellsData.map((cellData) => (
|
||||
<CalendarCell
|
||||
key={cellData.key}
|
||||
date={cellData.date}
|
||||
dateData={cellData.dateData}
|
||||
previousData={cellData.previousData}
|
||||
isSelected={false}
|
||||
ztData={cellData.ztData}
|
||||
eventCount={cellData.eventCount}
|
||||
previousZtData={cellData.previousZtData}
|
||||
isSelected={cellData.isSelected}
|
||||
isToday={cellData.isToday}
|
||||
isWeekend={cellData.isWeekend}
|
||||
onClick={handleDateClick}
|
||||
/>
|
||||
))}
|
||||
</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 spacing={3} mt={3} justify="center" flexWrap="wrap">
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Flame} boxSize={3} color="purple.400" />
|
||||
<Text fontSize="9px" color={textColors.muted}>涨停≥80</Text>
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Flame} boxSize={3} color="red.400" />
|
||||
<Text fontSize="9px" color={textColors.muted}>涨停≥60</Text>
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Flame} boxSize={3} color="orange.400" />
|
||||
<Text fontSize="9px" color={textColors.muted}>涨停≥40</Text>
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={FileText} boxSize={3} color="#22c55e" />
|
||||
<Text fontSize="9px" color={textColors.muted}>未来事件</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</HStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -534,7 +840,6 @@ const InfoModal = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 触发按钮 */}
|
||||
<HStack
|
||||
spacing={1.5}
|
||||
px={2.5}
|
||||
@@ -557,36 +862,22 @@ const InfoModal = () => {
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 弹窗 */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered motionPreset="slideInBottom">
|
||||
<ModalOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
|
||||
<ModalContent
|
||||
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.3)"
|
||||
borderRadius="2xl"
|
||||
boxShadow="0 25px 80px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,215,0,0.15), inset 0 1px 0 rgba(255,255,255,0.1)"
|
||||
boxShadow="0 25px 80px rgba(0,0,0,0.8)"
|
||||
maxW="500px"
|
||||
mx={4}
|
||||
>
|
||||
<ModalHeader
|
||||
borderBottom="1px solid rgba(255,215,0,0.2)"
|
||||
pb={4}
|
||||
>
|
||||
<ModalHeader borderBottom="1px solid rgba(255,215,0,0.2)" pb={4}>
|
||||
<HStack spacing={3}>
|
||||
<Box
|
||||
p={2}
|
||||
bg="rgba(255,215,0,0.15)"
|
||||
borderRadius="lg"
|
||||
border="1px solid rgba(255,215,0,0.3)"
|
||||
>
|
||||
<Box p={2} bg="rgba(255,215,0,0.15)" borderRadius="lg" border="1px solid rgba(255,215,0,0.3)">
|
||||
<Icon as={Info} color="gold" boxSize={5} />
|
||||
</Box>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
bgGradient="linear(to-r, #FFD700, #FFA500)"
|
||||
bgClip="text"
|
||||
>
|
||||
<Text fontSize="lg" fontWeight="bold" bgGradient="linear(to-r, #FFD700, #FFA500)" bgClip="text">
|
||||
事件中心使用指南
|
||||
</Text>
|
||||
</HStack>
|
||||
@@ -594,94 +885,33 @@ const InfoModal = () => {
|
||||
<ModalCloseButton color="whiteAlpha.600" _hover={{ color: 'white' }} />
|
||||
|
||||
<ModalBody py={5} px={6}>
|
||||
<VStack align="stretch" spacing={5}>
|
||||
{/* 1. SABC重要度说明 */}
|
||||
<VStack align="stretch" spacing={4}>
|
||||
<Box>
|
||||
<HStack mb={2}>
|
||||
<Text fontSize="sm" fontWeight="bold" color="whiteAlpha.900">
|
||||
1️⃣ 重要度等级 (SABC)
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box pl={4} borderLeft="3px solid rgba(255,215,0,0.4)" py={2}>
|
||||
<Text fontSize="sm" fontWeight="bold" color="whiteAlpha.900" mb={2}>📅 综合日历</Text>
|
||||
<Text fontSize="13px" color="whiteAlpha.800" lineHeight="1.7">
|
||||
重要度由<Text as="span" color="cyan.300" fontWeight="bold"> AI大模型 </Text>
|
||||
基于<Text as="span" color="gold" fontWeight="bold">事件本身的影响范围和重大程度</Text>来判定,
|
||||
<Text as="span" color="orange.300" fontWeight="bold">并非收益率预测策略</Text>。
|
||||
S级表示影响范围广、关注度高的重大事件,C级表示影响较小的普通事件。
|
||||
日历同时展示<Text as="span" color="purple.300" fontWeight="bold">历史涨停数据</Text>和
|
||||
<Text as="span" color="green.300" fontWeight="bold">未来投资事件</Text>,
|
||||
点击日期可查看详情。
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 2. 利好利空并存说明 */}
|
||||
<Box>
|
||||
<HStack mb={2}>
|
||||
<Text fontSize="sm" fontWeight="bold" color="whiteAlpha.900">
|
||||
2️⃣ 事件筛选机制
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box pl={4} borderLeft="3px solid rgba(255,215,0,0.4)" py={2}>
|
||||
<Text fontSize="sm" fontWeight="bold" color="whiteAlpha.900" mb={2}>🔥 涨停板块</Text>
|
||||
<Text fontSize="13px" color="whiteAlpha.800" lineHeight="1.7">
|
||||
事件列表中<Text as="span" color="#ef5350" fontWeight="bold">利好</Text>和
|
||||
<Text as="span" color="#26a69a" fontWeight="bold">利空</Text>并存,需自行判断。
|
||||
建议在<Text as="span" color="purple.300" fontWeight="bold">「历史相关事件」</Text>中查看:
|
||||
</Text>
|
||||
<VStack align="stretch" spacing={1} mt={2} pl={3}>
|
||||
<Text fontSize="12px" color="whiteAlpha.600">• 历史上类似事件的市场反应</Text>
|
||||
<Text fontSize="12px" color="whiteAlpha.600">• 事件的超预期程度</Text>
|
||||
<Text fontSize="12px" color="whiteAlpha.600">• 综合判断事件的投资价值</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 3. 数据延迟提醒 */}
|
||||
<Box
|
||||
p={4}
|
||||
bg="rgba(255,100,0,0.1)"
|
||||
border="1px solid rgba(255,100,0,0.3)"
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack mb={2}>
|
||||
<Icon as={Clock} color="orange.400" boxSize={5} />
|
||||
<Text fontSize="sm" fontWeight="bold" color="orange.300">
|
||||
3️⃣ 数据延迟提醒
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="13px" color="whiteAlpha.800" lineHeight="1.7">
|
||||
由于模型需要通过算法检索和分析历史数据,事件结果和发生时间会有
|
||||
<Text as="span" color="orange.300" fontWeight="bold"> 2-3分钟 </Text>左右的延迟。
|
||||
<Text as="span" color="#ef5350" fontWeight="bold"> 切勿追高!</Text>
|
||||
点击历史日期,左侧显示当日涨停板块分类,帮助理解市场主线。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 4. 盘前新闻经验 */}
|
||||
<Box
|
||||
p={4}
|
||||
bg="rgba(59,130,246,0.1)"
|
||||
border="1px solid rgba(59,130,246,0.3)"
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack mb={2}>
|
||||
<Icon as={AlertCircle} color="blue.400" boxSize={5} />
|
||||
<Text fontSize="sm" fontWeight="bold" color="blue.300">
|
||||
4️⃣ 实用经验分享
|
||||
</Text>
|
||||
</HStack>
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="bold" color="whiteAlpha.900" mb={2}>📊 未来事件</Text>
|
||||
<Text fontSize="13px" color="whiteAlpha.800" lineHeight="1.7">
|
||||
一个比较有效的经验是在
|
||||
<Text as="span" color="blue.300" fontWeight="bold"> 9:20左右 </Text>
|
||||
研究<Text as="span" color="gold" fontWeight="bold">上一交易日收盘后至盘前</Text>的新闻事件,
|
||||
往往能发现一些<Text as="span" color="cyan.300" fontWeight="bold">当日主线题材</Text>。
|
||||
点击未来日期,查看即将发生的投资事件,包括背景分析、未来推演、相关股票。
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 操作提示 */}
|
||||
<Box
|
||||
pt={3}
|
||||
mt={2}
|
||||
borderTop="1px solid rgba(255,215,0,0.2)"
|
||||
>
|
||||
<Box pt={2} borderTop="1px solid rgba(255,215,0,0.2)">
|
||||
<Text fontSize="12px" color="yellow.300" textAlign="center" fontWeight="medium">
|
||||
💡 点击事件卡片查看详情 · K线图支持滚轮缩放和拖动
|
||||
💡 颜色越深表示涨停数越多 · 绿色表示有未来事件
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
@@ -749,7 +979,6 @@ const HeroPanel = () => {
|
||||
</Text>
|
||||
</Heading>
|
||||
<InfoModal />
|
||||
{/* 交易状态 */}
|
||||
{isInTradingTime() && (
|
||||
<HStack
|
||||
spacing={1.5}
|
||||
@@ -775,8 +1004,8 @@ const HeroPanel = () => {
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 涨停日历 */}
|
||||
<LimitUpCalendar />
|
||||
{/* 综合日历 */}
|
||||
<CombinedCalendar />
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user