Bootstrap

FullCalendar日历组件集成系列4——事件加载

加载事件

FullCalendar中,事件是核心概念,与通常我们说的日程项是一个概念。
当前我是基于FullCalendar实现一个任务管理系统,任务对应着事件的概念。
FullCalendar官方事件对应的介绍参见https://fullcalendar.io/docs/event-object
image.png
注意:上图未截全,实际属性更多一些。
核心属性是三个,标题title、开始时间start和结束时间end,此外,唯一性标识id组件也很贴心的预置了,用来关联前后端数据,例如在点击事件时调用后端服务,将id属性作为参数传入来查询数据。

我们可以调用后端服务,一次性把所有任务数据都传回了前端,由前端将任务数据转换为FullCalendar的事件对象显示在日历中。这种模式系统刚开始使用,数据量比较小,系统会流畅运行。但随着时间的推移,数据越来越多,性能上可能存在问题,因此需要按需加载数据。
因官方文档过于简略,这地方花了很长时间摸索,详细说说。

功能需求

1.初始化页面时调用后端服务加载数据
2.点击头部按钮工具栏时,进行视图切换(月、周、日)或点击前一个、后一个时,调用后端服务来加载数据。

方案探索

按需加载的关键在于拿到起止时间。
通过查看官方文档,当前视图的起止时间,组件模型倒是提供了基础数据模型支撑(https://fullcalendar.io/docs/view-object),在视图对象中有几个关键属性:
activeStart:可见开始时间
activeEnd:可见结束时间
currentStart:真实开始时间
currentEnd:真实开始时间
两类起止时间,差异在于前者是可见,后者是真实,以月视图为例,默认是显示6行,42天。
image.png
以上图为例,activeStart是5月27日,activeEnd是7月7日,currentStart和currentEnd则是6月1日和6月30日。
结合上面例子,我们想控制后端返回数据的范围,使用的属性应该是activeStart和activeEnd。

接下来就在于如何拿到起止时间了,并且在视图切换时触发调用后端服务来获取数据。
翻了很长时间的官方文档,发现组件预置的几个按钮,没有暴漏接口出来,要想添加自己的逻辑,能想到的曲线救国的方式,就是使用自定义按钮去覆写整个头部工具栏,以上一个按钮为例:

 customButtons: {
    changeShowScopeButton: {
      text: '显示全部',
      click: this.changeShowScope
    },
    myPreButton: {
      text: '‹',
      click: this.navPre
    }
  }
 navPre(e) {
      const view = this.$refs.fullCalendar.calendar.view
      ……     
 }

这么做缺点明显,很繁琐……

进一步探索,组件自身是否在视图呈现时回调方法,获取到视图对象,拿到起止时间。
还真找到了https://fullcalendar.io/docs/view-render-hooks
添加属性与方法,如下:

// 视图展示回调
viewDidMount: this.viewDidMount

viewDidMount(view) {
  console.log(view)
}

测试了下,只有视图加载时才会触发,同一视图,如月视图,点击上一个或下一个按钮,都不会触发回调。

此外,还有一个隐含的关键问题,头部工具栏最右侧的四个切换按钮实际来自于三个不同的视图插件,周视图和日视图是公用一个插件timeGrid。而机制是只有视图切换时才会触发,因此日视图和周视图之间切换,并不会触发回调。这样就无法实现我们的功能需求了。

方案确认

一度打算采用最初的思路,重写头部工具栏的方式来实现,工作量略大,但整体上可行。
后来在系统地查看官方文档时,突然从一个角落找到了解决方案,即通过函数的方式来获取事件数据源。
官方文档:https://fullcalendar.io/docs/events-function
不得不说,太隐蔽了,当时查看文档时,仅当成一种提供事件数据源方式,没有点开细看。
按照官方说明,通过events属性指定一个回调方法,当用户点击上一个、下一个或者切换视图时触发,并且回调时会传入开始时间和结束时间。

做了下验证:

// 加载事件数据
events: this.loadEvent

// 加载事件数据
loadEvent(fetchInfo, successCallback, failureCallback) {
  this.startTime = this.$dateFormatter.formatUTCTime(fetchInfo.start)
  this.endTime = this.$dateFormatter.formatUTCTime(fetchInfo.end)
  console.log('loadEvent', this.startTime, this.endTime)
}   

打开控制台,点击按钮测试,结果如下:
image.png
组件内置的按钮(今天、上一个、下一个、月视图、周视图、日视图和列表视图),均能触发回调,并且内部做了逻辑判断,只有当前展示的数据本地没有,才会发起回调。
怎么理解呢?例如先加载了月视图,这时候拿到了一个月的事件数据,如果这时候切换到周视图,如果当前显示的周数据没超出已获取到的一个月范围内,则不会发起回调,如果超出,才会回调,这些细节只有测试和验证才能获取到。

方案实现

基于上述方案,实现如下:

// 加载事件数据
events: this.loadEvent

// 加载事件数据
loadEvent(fetchInfo, successCallback, failureCallback) {
  const startTime = this.$dateFormatter.formatUTCTime(fetchInfo.start)
  const endTime = this.$dateFormatter.formatUTCTime(fetchInfo.end)
  this.$api.personaltask.task.listWithScope(startTime, endTime).then((res) => {
    if (res.data) {
      const eventArray = res.data.map((task) => {
        return this.convertTaskToEvent(task)
      })
      successCallback(eventArray)
    }
  })
}

后端处理

前面实现了功能,不过说的主要的前端的事。
后端取数也比较重要,在这补充说明下。
后端取数逻辑需要考虑周全,涉及到四个时间的比较。
任务开始时间
任务结束时间
查询范围开始时间(FullCalendar组件)
查询范围结束时间(FullCalendar组件)

假设任务的起止时间都有值的情况下,实际需要考虑三种情况:

  • 任务开始时间>=区域开始时间且任务开始时间<=区域结束时间
  • 任务结束时间>=区域开始时间且任务结束时间<=区域结束时间
  • 任务开始时间<=区域开始时间且任务结束时间>=区域结束时间

即任务开始时间落在区域时间范围内,或任务结束时间落在区域时间范围内,或任务起止时间包含在区域时间内部。
服务方法如下:

 @Override
public List<Task> listWithScope(LocalDateTime startTime, LocalDateTime endTime) {

    QueryWrapper<Task> queryWrapper = new QueryWrapper<>();
    queryWrapper.lambda()
            // 任务开始时间落在区域范围内,即任务开始时间>=区域开始时间且任务开始时间<=区域结束时间
            .and(x -> x.ge(Task::getStartTime, startTime).le(Task::getStartTime, endTime))
            // 或者 任务结束时间落在区域范围内,即任务结束时间>=区域开始时间且任务结束时间<=区域结束时间
            .or(x -> x.ge(Task::getEndTime, startTime).le(Task::getEndTime, endTime))
            // 或者 任务起止时间包含区域范围,即任务开始时间<=区域开始时间且任务结束时间>=区域结束时间
            .or(x -> x.le(Task::getStartTime, startTime).ge(Task::getEndTime, endTime))
    ;
    return this.list(queryWrapper);

}

此外,任务的开始时间和结束时间并非必填项,有些业务场景会只设置其中一项:

  • 场景1:任务耗时极短,我们只需要标记开始时间即可,无需设置结束时间,相当于起到备忘功能,即开始时间有值,结束时间为空
  • 场景2:任务耗时不确定,开始时间可以安排,结束时间无法计划,即开始时间有值,结束时间为空
  • 场景3:任务有截止时间,但尚未安排什么时间开始做,即开始时间为空,结束时间有值

以上3个场景对应两类情况,上面的后端处理逻辑仍能获取期望范围内的数据。

但是,FullCalendar组件这边发现存在问题。
对于场景1和2,开始时间有值,结束时间为空,会在视图中显示一条只有开始时间,没有结束时间的事件,正常。
对于场景3,开始时间为空,结束时间有值,该事件不会在视图中显示,就跟不存在一样。

思考了下,对于场景3,不适合由系统自动补全一个开始时间,例如参照结束时间,会跟初衷相违背。此时,由用户注意这点,设置一个相对宽泛的开始时间,比如周五截止的任务,设置周一为开始时间,或者干脆就设置截止时间同一天,但作为日程安排的制定人,对任务的情况是清楚的,也没影响到用户体验。

注:至于起止时间都为空,往往是一项尚未拆解的大任务大目标,或者作为任务的归类,没有显示在日历中的必要。

应用系统

名称:遇见
地址:https://meet.popsoft.tech
说明:基于一二三应用开发平台和FullCalendar日历组件实现的面向个人的时间管理、任务管理系统,1分钟注册,完整功能,欢迎使用~

;