在日常开发中,日历组件是非常常见的需求,尤其是需要支持跨天事件展示 、事件数量限制 、日期选择校验等复杂场景时,普通的日历组件往往无法满足需求。本文将详细讲解如何基于 React + FullCalendar 打造一个支持单天 / 跨天事件、事件数量限制、日期权限控制的高性能日历组件。
一、需求分析与技术选型
核心需求
- 支持单天事件和跨天(多天)事件的统一展示
- 限制每日可添加的最大事件数量
- 禁止选择过去的日期添加事件
- 事件展示自定义样式(时间 + 标题)
- 支持新增 / 编辑事件的弹窗交互
- 中文本地化展示
技术栈选择
- 核心框架:React(Hooks 写法)
- 日历组件:FullCalendar(功能完善、可定制性强)
- 日期处理:date-fns(轻量级日期工具库)
- UI 组件:Ant Design(弹窗、提示等基础组件)
- 样式:CSS Modules(样式隔离)
二、核心实现步骤
1. 环境准备与依赖安装
首先安装所需依赖包:
bash
运行
# 核心依赖
npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction @fullcalendar/core
# 日期处理
npm install date-fns
# UI组件
npm install antd
# 类型支持(TS项目)
npm install @types/react --save-dev
2. 基础组件结构搭建
先定义核心的类型和基础配置,为后续开发打下基础:
tsx
import React, { useEffect, useState } from 'react';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import { format, addDays, isBefore, parse, isSameDay, startOfDay, endOfDay, isWithinInterval } from 'date-fns';
import zhCN from '@fullcalendar/core/locales/zh-cn';
import { Modal, message } from 'antd';
import 'antd/dist/reset.css';
import styles from './index.module.css';
// 事件类型定义
interface EventItem {
id: string;
sourceId: string;
title: string;
start: string;
end: string;
allDay: false;
backgroundColor: string;
borderColor: string;
textColor: string;
extendedProps: {
displayTime: string;
originalStart: string;
originalEnd: string;
repeatExecute: boolean;
};
}
const ExperimentalSchedule = () => {
// 模拟数据源(单天/跨天事件统一格式)
const eventSource = [
{
id: '1',
name: '第一个任务(单天)',
startTime: "2025-11-09 13:22:05",
endTime: "2025-11-09 15:38:05",
repeatExecute: false,
},
{
id: '6',
name: '第六个任务(跨天)',
startTime: "2025-11-13 22:49:05",
endTime: "2025-11-26 03:15:05",
repeatExecute: false,
},
// 更多测试数据...
];
// 颜色配置(用于区分不同事件)
const colorConfig = [
{ backgroundColor: '#3498db', textColor: 'white' },
{ backgroundColor: '#2ecc71', textColor: 'white' },
{ backgroundColor: '#f39c12', textColor: 'white' },
// 更多颜色...
];
// 状态管理
const [events, setEvents] = useState<EventItem[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAddMode, setIsAddMode] = useState(false);
const today = startOfDay(new Date()); // 今天0点
const MAX_EVENTS_PER_DAY = 4; // 每天最多事件数
// 后续核心逻辑...
};
export default ExperimentalSchedule;
3. 核心工具函数实现
(1)事件颜色分配
根据事件 ID 分配固定颜色,确保同一事件始终显示相同颜色:
tsx
// 生成事件颜色
const getEventColor = (id: string) => {
return colorConfig[parseInt(id) % colorConfig.length];
};
(2)时间格式化处理
统一格式化时间显示,解决跨天时间异常问题:
tsx
// 格式化时间(仅显示时分)
const formatTime = (timeStr: string) => {
const time = parse(timeStr, 'yyyy-MM-dd HH:mm:ss', new Date());
return format(time, 'HH:mm');
};
// 处理跨天/单天时间格式(FullCalendar标准格式)
const handleTimeFormat = (startStr: string, endStr: string) => {
const start = parse(startStr, 'yyyy-MM-dd HH:mm:ss', new Date());
const end = parse(endStr, 'yyyy-MM-dd HH:mm:ss', new Date());
// 跨天处理:如果结束时间早于开始时间,自动顺延1天
const realEnd = isBefore(end, start) ? addDays(end, 1) : end;
return {
start: format(start, "yyyy-MM-dd'T'HH:mm:ss"),
end: format(realEnd, "yyyy-MM-dd'T'HH:mm:ss"),
};
};
(3)日期事件数量统计
统计指定日期的事件数量,用于限制每日最大事件数:
tsx
// 统计目标日期已有的事件数
const countEventsOnDate = (targetDate: Date) => {
const targetDayStart = startOfDay(targetDate); // 目标日期的开始(00:00:00)
const targetDayEnd = endOfDay(targetDate); // 目标日期的结束(23:59:59)
return events.filter(event => {
const eventStart = parse(event.start, "yyyy-MM-dd'T'HH:mm:ss", new Date());
const eventEnd = parse(event.end, "yyyy-MM-dd'T'HH:mm:ss", new Date());
// 检查事件是否与目标日期有重叠
const eventOverlapsTargetDay =
// 事件在目标日期内开始
(isSameDay(eventStart, targetDate)) ||
// 事件在目标日期内结束
(isSameDay(eventEnd, targetDate)) ||
// 事件跨越目标日期
(isBefore(eventStart, targetDayStart) && isBefore(targetDayEnd, eventEnd)) ||
// 事件包含目标日期
(isWithinInterval(targetDayStart, { start: eventStart, end: eventEnd }) ||
isWithinInterval(targetDayEnd, { start: eventStart, end: eventEnd }));
return eventOverlapsTargetDay;
}).length;
};
4. 事件初始化与交互逻辑
(1)事件数据初始化
将原始数据源转换为 FullCalendar 可识别的格式:
tsx
// 初始化事件
useEffect(() => {
const formatEvents: EventItem[] = eventSource.map((item, index) => {
const { start, end } = handleTimeFormat(item.startTime, item.endTime);
const color = getEventColor(item.id);
return {
id: `${item.id}_${index}`, // 确保ID唯一
sourceId: item.id,
title: item.name,
start,
end,
allDay: false,
backgroundColor: color.backgroundColor,
borderColor: color.backgroundColor,
textColor: color.textColor,
extendedProps: {
displayTime: formatTime(item.startTime),
originalStart: item.startTime,
originalEnd: item.endTime,
repeatExecute: item.repeatExecute,
},
};
});
setEvents(formatEvents);
}, []);
(2)事件点击与日期选择逻辑
实现事件编辑、日期选择校验、事件数量限制等核心交互:
tsx
// 编辑事件(无日期限制)
const handleEventClick = () => {
setIsAddMode(false);
setIsModalOpen(true);
};
// 空白区域点击(新增事件)
const handleSelect = (selectInfo: any) => {
const selectedDate = startOfDay(selectInfo.start); // 选中日期(0点)
// 1. 检查是否是今天及以后
if (isBefore(selectedDate, today)) {
message.warning('不能选择比今天更早的日期添加事件');
selectInfo.view.calendar.unselect();
return;
}
// 2. 检查当天已有的事件数
const eventCount = countEventsOnDate(selectedDate);
if (eventCount >= MAX_EVENTS_PER_DAY) {
message.warning(`已达最大事件数${MAX_EVENTS_PER_DAY},无法继续添加`);
selectInfo.view.calendar.unselect();
return;
}
// 3. 满足条件,打开新增弹窗
setIsAddMode(true);
setIsModalOpen(true);
selectInfo.view.calendar.unselect();
};
// 关闭弹窗
const handleCancel = () => {
setIsModalOpen(false);
};
5. 自定义事件渲染与组件渲染
(1)自定义事件内容渲染
定制事件显示样式,展示时间和标题:
tsx
// 事件内容渲染
const renderEventContent = (eventInfo: any) => {
return (
<div
className={styles.eventContent}
style={{
background: eventInfo.event.backgroundColor,
color: eventInfo.event.textColor,
}}
>
<span className={styles.eventTime}>{eventInfo.event.extendedProps.displayTime}</span>
<span className={styles.eventTitle}>{eventInfo.event.title}</span>
</div>
);
};
(2)核心组件渲染
配置 FullCalendar 并渲染弹窗:
tsx
return (
<div className={styles.container}>
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
headerToolbar={{ left: "prev", center: "title", right: "today,next" }}
locale={zhCN} // 中文本地化
buttonText={{ today: '今天' }}
allDayText="全天"
firstDay={1} // 周一作为一周的第一天
slotLabelFormat={{ hour: '2-digit', minute: '2-digit', meridiem: false, hour12: false }}
height="auto"
contentHeight="auto"
aspectRatio={1.2}
events={events} // 事件数据源
eventClick={handleEventClick} // 事件点击
select={handleSelect} // 日期选择
selectable={true} // 允许选择日期
editable={true} // 允许编辑事件
eventDurationEditable={true} // 允许修改事件时长
dayMaxEvents={true} // 超出数量显示"+N more"
dayMaxEventRows={3} // 每天最多显示3行事件
slotMinTime="00:00:00" // 显示开始时间
slotMaxTime="24:00:00" // 显示结束时间
eventContent={renderEventContent} // 自定义事件渲染
/>
{/* 新增/编辑事件弹窗 */}
<Modal
title={isAddMode ? '新增事件' : '编辑事件'}
open={isModalOpen}
onCancel={handleCancel}
footer={[
<button className={styles.cancelBtn} onClick={handleCancel}>
关闭
</button>
]}
width={500}
maskClosable={false}
centered={true}
>
<div>
{isAddMode ? (
<p>新增事件表单(单天/多天事件统一填写,无需区分)</p>
) : (
<p>编辑事件表单(支持修改单天/多天事件信息)</p>
)}
</div>
</Modal>
</div>
);
6. 样式配置(index.module.css)
css
.container {
padding: 20px;
margin: 0 auto;
max-width: 1400px;
background-color: #fff;
}
/* 事件内容样式 */
.eventContent {
padding: 8px;
border-radius: 3px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
min-height: 24px;
line-height: 16px;
border: none;
display: flex;
align-items: center;
gap: 4px;
margin: 1px 0;
}
.eventTime {
font-size: 11px;
font-weight: bold;
opacity: 0.9;
min-width: 28px;
}
.eventTitle {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
/* FullCalendar 全局样式覆盖 */
:global(.fc-daygrid-day-frame) {
min-height: 120px !important;
height: auto !important;
}
:global(.fc-header-toolbar .fc-button:focus),
:global(.fc-button-group .fc-button:focus) {
outline: none !important;
box-shadow: none !important;
}
:global(.fc-col-header-cell-cushion),
:global(.fc-daygrid-day-number) {
color: #000000 !important;
}
:global(.fc-button-primary:not(:disabled).fc-button-active) {
background-color: #007bff !important;
border-color: #007bff !important;
}
三、关键功能亮点
1. 跨天事件处理
通过handleTimeFormat函数自动处理跨天时间异常,确保结束时间始终晚于开始时间。
2. 日期权限控制
- 禁止选择过去的日期添加事件
- 限制每日最大事件数量(可配置)
3. 高性能事件统计
通过countEventsOnDate函数精准统计指定日期的事件数量,支持跨天事件的日期归属判断。
4. 自定义事件样式
通过renderEventContent实现事件内容的个性化展示,区分时间和标题显示。
5. 中文本地化
完整的中文配置,包括星期、月份、按钮文字等。
四、扩展与优化建议
- 表单完善:新增 / 编辑弹窗中添加实际的表单控件(时间选择器、输入框等),实现事件的增删改查
- 性能优化:对于大量事件数据,可实现事件的懒加载和分页加载
- 拖拽优化:完善事件拖拽功能,支持跨天事件的拖拽调整
- 数据持久化:对接后端接口,实现事件数据的保存和读取
- 响应式适配:优化移动端显示效果,适配不同屏幕尺寸
五、总结
本文基于 React 和 FullCalendar 实现了一个功能完善的日历组件,核心亮点包括:
- 统一处理单天 / 跨天事件的展示逻辑,解决跨天时间异常问题
- 实现日期权限控制和事件数量限制,提升用户体验
- 自定义事件样式和交互逻辑,满足个性化需求
- 完整的中文本地化配置,符合国内使用习惯
该组件可以直接集成到 React 项目中,通过简单的配置和扩展即可满足大部分业务场景的日历需求。
关键点回顾
- 核心依赖:FullCalendar 提供日历基础能力,date-fns 处理日期逻辑,Ant Design 提供 UI 组件支持
- 核心逻辑:跨天事件时间格式化、日期事件数量统计、日期权限校验是实现的关键
- 扩展方向:完善表单交互、对接后端接口、优化性能是后续开发的重点