community增加事件详情

This commit is contained in:
2026-01-07 15:50:56 +08:00
parent 56c8de352d
commit 401762dfba

View File

@@ -1,16 +1,16 @@
/** /**
* FullCalendarPro - 炫酷黑金主题日历组件 * FullCalendarPro - 炫酷黑金主题日历组件
* 支持跨天事件条、动画效果、悬浮提示 * 支持跨天事件条、动画效果、悬浮提示
* 使用 dayCellDidMount 钩子实现完整的单元格自定义内容
*/ */
import React, { useMemo, useRef, useCallback } from 'react'; import React, { useMemo, useRef, useCallback, useEffect } from 'react';
import FullCalendar from '@fullcalendar/react'; import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid'; import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import type { EventInput, EventClickArg, DatesSetArg } from '@fullcalendar/core'; import type { EventInput, EventClickArg, DatesSetArg, DayCellMountArg } from '@fullcalendar/core';
import { Box, Text, HStack, VStack, Tooltip } from '@chakra-ui/react'; import { Box, Text, VStack, Tooltip } from '@chakra-ui/react';
import { keyframes } from '@emotion/react'; import { keyframes } from '@emotion/react';
import { Flame } from 'lucide-react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
// 动画定义 // 动画定义
@@ -170,11 +170,86 @@ const createEventInput = (event: { concept: string; startDate: string; endDate:
daysCount: event.dates.length, daysCount: event.dates.length,
gradient: color.bg, gradient: color.bg,
borderColor: color.border, borderColor: color.border,
textColor: color.text, // 确保文字颜色传递到 extendedProps textColor: color.text,
}, },
}; };
}; };
/**
* 创建自定义单元格内容的 HTML
*/
const createCellContentHTML = (
date: Date,
dateData: CalendarEventData | undefined,
isToday: boolean
): string => {
const dayNum = date.getDate();
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
const hasZtData = dateData && dateData.count > 0;
const hasEventCount = dateData?.eventCount && dateData.eventCount > 0;
const hasIndexChange = dateData?.indexChange !== undefined && dateData?.indexChange !== null;
// 日期颜色
const dateColor = isToday ? '#FFD700' : isWeekend ? '#FB923C' : '#FFFFFF';
const dateFontWeight = isToday ? 'bold' : '600';
// 上证涨跌幅
let indexChangeHTML = '';
if (hasIndexChange) {
const indexChange = dateData.indexChange!;
const indexColor = indexChange >= 0 ? '#EF4444' : '#22C55E';
const sign = indexChange >= 0 ? '+' : '';
indexChangeHTML = `<span style="font-size: 13px; font-weight: 700; color: ${indexColor};">${sign}${indexChange.toFixed(2)}%</span>`;
}
// 涨停数据
let ztDataHTML = '';
if (hasZtData) {
const ztColor = dateData.count >= 60 ? '#EF4444' : '#F59E0B';
ztDataHTML = `
<div style="display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 4px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="${ztColor}" stroke-width="2">
<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>
</svg>
<span style="font-size: 15px; font-weight: bold; color: ${ztColor};">${dateData.count}</span>
</div>
`;
}
// 未来事件计数
let eventCountHTML = '';
if (hasEventCount) {
eventCountHTML = `
<div style="display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 4px;">
<div style="
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(135deg, #22C55E 0%, #16A34A 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 6px rgba(34, 197, 94, 0.4);
">
<span style="font-size: 11px; font-weight: bold; color: white;">${dateData.eventCount}</span>
</div>
<span style="font-size: 13px; color: #22C55E; font-weight: 600;">事件</span>
</div>
`;
}
return `
<div class="fc-custom-cell-content" style="width: 100%; padding: 4px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<span style="font-size: 18px; font-weight: ${dateFontWeight}; color: ${dateColor};">${dayNum}</span>
${indexChangeHTML}
</div>
${ztDataHTML}
${eventCountHTML}
</div>
`;
};
/** /**
* FullCalendarPro 组件 * FullCalendarPro 组件
*/ */
@@ -187,6 +262,7 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
height = '600px', height = '600px',
}) => { }) => {
const calendarRef = useRef<FullCalendar>(null); const calendarRef = useRef<FullCalendar>(null);
const dataMapRef = useRef<Map<string, CalendarEventData>>(new Map());
// 将数据转换为事件 // 将数据转换为事件
const events = useMemo(() => mergeConsecutiveConcepts(data), [data]); const events = useMemo(() => mergeConsecutiveConcepts(data), [data]);
@@ -198,12 +274,17 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
return map; return map;
}, [data]); }, [data]);
// 同步 dataMap 到 ref供 dayCellDidMount 使用
useEffect(() => {
dataMapRef.current = dataMap;
}, [dataMap]);
// 处理日期点击 // 处理日期点击
const handleDateClick = useCallback((arg: { date: Date; dateStr: string }) => { const handleDateClick = useCallback((arg: { date: Date; dateStr: string }) => {
const dateStr = dayjs(arg.date).format('YYYYMMDD'); const dateStr = dayjs(arg.date).format('YYYYMMDD');
const dateData = dataMap.get(dateStr); const dateData = dataMapRef.current.get(dateStr);
onDateClick?.(arg.date, dateData); onDateClick?.(arg.date, dateData);
}, [dataMap, onDateClick]); }, [onDateClick]);
// 处理事件点击 // 处理事件点击
const handleEventClick = useCallback((arg: EventClickArg) => { const handleEventClick = useCallback((arg: EventClickArg) => {
@@ -224,77 +305,21 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
onMonthChange?.(visibleDate.getFullYear(), visibleDate.getMonth() + 1); onMonthChange?.(visibleDate.getFullYear(), visibleDate.getMonth() + 1);
}, [onMonthChange]); }, [onMonthChange]);
// 自定义日期单元格内容 - 优化布局,增大字体 // 单元格挂载时插入自定义内容
const dayCellContent = useCallback((arg: { date: Date; dayNumberText: string; isToday: boolean }) => { const handleDayCellDidMount = useCallback((arg: DayCellMountArg) => {
const dateStr = dayjs(arg.date).format('YYYYMMDD'); const { date, el, isToday } = arg;
const dateData = dataMap.get(dateStr); const dateStr = dayjs(date).format('YYYYMMDD');
const isWeekend = arg.date.getDay() === 0 || arg.date.getDay() === 6; const dateData = dataMapRef.current.get(dateStr);
const hasZtData = dateData && dateData.count > 0;
const hasEventCount = dateData?.eventCount && dateData.eventCount > 0;
const hasIndexChange = dateData?.indexChange !== undefined && dateData?.indexChange !== null;
return ( // 找到 day-top 容器并插入自定义内容
<Box position="relative" w="100%" h="100%" py={1}> const dayTop = el.querySelector('.fc-daygrid-day-top');
{/* 顶部:日期数字 + 上证涨跌幅 */} if (dayTop) {
<HStack justify="space-between" align="flex-start" px={1}> // 清空默认内容
<Text dayTop.innerHTML = '';
fontSize="xl" // 插入自定义内容
fontWeight={arg.isToday ? 'bold' : '600'} dayTop.innerHTML = createCellContentHTML(date, dateData, isToday);
color={arg.isToday ? '#FFD700' : isWeekend ? 'orange.300' : 'white'} }
> }, []);
{arg.date.getDate()}
</Text>
{/* 上证指数涨跌幅 - 右上角显示 */}
{hasIndexChange && (
<Text
fontSize="sm"
fontWeight="700"
color={dateData.indexChange! >= 0 ? '#EF4444' : '#22C55E'}
>
{dateData.indexChange! >= 0 ? '+' : ''}{dateData.indexChange!.toFixed(2)}%
</Text>
)}
</HStack>
{/* 中间区域:涨停数 + 事件数 */}
<VStack spacing={1} mt={1} align="stretch">
{/* 涨停数据 */}
{hasZtData && (
<HStack spacing={1.5} justify="center">
<Flame size={16} color={dateData.count >= 60 ? '#EF4444' : '#F59E0B'} />
<Text fontSize="md" fontWeight="bold" color={dateData.count >= 60 ? '#EF4444' : '#F59E0B'}>
{dateData.count}
</Text>
</HStack>
)}
{/* 未来事件计数 */}
{hasEventCount && (
<HStack spacing={1.5} justify="center">
<Box
w="18px"
h="18px"
borderRadius="full"
bg="linear-gradient(135deg, #22C55E 0%, #16A34A 100%)"
display="flex"
alignItems="center"
justifyContent="center"
boxShadow="0 0 6px rgba(34, 197, 94, 0.4)"
>
<Text fontSize="xs" fontWeight="bold" color="white">
{dateData.eventCount}
</Text>
</Box>
<Text fontSize="sm" color="#22C55E" fontWeight="600">
</Text>
</HStack>
)}
</VStack>
</Box>
);
}, [dataMap]);
// 自定义事件内容(跨天条) // 自定义事件内容(跨天条)
const eventContent = useCallback((arg: { event: { title: string; extendedProps: Record<string, unknown> } }) => { const eventContent = useCallback((arg: { event: { title: string; extendedProps: Record<string, unknown> } }) => {
@@ -371,6 +396,15 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
); );
}, []); }, []);
// 当数据变化时,需要重新渲染日历
useEffect(() => {
if (calendarRef.current) {
const api = calendarRef.current.getApi();
// 触发重新渲染
api.render();
}
}, [data]);
return ( return (
<Box <Box
height={height} height={height}
@@ -442,25 +476,18 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
minHeight: '110px', minHeight: '110px',
}, },
'.fc-daygrid-day-top': { '.fc-daygrid-day-top': {
// 保留容器,让 dayCellContent 正常显示
width: '100%', width: '100%',
padding: '0 !important', padding: '0 !important',
}, flexDirection: 'column',
// 隐藏默认的日期数字链接
'.fc-daygrid-day-number': {
display: 'none !important',
},
// dayCellContent 占满整个顶部区域
'.fc-daygrid-day-top > a': {
display: 'none !important', // 隐藏默认链接
},
'.fc-daygrid-day-top > div': {
width: '100%', // 让自定义内容占满宽度
}, },
// 非当月日期 // 非当月日期
'.fc-day-other': { '.fc-day-other': {
opacity: 0.4, opacity: 0.4,
}, },
// 自定义单元格内容样式
'.fc-custom-cell-content': {
width: '100%',
},
// 日期内容区域 // 日期内容区域
'.fc-daygrid-day-events': { '.fc-daygrid-day-events': {
marginTop: '0 !important', marginTop: '0 !important',
@@ -503,7 +530,7 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
dateClick={handleDateClick} dateClick={handleDateClick}
eventClick={handleEventClick} eventClick={handleEventClick}
datesSet={handleDatesSet} datesSet={handleDatesSet}
dayCellContent={dayCellContent} dayCellDidMount={handleDayCellDidMount}
eventContent={eventContent} eventContent={eventContent}
dayMaxEvents={3} dayMaxEvents={3}
moreLinkText={(n) => `+${n} 更多`} moreLinkText={(n) => `+${n} 更多`}