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 组件支持
  • 核心逻辑:跨天事件时间格式化、日期事件数量统计、日期权限校验是实现的关键
  • 扩展方向:完善表单交互、对接后端接口、优化性能是后续开发的重点
相关推荐
却尘3 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare3 小时前
浅浅看一下设计模式
前端
Lee川3 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix4 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人4 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl4 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人4 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼4 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空4 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust