FullCalendar日历插件高度定制化实践

517 阅读8分钟

最近由于客户需求需要做一个高度可定制化的日历,于是机缘巧合之下我认识了FullCalendar这个插件,查了相关中文版的使用教程,发现大部分讲解的其实不是很详细,并且无法满足我的需求,没办法只能生啃官方的英文文档了,下面我将我的一些使用经验分享给大家,也是踩了不少坑......话不多说直接进入正题

FullCalendar官网

先看最终效果实现:

嘿嘿是不是看着功能还是挺强大的,下面将讲解具体的一个实现思路

首先我这边是直接把对应的js文件下载下来了通过script标签引入的,版本号是v6.1.13,如果你是使用的是vue、react、angular等框架,不用担心官方文档中也有对应的教程,例如Vue版本教程,即使你们使用的方式与我不一样,也可以继续观看下面的内容,或许能够帮助你理解这个插件的大致作用

总体可分为3个步骤: 插件本地化 >> 插件初始化 >> 动态更新

第一步 插件本地化

由于默认显示的是英文,因此我需要先设置成中文,如果你需要其他语言的支持可查看官方文档

具体代码如下:

// 为 FullCalendar 添加多种语言支持
function setFullCalendarLocales() {
  // 每个本地化配置对象定义了该语言环境下插件的文本和日期格式
  window.FullCalendar.globalLocales.push({
      // 表示语言代码
      code: 'zh-cn',
      // 包含一周配置的对象
      week: {
          // 表示一周的第一天是星期一(周一),符合 ISO 8601 标准
          dow: 1,
          // 表示包含 1 月 4 日的周是该年的第一周
          doy: 4,
      },
      // 定义日历头部按钮的文本
      buttonText: {
          // 上月按钮的文本
          prev: '上月',
          // 下月按钮的文本
          next: '下月',
          // 今天按钮的文本
          today: '今天',
          // 月视图按钮的文本
          month: '月',
          // 周视图按钮的文本
          week: '周',
          // 日视图按钮的文本
          day: '日',
          // 日程视图按钮的文本
          list: '日程',
      },
      // 周视图的标题文本
      weekText: '周',
      // 全天事件的文本
      allDayText: '全天',
      // 一个函数,返回显示额外事件的文本。n 是额外事件的数量
      moreLinkText: (n:any) => '另外 ' + n + ' 个',
      // 没有事件时的显示文
      noEventsText: '没有事件显示',
  });
}

第二步 插件初始化

设置成中文后这个时候我们就需要把我们的日历渲染到界面上了

具体代码如下: 

// 插件初始化后的实例
let _calendar=null

// 创建日历
function createCalendar() {
  // 获取日历容器
  const _calendarEl = document.querySelector("#calendar");

  _calendar = new window.FullCalendar.Calendar(_calendarEl, {
    // 全局配置
    // 允许用户编辑事件(拖动和调整大小)
    editable: true,
    // 不显示全天事件栏
    allDaySlot: false,
    // 使用本地时区
    timeZone: "local",
    // 使用简体中文本地化
    locale: "zh-cn",
    // 初始视图模式为周视图
    initialView: "timeGridWeek",
    // 不显示头部工具栏
    headerToolbar: null,
    // 允许监听窗口大小变化 以改变日历的大小
    handleWindowResize: true,

    views: {
      // 针对于周视图的配置 (可覆盖全局配置)
      // 对于年视图、月视图、日视图也可以在 views 中单独配置,数据都是一样的,只不过可以用年份展示也可以用月份展示等
      timeGridWeek: {
        // 时间间隔默认为30分钟
        slotDuration: "00:05:00",
        // 日期视图左侧时间轴多长间隔显示一条日期文字(默认跟着slotDuration走的,可自定义)
        slotLabelInterval: "00:30:00",
        // 每天的开始时间
        slotMinTime: "05:00:00",
        // 每天的结束时间
        slotMaxTime: "21:00:00",
        // 自定义每个时间段的高度
        // slotHeight: 24,
        // 是否显示周末
        weekends: true,
        // 周/日视图中显示今天当前时间点(以红线标记),默认false不显示
        nowIndicator: false,
        // 隐藏一周中的某一天或某几天 0代表周末
        hiddenDays:[0,1],
        // 是否在日历中显示周次(一年中的第几周)
        weekNumbers: false,
        // 重叠数据条数太多时,限制各自里显示的数据条数(多余的以“+2more”格式显示),默认false不限制,支持输入数字设定固定的显示条数
        eventLimit: true,
        // 当一块区域内容太多以"+2 more"格式显示时,这个more的名称自定义
        eventLimitText  : "更多",
        // 点开"+2 more"弹出的小窗口标题,与eventLimitClick可以结合用
        dayPopoverFormat : "YYYY年M月d日",
        // 在Event Object中如果没有end参数时使用,如start=7:00pm,则该日程对象时间范围就是7:00~8:00
        defaultTimedEventDuration : "01:00:00",
        // 自定义日历横向头部标
        dayHeaderContent: dayHeaderContentCallback,
        // 初始化事件内容回调
        eventContent: eventContentCallback,
        // 在视图添加到 DOM 后立即调用
        eventDidMount: eventDidMountCallback,
        // 调整事件时间间隔回调
        eventResize: eventChangeCallback,
        // 拖动完成事件回调
        eventDrop: eventChangeCallback,
      },
    },
    // 事件数据 (初始化时自动调用此函数获取日历对应的数据)
    events: initEventDataCallback,
  });

  // 开始渲染
  _calendar.render();
  // 如果容器能滚动将每次滚动至开始时间
  _calendar.scrollToTime("05:00:00");
},

这里需要强调的是创建中定义的几个回调函数的执行顺序
initEventDataCallback>>dayHeaderContentCallback>>eventContentCallback>>eventDidMountCallback

比较特殊的是eventChangeCallback,他是在渲染完成后,用户拖动时间块或者下拉时间块改变结束时间的位置才会触发的

具体代码如下:

// 讲后端数据转换为FullCalendar插件所需要的数据,并给到插件
function initEventDataCallback(dateInfo, success, fail) {
  // 这里假设 schedulingDataList 是从后端获取的数据
  const data = []

  // FullCalendar插件所需要的数据
  const eventList = []

  data.forEach((item) => {
    // 利用dayjs这个库获取星期几对应的日期
    const startDate = dayjs(dateInfo.start)
      .day(~~item.day_no)
      .format("YYYY-MM-DD");

      const _start = dayjs(`${startDate} ${item.start_time}`)
        .second(0)
        .format("YYYY-MM-DD HH:mm:ss");
      const _end = dayjs(`${startDate} ${item.end_time}`)
        .second(0)
        .format("YYYY-MM-DD HH:mm:ss");

    const event = {
      // 事件唯一标识 可通过 _calendar 的 `getEventById` 方法获取事件
      id: '',
      // 事件标题,途中会经过 `eventContent` 回调来进行处理自定义的时间块显示内容
      title: '',
      // 事件开始时间 注意格式为 `YYYY-MM-DD HH:mm:ss`
      start: _start,
      // 事件结束时间 注意格式为 `YYYY-MM-DD HH:mm:ss`
      end: _end,
      // 是否允许拖动
      editable: false,
      // 是否允许拖动开始时间 覆盖全局的 eventStartEditable 选项
      startEditable: false,
      // 用来同时指定 backgroundColor 和 borderColor
      color: "#d3d3d3",
      // 是否允许与其他时间块重叠
      overlap: true,
      // 是否允许调整时间块的大小,也就是是否能够拖动边缘来改变时间间隔,覆盖全局的 eventResourceEditable 选项
      durationEditable: false,
      // 添加自定义类名样式,也可以使用 `eventClassNames` 回调实现
      className: "calendar-event",
      classNames: ["calendar-event"],
      // 自定义属性 可从 `_calendar.getEventById('xxx').extendedProps` 中获取
      userName: item.user_name,
    };

    eventList.push(event)
  });

  // 成功回调,会将这些数据渲染到日历上
  success(eventList);
},

// 自定义日历横向头部标回调函数
function dayHeaderContentCallback(arg) {
  const _weekDaysHeaders = [
    "日",
    "一",
    "二",
    "三",
    "四",
    "五",
    "六",
  ];
  return `周${_weekDaysHeaders[arg.dow]}`;
},

// 事件内容回调函数,在这里面可以自定义时间块的显示内容,除了传递html外,还可以传递dom元素
function eventContentCallback(arg, createElement) {
  return {
    html: `<div id="timeText-${arg.event.id}">${arg.timeText}</div>`,
  };
},

// 点击时间文本的回调函数
function eventTimeTextClickCallback(event) {
  console.log(event);
},

// 事件渲染完成后的回调函数,此时 `eventContentCallback` 中的自定义元素已经渲染到页面上了
function eventDidMountCallback(e) {
  const { event, el } = e;

  // 将每个时间块的dom元素存储到事件的 `extendedProps` 中,`extendedProps` 是专门用于存放你定义的自定义属性的
  event.setExtendedProp("el", el);

  // 拿到时间块的id(唯一标识)
  const _id = event.id;
  // 获取并绑定 `eventContentCallback` 回调中所自定义元素的点击事件
  const _timeTextEl = document.getElementById(`timeText-${_id}`);
  // 如果存在时间元素
  if (_timeTextEl) {

    if (!_timeTextEl?.fn) {
      _timeTextEl.fn = eventTimeTextClickCallback.bind(this, event);
    }

    _timeTextEl.removeEventListener("click", _timeTextEl.fn);
    _timeTextEl.addEventListener("click", _timeTextEl.fn);

  }
},

// 拖拽和调整事件时间间隔大小回调函数
function eventChangeCallback(e) {
  console.log('e:',e);

  // 是否回退操作
  const isRevert = false

  // 例如在拖动或者下拉完毕后,发现时间块与其他时间块重叠,可以回退操作
  if (isRevert) {
    e.revert();
    return;
  }
  // 可以设置动态设置 _calendar 初始化时的全局配置,例如这里修改了 `dayHeaderContent` 的回调函数,这个时候会立即调用回调函数
  _calendar.setOption('dayHeaderContent', ()=>  console.log('e:',e));
},

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 > "点击并拖拽以移动")

ok这个时候理论上界面上就应该会显示日历了

**第三步 动态更新**

我们通常都会去对日历进行交互操作,这个时候就需要把日历所对应的数据也进行同步,下面是我用到的一些方法,可能不是很完整你们可做参考

// 添加新的时间块
const newEvent = {
  id: '',
  title: '',
  start: '2024-11-02 05:00:00',
  end: '2024-11-02 22:00:00',
  className: "xxx",
  // ... 还可以添加其他属性
};
_calendar.addEvent(newEvent);

// 添加多个新的时间块
_calendar.addEventSource([newEvent,newEvent])

// 根据时间块的唯一标识id获取对象配置
const _event = _calendar.getEventById('');

// 设置开始时间和结束时间
_event.setDates('2024-11-02 05:00:00', '2024-11-02 22:00:00');

// 设置时间块对象的属性值,例如 color 等
_event.setProp("backgroundColor", "#43CF7C");

// 设置自定义属性的值
_event.setExtendedProp("xxx", 'xxx');

// 删除对应的时间块
_event.remove()

// 渲染日历
_calendar.render();

// 设置 _calendar 的配置
_calendar.setOption('dayHeaderContent', ()=>  console.log('e:',e));

// 如果日历允许滚动,将会滚动到指定时间的位置
_calendar.scrollToTime('2024-11-02 05:00:00');

// 获取当前所有时间块的数据
_calendar.getEvents()

// 当前视图下对应今天的开始时间
_calendar.view.activeStart

// 当前视图下对应今天的结束时间
_calendar.view.activeEnd

// 销毁日历实例
_calendar.destroy()

ok终于讲完了,通过这些方式,已经能够定制属于你的个性化日历了,希望能够帮助到你!