React + FullCalendar 实现高性能跨天事件日历组件

在日常开发中,日历组件是非常常见的需求,尤其是需要支持跨天事件展示事件数量限制日期选择校验等复杂场景时,普通的日历组件往往无法满足需求。本文将详细讲解如何基于 React + FullCalendar 打造一个支持单天 / 跨天事件、事件数量限制、日期权限控制的高性能日历组件。

一、需求分析与技术选型

核心需求

  1. 支持单天事件和跨天(多天)事件的统一展示
  2. 限制每日可添加的最大事件数量
  3. 禁止选择过去的日期添加事件
  4. 事件展示自定义样式(时间 + 标题)
  5. 支持新增 / 编辑事件的弹窗交互
  6. 中文本地化展示

技术栈选择

  • 核心框架: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. 中文本地化

完整的中文配置,包括星期、月份、按钮文字等。

四、扩展与优化建议

  1. 表单完善:新增 / 编辑弹窗中添加实际的表单控件(时间选择器、输入框等),实现事件的增删改查
  2. 性能优化:对于大量事件数据,可实现事件的懒加载和分页加载
  3. 拖拽优化:完善事件拖拽功能,支持跨天事件的拖拽调整
  4. 数据持久化:对接后端接口,实现事件数据的保存和读取
  5. 响应式适配:优化移动端显示效果,适配不同屏幕尺寸

五、总结

本文基于 React 和 FullCalendar 实现了一个功能完善的日历组件,核心亮点包括:

  1. 统一处理单天 / 跨天事件的展示逻辑,解决跨天时间异常问题
  2. 实现日期权限控制和事件数量限制,提升用户体验
  3. 自定义事件样式和交互逻辑,满足个性化需求
  4. 完整的中文本地化配置,符合国内使用习惯

该组件可以直接集成到 React 项目中,通过简单的配置和扩展即可满足大部分业务场景的日历需求。

关键点回顾

  • 核心依赖:FullCalendar 提供日历基础能力,date-fns 处理日期逻辑,Ant Design 提供 UI 组件支持
  • 核心逻辑:跨天事件时间格式化、日期事件数量统计、日期权限校验是实现的关键
  • 扩展方向:完善表单交互、对接后端接口、优化性能是后续开发的重点
相关推荐
C_心欲无痕2 小时前
react - 组件之间的通信
前端·javascript·react.js
走粥2 小时前
JavaScript Promise
开发语言·前端·javascript
-CRzy2 小时前
CTF之web-信息收集
前端
神算大模型APi--天枢6462 小时前
合规落地加速期,大模型后端开发与部署的实战指南
大数据·前端·人工智能·架构·硬件架构
四瓣纸鹤2 小时前
F2图表柱状图添加文本标注
前端·javascript·antv/f2
inferno2 小时前
HTML基础(第二部分)
前端·html
Dreamcatcher_AC2 小时前
Ajax技术:前后端交互全解析
前端·ajax
韭菜炒大葱2 小时前
TailwindCSS:从“样式民工”到“UI乐高大师”的逆袭
前端·面试·编程语言
whyfail2 小时前
CSS实现水滴样式
前端·css