refactor(Concept): ConceptTimelineModal 迁移到 BaseCalendar
This commit is contained in:
@@ -2,11 +2,10 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { useConceptTimelineEvents } from './hooks/useConceptTimelineEvents';
|
import { useConceptTimelineEvents } from './hooks/useConceptTimelineEvents';
|
||||||
import RiskDisclaimer from '../../components/RiskDisclaimer';
|
import RiskDisclaimer from '../../components/RiskDisclaimer';
|
||||||
import FullCalendar from '@fullcalendar/react';
|
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
// 使用新的公共日历组件
|
||||||
|
import { BaseCalendar } from '@components/Calendar';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@@ -198,93 +197,19 @@ const ConceptTimelineModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 转换时间轴数据为日历事件格式(一天拆分为多个独立事件)
|
// 按日期索引的事件数据(用于日历单元格渲染)
|
||||||
const calendarEvents = useMemo(() => {
|
const eventsByDate = useMemo(() => {
|
||||||
const events = [];
|
const map = {};
|
||||||
|
|
||||||
timelineData.forEach(item => {
|
timelineData.forEach(item => {
|
||||||
const priceInfo = getPriceInfo(item.price);
|
map[item.date] = item;
|
||||||
const newsCount = (item.events || []).filter(e => e.type === 'news').length;
|
|
||||||
const reportCount = (item.events || []).filter(e => e.type === 'report').length;
|
|
||||||
const hasPriceData = item.price && item.price.avg_change_pct !== null;
|
|
||||||
|
|
||||||
// 如果有新闻,添加新闻事件
|
|
||||||
if (newsCount > 0) {
|
|
||||||
events.push({
|
|
||||||
id: `${item.date}-news`,
|
|
||||||
title: `📰 ${newsCount} 条新闻`,
|
|
||||||
date: item.date,
|
|
||||||
start: item.date,
|
|
||||||
backgroundColor: '#9F7AEA',
|
|
||||||
borderColor: '#9F7AEA',
|
|
||||||
extendedProps: {
|
|
||||||
eventType: 'news',
|
|
||||||
count: newsCount,
|
|
||||||
originalData: item,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有研报,添加研报事件
|
|
||||||
if (reportCount > 0) {
|
|
||||||
events.push({
|
|
||||||
id: `${item.date}-report`,
|
|
||||||
title: `📊 ${reportCount} 篇研报`,
|
|
||||||
date: item.date,
|
|
||||||
start: item.date,
|
|
||||||
backgroundColor: '#805AD5',
|
|
||||||
borderColor: '#805AD5',
|
|
||||||
extendedProps: {
|
|
||||||
eventType: 'report',
|
|
||||||
count: reportCount,
|
|
||||||
originalData: item,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有价格数据,添加价格事件
|
|
||||||
if (hasPriceData) {
|
|
||||||
const changePercent = item.price.avg_change_pct;
|
|
||||||
const isSignificantRise = changePercent >= 3; // 涨幅 >= 3% 为重大利好
|
|
||||||
let bgColor = '#e2e8f0';
|
|
||||||
let title = priceInfo.text;
|
|
||||||
|
|
||||||
if (priceInfo.color === 'red') {
|
|
||||||
if (isSignificantRise) {
|
|
||||||
// 涨幅 >= 3%,使用醒目的橙红色 + 火焰图标
|
|
||||||
bgColor = '#F56565'; // 更深的红色
|
|
||||||
title = `🔥 ${priceInfo.text}`;
|
|
||||||
} else {
|
|
||||||
bgColor = '#FC8181'; // 普通红色(上涨)
|
|
||||||
}
|
|
||||||
} else if (priceInfo.color === 'green') {
|
|
||||||
bgColor = '#68D391'; // 绿色(下跌)
|
|
||||||
}
|
|
||||||
|
|
||||||
events.push({
|
|
||||||
id: `${item.date}-price`,
|
|
||||||
title: title,
|
|
||||||
date: item.date,
|
|
||||||
start: item.date,
|
|
||||||
backgroundColor: bgColor,
|
|
||||||
borderColor: isSignificantRise ? '#C53030' : bgColor, // 深红色边框强调
|
|
||||||
extendedProps: {
|
|
||||||
eventType: 'price',
|
|
||||||
priceInfo,
|
|
||||||
originalData: item,
|
|
||||||
isSignificantRise, // 标记重大涨幅
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return map;
|
||||||
return events;
|
|
||||||
}, [timelineData]);
|
}, [timelineData]);
|
||||||
|
|
||||||
// 处理日期点击
|
// 处理日期选择(点击日期单元格)
|
||||||
const handleDateClick = (info) => {
|
const handleDateSelect = (date) => {
|
||||||
const clickedDate = info.dateStr;
|
const clickedDate = date.format('YYYY-MM-DD');
|
||||||
const dateData = timelineData.find(item => item.date === clickedDate);
|
const dateData = eventsByDate[clickedDate];
|
||||||
|
|
||||||
if (dateData) {
|
if (dateData) {
|
||||||
setSelectedDate(clickedDate);
|
setSelectedDate(clickedDate);
|
||||||
@@ -296,16 +221,128 @@ const ConceptTimelineModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理事件点击
|
// 自定义日期单元格内容渲染
|
||||||
const handleEventClick = (info) => {
|
const renderCellContent = (date) => {
|
||||||
// 从事件的 extendedProps 中获取原始数据
|
const dateStr = date.format('YYYY-MM-DD');
|
||||||
const dateData = info.event.extendedProps?.originalData;
|
const item = eventsByDate[dateStr];
|
||||||
|
|
||||||
if (dateData) {
|
if (!item) return null;
|
||||||
setSelectedDate(dateData.date);
|
|
||||||
setSelectedDateData(dateData);
|
const priceInfo = getPriceInfo(item.price);
|
||||||
onDateDetailOpen();
|
const newsCount = (item.events || []).filter(e => e.type === 'news').length;
|
||||||
|
const reportCount = (item.events || []).filter(e => e.type === 'report').length;
|
||||||
|
const hasPriceData = item.price && item.price.avg_change_pct !== null;
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
|
||||||
|
// 新闻事件
|
||||||
|
if (newsCount > 0) {
|
||||||
|
events.push(
|
||||||
|
<HStack
|
||||||
|
key="news"
|
||||||
|
spacing={1}
|
||||||
|
fontSize="10px"
|
||||||
|
color="#9F7AEA"
|
||||||
|
cursor="pointer"
|
||||||
|
px={1}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="sm"
|
||||||
|
bg="rgba(159, 122, 234, 0.2)"
|
||||||
|
_hover={{ bg: 'rgba(159, 122, 234, 0.3)' }}
|
||||||
|
w="100%"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<Text fontWeight="600" fontSize="10px" isTruncated flex="1" minW={0}>
|
||||||
|
📰 {newsCount} 条新闻
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 研报事件
|
||||||
|
if (reportCount > 0) {
|
||||||
|
events.push(
|
||||||
|
<HStack
|
||||||
|
key="report"
|
||||||
|
spacing={1}
|
||||||
|
fontSize="10px"
|
||||||
|
color="#805AD5"
|
||||||
|
cursor="pointer"
|
||||||
|
px={1}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="sm"
|
||||||
|
bg="rgba(128, 90, 213, 0.2)"
|
||||||
|
_hover={{ bg: 'rgba(128, 90, 213, 0.3)' }}
|
||||||
|
w="100%"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<Text fontWeight="600" fontSize="10px" isTruncated flex="1" minW={0}>
|
||||||
|
📊 {reportCount} 篇研报
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 涨跌数据
|
||||||
|
if (hasPriceData) {
|
||||||
|
const changePercent = item.price.avg_change_pct;
|
||||||
|
const isSignificantRise = changePercent >= 3;
|
||||||
|
let bgColor = 'rgba(226, 232, 240, 0.2)';
|
||||||
|
let textColor = '#e2e8f0';
|
||||||
|
let title = priceInfo.text;
|
||||||
|
|
||||||
|
if (priceInfo.color === 'red') {
|
||||||
|
if (isSignificantRise) {
|
||||||
|
bgColor = 'rgba(245, 101, 101, 0.3)';
|
||||||
|
textColor = '#F56565';
|
||||||
|
title = `🔥 ${priceInfo.text}`;
|
||||||
|
} else {
|
||||||
|
bgColor = 'rgba(252, 129, 129, 0.2)';
|
||||||
|
textColor = '#FC8181';
|
||||||
|
}
|
||||||
|
} else if (priceInfo.color === 'green') {
|
||||||
|
bgColor = 'rgba(104, 211, 145, 0.2)';
|
||||||
|
textColor = '#68D391';
|
||||||
|
}
|
||||||
|
|
||||||
|
events.push(
|
||||||
|
<HStack
|
||||||
|
key="price"
|
||||||
|
spacing={1}
|
||||||
|
fontSize="10px"
|
||||||
|
color={textColor}
|
||||||
|
cursor="pointer"
|
||||||
|
px={1}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="sm"
|
||||||
|
bg={bgColor}
|
||||||
|
border={isSignificantRise ? '1px solid #C53030' : 'none'}
|
||||||
|
_hover={{ opacity: 0.8 }}
|
||||||
|
w="100%"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold" fontSize="10px" isTruncated flex="1" minW={0}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最多显示 3 个事件,超出显示 "更多"
|
||||||
|
const maxDisplay = 3;
|
||||||
|
const displayEvents = events.slice(0, maxDisplay);
|
||||||
|
const remainingCount = events.length - maxDisplay;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={0.5} align="stretch" w="100%" mt={1}>
|
||||||
|
{displayEvents}
|
||||||
|
{remainingCount > 0 && (
|
||||||
|
<Text fontSize="9px" color="whiteAlpha.600" px={1}>
|
||||||
|
+{remainingCount} 更多
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取时间轴数据
|
// 获取时间轴数据
|
||||||
@@ -833,7 +870,7 @@ const ConceptTimelineModal = ({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* FullCalendar 日历组件 */}
|
{/* Ant Design 日历组件 */}
|
||||||
<Box
|
<Box
|
||||||
height={{ base: '500px', md: '700px' }}
|
height={{ base: '500px', md: '700px' }}
|
||||||
bg="rgba(15, 23, 42, 0.6)"
|
bg="rgba(15, 23, 42, 0.6)"
|
||||||
@@ -841,129 +878,12 @@ const ConceptTimelineModal = ({
|
|||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor="whiteAlpha.100"
|
borderColor="whiteAlpha.100"
|
||||||
p={{ base: 1, md: 4 }}
|
p={{ base: 1, md: 4 }}
|
||||||
sx={{
|
|
||||||
// FullCalendar 深色主题样式定制
|
|
||||||
'.fc': {
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
'.fc-header-toolbar': {
|
|
||||||
marginBottom: { base: '0.5rem', md: '1.5rem' },
|
|
||||||
padding: { base: '0 4px', md: '0' },
|
|
||||||
flexWrap: 'nowrap',
|
|
||||||
gap: { base: '4px', md: '8px' },
|
|
||||||
},
|
|
||||||
'.fc-toolbar-chunk': {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
'.fc-toolbar-title': {
|
|
||||||
fontSize: { base: '1rem', md: '1.5rem' },
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'white',
|
|
||||||
},
|
|
||||||
'.fc-button': {
|
|
||||||
backgroundColor: 'rgba(139, 92, 246, 0.6)',
|
|
||||||
borderColor: 'rgba(139, 92, 246, 0.8)',
|
|
||||||
color: 'white',
|
|
||||||
padding: { base: '4px 8px', md: '6px 12px' },
|
|
||||||
fontSize: { base: '12px', md: '14px' },
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(139, 92, 246, 0.8)',
|
|
||||||
borderColor: 'rgba(139, 92, 246, 1)',
|
|
||||||
},
|
|
||||||
'&:active, &:focus': {
|
|
||||||
backgroundColor: 'rgba(139, 92, 246, 1)',
|
|
||||||
borderColor: 'rgba(139, 92, 246, 1)',
|
|
||||||
boxShadow: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.fc-button-active': {
|
|
||||||
backgroundColor: 'rgba(139, 92, 246, 1)',
|
|
||||||
borderColor: 'rgba(139, 92, 246, 1)',
|
|
||||||
},
|
|
||||||
// 深色主题 - 表格边框和背景
|
|
||||||
'.fc-theme-standard td, .fc-theme-standard th': {
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
'.fc-theme-standard .fc-scrollgrid': {
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
},
|
|
||||||
'.fc-col-header-cell': {
|
|
||||||
backgroundColor: 'rgba(15, 23, 42, 0.8)',
|
|
||||||
},
|
|
||||||
'.fc-col-header-cell-cushion': {
|
|
||||||
color: 'rgba(255, 255, 255, 0.8)',
|
|
||||||
fontSize: { base: '0.75rem', md: '0.875rem' },
|
|
||||||
padding: { base: '4px 2px', md: '8px' },
|
|
||||||
},
|
|
||||||
'.fc-daygrid-day': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(139, 92, 246, 0.2)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.fc-daygrid-day-number': {
|
|
||||||
color: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
padding: { base: '2px', md: '4px' },
|
|
||||||
fontSize: { base: '0.75rem', md: '0.875rem' },
|
|
||||||
},
|
|
||||||
'.fc-day-today': {
|
|
||||||
backgroundColor: 'rgba(139, 92, 246, 0.15) !important',
|
|
||||||
},
|
|
||||||
'.fc-day-other .fc-daygrid-day-number': {
|
|
||||||
color: 'rgba(255, 255, 255, 0.4)',
|
|
||||||
},
|
|
||||||
'.fc-event': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
border: 'none',
|
|
||||||
padding: { base: '1px 2px', md: '2px 4px' },
|
|
||||||
fontSize: { base: '0.65rem', md: '0.75rem' },
|
|
||||||
fontWeight: 'bold',
|
|
||||||
borderRadius: '4px',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
'&:hover': {
|
|
||||||
transform: 'scale(1.05)',
|
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.fc-daygrid-event-harness': {
|
|
||||||
marginBottom: { base: '1px', md: '2px' },
|
|
||||||
},
|
|
||||||
'.fc-more-link': {
|
|
||||||
color: 'rgba(255, 255, 255, 0.8)',
|
|
||||||
},
|
|
||||||
// H5 端隐藏事件文字,只显示色块
|
|
||||||
'@media (max-width: 768px)': {
|
|
||||||
'.fc-event-title': {
|
|
||||||
fontSize: '0.6rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<FullCalendar
|
<BaseCalendar
|
||||||
plugins={[dayGridPlugin, interactionPlugin]}
|
onSelect={handleDateSelect}
|
||||||
initialView="dayGridMonth"
|
cellRender={renderCellContent}
|
||||||
locale="zh-cn"
|
|
||||||
headerToolbar={{
|
|
||||||
left: 'prev,next today',
|
|
||||||
center: 'title',
|
|
||||||
right: '',
|
|
||||||
}}
|
|
||||||
events={calendarEvents}
|
|
||||||
dateClick={handleDateClick}
|
|
||||||
eventClick={handleEventClick}
|
|
||||||
height="100%"
|
height="100%"
|
||||||
dayMaxEvents={3}
|
showToolbar={true}
|
||||||
moreLinkText="更多"
|
|
||||||
buttonText={{
|
|
||||||
today: '今天',
|
|
||||||
month: '月',
|
|
||||||
week: '周',
|
|
||||||
}}
|
|
||||||
eventDisplay="block"
|
|
||||||
displayEventTime={false}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user